I'm receiving a static analysis error, and I'm not sure if it can be safely ignored, or if I can improve the design to remove it without to much change, this is legacy code.
This does NOT use ARC.
-(id) initCustom{
NSString* key = #"foo";
NSData* objectData = nil;
objectData = [[NSUserDefaults standardUserDefaults] objectForKey:key];
if( objectData != nil)
{
//If this path is taken the error occurs
self = [NSKeyedUnarchiver unarchiveObjectWithData:objectData];
}
else
{
self = [super init];
}
if (self)
{
//Static analysis warns here
m_fiz = [[NSString alloc] initWithString:#"bar"];
//Instance variable used while 'self' is not set to the result of '[(super or self) init....]'
}
}
My understanding is that [NSKeyedUnarchiver unarchiveObjectWithData:objectData] will cause the "initWithCoder" to be called. This object implements NSCoding, and has the proper methods required by NSCoding implemented.
Is this a false positive from the static analysis, or can I make it better?
The compiler is warning because that is a bizarre way of doing this. :)
Move the "read from defaults or create new" logic into a class method. That will both fix the compiler warning message and be a more consistent pattern.
+ (instancetype) defaultThingamahoover
{
... check defaults database and unarchive ...
... or return new ....
}
BTW: In general, you don't want to shove anything large into the defaults database. It is pretty atypical to archive an object and stick it in there. The defaults database is generally intended for small key/value pairs.
Related
My app currently uses this deprecated function:
id unarchivedObject=[NSKeyedUnarchiver unarchiveObjectWithData:codedData];
if([unarchivedObject isKindOfClass:[NSDictionary class]]){
// currently returns TRUE when reading existing user data.
}
To update, I've converted to this:
id unarchivedObject=[NSKeyedUnarchiver unarchivedObjectOfClass:[NSDictionary class] fromData:codedData error:nil];
if([unarchivedObject isKindOfClass:[NSDictionary class]]){
// currently returns FALSE when reading existing user data.
}
The data was originally encoded like this:
-(void)encodeWithCoder:(NSCoder*)encoder{
[encoder encodeObject:text forKey:#"text"];
}
-(instancetype)initWithCoder:(NSCoder*)decoder{
if(self=[super init]){
text=[decoder decodeObjectForKey:#"text"];
}
What could be causing the IF statement to return FALSE using the newer code?
Please note that I am concerned primarily with reading existing data stored prior to deprecating the Archiving functions. Simply changing to the newer functions does not resolve the issue.
Interesting question! I've been supporting iOS 10.0 so I haven't encountered such issue until I saw this. I was tinkering for an hour and I successfully found the issue.
What could be causing the IF statement to return FALSE using the newer
code?
It's because your unarchivedObject object is nil!
If you use the parameter error in the new method, you would see an error like this:
Error Domain=NSCocoaErrorDomain Code=4864 "This decoder will only
decode classes that adopt NSSecureCoding. Class 'QTPerson' does not
adopt it." UserInfo={NSDebugDescription=This decoder will only decode
classes that adopt NSSecureCoding. Class 'QTPerson' does not adopt it.
But how do we get the correct value for this unarchivedObject and not nil? It would take a couple of steps.
First off, make your model/class conform to <NSCoding, NSSecureCoding>
Example:
QTPerson.h
#import <Foundation/Foundation.h>
#class QTPerson;
NS_ASSUME_NONNULL_BEGIN
#pragma mark - Object interfaces
#interface QTPerson : NSObject <NSCoding, NSSecureCoding>
#property (nonatomic, copy) NSString *text;
#end
NS_ASSUME_NONNULL_END
And then implement the protocol methods:
QTPerson.m
#import "QTPerson.h"
#implementation QTPerson
+ (BOOL)supportsSecureCoding {
return YES;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:_text forKey:#"text"];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super init];
if (self) {
_text = [coder decodeObjectOfClass:[NSString class] forKey:#"text"];
}
return self;
}
#end
And then when archiving an object, you would want to pass YES to the parameter requiringSecureCoding, like so:
QTPerson *person = [[QTPerson alloc] init];
person.text = #"Glenn";
NSData *codedData1 = [NSKeyedArchiver archivedDataWithRootObject:person requiringSecureCoding:YES error:nil];
[[NSUserDefaults standardUserDefaults] setValue:codedData1 forKey:#"boom"];
Lastly, when unarchiving, just do what you did correctly, like so:
NSData *codedData = [[NSUserDefaults standardUserDefaults] dataForKey:#"boom"];
NSError *er;
id unarchivedObject=[NSKeyedUnarchiver unarchivedObjectOfClass:[QTPerson class] fromData:codedData error:&er];
if([unarchivedObject isKindOfClass:[QTPerson class]]){
NSLog(#"TRUE!");
} else {
NSLog(#"FALSE!");
}
Voila! You'll get nonnull object unarchivedObject, hence the TRUE/YES value you're looking for!
So this is very odd. I just created a new singleton in my app, using the same pattern I have used for the numerous other singletons. This one, however, doesn't play nice with the debugger. Here's the code for getting the singleton:
- (id)init
{
self = [super init];
if (self) {
[self loadData];
}
return self;
}
+ (Settings *)sharedInstance
{
static Settings *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
// Do any other initialisation stuff here
});
return sharedInstance;
}
Now when I try to access the object from the debugger, it gives me this:
error: Execution was interrupted, reason: EXC_BAD_ACCESS (code=1, address=0x20).
The process has been returned to the state before expression evaluation.
Obviously, I wasn't trying to just get a handle on the singleton from the debugger, I was trying to get a property. This gives some even more interesting output:
(lldb) po [Settings sharedInstance].jpegCompressionLevel
error: warning: got name from symbols: Settings
warning: receiver type 'void *' is not 'id' or interface pointer, consider casting it to 'id'
error: no known method '-sharedInstance'; cast the message send to the method's return type
error: 1 errors parsing expression
What in the world is going on here? Calls from within code seem to go just fine. The debugger constantly fails though. In the same context, I'm able to access other singletons (using the same pattern) just fine.
It may be worth noting that the loadData method just loads a dictionary from disk, and that the getters for the properties on the class utilise the values in that dictionary, rather than ivars.
Here's the code for loadData:
-(void)loadData
{
// Load data from persistent storage
NSString *path = [self pathToDataFile];
NSMutableDictionary *settingsDict = [NSMutableDictionary dictionaryWithDictionary:[NSKeyedUnarchiver unarchiveObjectWithFile:path]];
_settingsDict = settingsDict;
}
Now I can't explain why, but renaming the class from 'Settings' to something else fixed the issue. I can't see anything in the Symbol Navigator that would clash with this name... very weird indeed.
I've been stuck on this for approximately two weeks. I hate posting things that have been asked a lot but I really have gone through them all.
I used Ray Wenderlich's tutorial for saving data in an iPhone app.
http://www.raywenderlich.com/tutorials
So that is the setup I have going on in my app. I'm saving very simple objects. My Card object consists of a name, type, and image. That's all. So the tutorial is quite close to mine. Which is making this more frustrating.
The thing is, I have some NSLog statements in there for loading. I have it displaying the folder it's using to load and what objects it does load. Right now it is displaying this.
Loading cards from /Users/zach/Library/Application Support/iPhone Simulator/7.0.3-64/Applications/E3DB01FD-A37E-4A69-840B-43830F2BDE2C/Library/Private Documents
2013-11-04 00:02:50.073 CardMinder[84170:a0b] ()
So it seems to be trying to load them, but there's nothing there to load. Here is my function to save data.
- (void)saveData {
if (_data == nil) return;
[self createDataPath];
NSString *dataPath = [_docPath stringByAppendingPathComponent:kDataFile];
NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[archiver encodeObject:_data forKey:kDataKey];
[archiver finishEncoding];
NSLog(#"%#",dataPath);
NSLog(#"%#",data);
[data writeToFile:dataPath atomically:YES];
}
Which is really just what's posted in that tutorial. I know if you feel generous enough to help me out i'll have to post some more code but I don't want to flood the post with useless stuff so just let me know and i'll get it out here.
I really appreciate anyone that can help, I have recently entered the desperation state and need help.
Thanks
UPDATE
NSError *error;
[data writeToFile:dataPath options:NSDataWritingAtomic error:&error];
NSLog(#"error: %#", error.localizedFailureReason);
These are the methods for the CardData class. I'm doing the name, type, and a bool here.
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:_name forKey:kNameKey];
[aCoder encodeObject:_cardType forKey:kTypeKey];
[aCoder encodeBool:_checkedOut forKey:kOutKey];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
NSString *name = [aDecoder decodeObjectForKey:kNameKey];
NSString *cardType = [aDecoder decodeObjectForKey:kTypeKey];
BOOL checkedOut = [aDecoder decodeBoolForKey:kOutKey];
return [self initWithName:name cardType:cardType _Bool:checkedOut];
}
UPDATE 2
I just put some more NSLog statements in and I found out that when I press the "Save card" button in my app, it doesn't seem to execute the saveData function at all. I have log statements galore in that saveData function and when I click the saveCard button it doesn't show any of those logs. Why would that be happening?
This is my saveButton code.
- (IBAction)saveNewCard:(id)sender
{
NSString *cardName = self.nameField.text;
_cardDoc.data.name = cardName;
CardDoc *newCard = [[CardDoc alloc] initWithName:cardName cardType:cardTypeString _Bool:NO image:chosenIcon];
[_cardDoc saveData];
NSLog(#"Card save button pressed!");
CardViewController *cardViewController = (CardViewController *)[self.navigationController.viewControllers objectAtIndex:self.navigationController.viewControllers.count-2];
[cardViewController.cards addObject:newCard];
[self.navigationController popToRootViewControllerAnimated:YES];
}
You should use writeToFile:options:error: instead of writeToFile:atomically:; that will give you an error message that should prove helpful. (The equivalent to atomically:YES is the option constant NSDataWritingAtomic.) Make sure you're getting back a return value of YES; if not, the error should be set.
If you're getting a value of NO but the error is not set, it means you're messaging nil. A quirk of Objective-C is that messaging nil is completely valid. If the method is defined to return something, you'll even get a result: 0 or equivalent (NO, nil, etc.)
In this case, you're messaging _cardDoc. There's no return result to detect. This is a bit harder to defensively code around, but [_cardDoc saveData] is actually [nil saveData]. The debugger will just breeze past the line.
Generally, if something absolutely should not be nil, you can use NSAssert:
NSAssert(_cardData, #"_cardData should not be nil");
[_cardData saveData];
But use this sparingly; you'll probably come to usually appreciate this behaviour.
A few things.
Post the results of your log statements so we know what you are seeing.
In order for your approach to work, your _data object needs to conform to the NSCoding protocol. That means you need to add the protocol declaration to your interface, and implement the methods encodeWithCoder and initWithCoder.
In those methods you need to save all the state data for your object / load the state back into your object.
Those methods are the most likely source of problems with your code. Post those methods if you need help with them, and walk though them in the debugger.
You might also look at the NSKeyedArchvier class method archivedDataWithRootObject. That method takes an object and encodes it into an NSData object in one step. The method archiveRootObject:toFile: take it a step further, and writes the data directly to a file for you.
NSKeyedUnarchiver has the corresponding methods unarchiveObjectWithData and unarchiveObjectWithFile to recreate your object from data/a file.
I have created a FacebookManager singleton that gets called on a background thread when my app launches. Everything is working just fine with the facebook manager the singleton, the app etc. However, when the app first launches, it is quite a few seconds before it is useful because the facebook manager has not finished doing its thing yet. So what I want to do, is use NSKeyedArchiver to save the facebookManager and all its dictionaries so that upon launch, the app has a navigable interface while the facebook data is being updated in the background. Make sense?
All within the FacebookManager.m, first, when the manager is done updating the friends dictionaries, etc, I call the method that saves the data:
- (BOOL)saveFacebookData
{
// returns success or failure
NSString *path = [self archivePath]; // just a helper method
return [NSKeyedArchiver archiveRootObject:self toFile:path];
}
Then in init, I am trying this, which doesn't seem to work. :
-(id)init
{
self = [super init];
NSString *path = [self archivePath];
self = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
// If the manager hadn't been saved previously, create a new new one
if (!self) {
if (_idsByNameDict == nil) {
_idsByNameDict = [[NSMutableDictionary alloc] init];
}
if (_photosByNameDict == nil) {
_photosByNameDict = [[NSMutableDictionary alloc] init];
}
if (_installedByNameDict == nil) {
_installedByNameDict = [[NSMutableDictionary alloc] init];
}
if (_allFriendsArray == nil) {
_allFriendsArray = [[NSArray alloc] init];
}
basicPermissions = NO;
extendedPermissions = NO;
// Create synchronous dispatch queue for all facebook activity
if (_facebookUpdateQueue == nil) {
_facebookUpdateQueue = dispatch_queue_create("com.facebookUpdateQueue", NULL);
}
}
I think my general strategy is sound but I am tripping over how to actually grab the archived version of the manager during init! Any advice?
Your class needs to implement <NSCoding> and both of its methods encodeWithCoder: to archive all of your property values and initWithCoder: to in archive them. Make sure to call super in the implementations. Generally, the class using the archived class would know about the archiving but you could hide that knowledge in init by using initForReadingWithData: to create your NSKeyedUnarchiver and then calling [self initWithCoder:...];.
I'm basically implementing a fancier NSURLConnection class that downloads data from a server parses it into a dictionary, and returns an NSDictionary of the data. I'm trying add a completion block option (in addition to a delegate option), but it crashes anytime I try to store that data in another class.
[dataFetcher_ fetchDataWithURL:testURL completionHandler:^(NSDictionary *data, NSInteger error) {
contentDictionary_ = data;
}];
I can NSLog that data just fine, and basically do whatever I want with it, but as soon as I try to save it into another variable it crashes with a really obscure message.
EDIT: the crash message is EXC_BAD_ACCESS, but the stack trace is 0x00000000 error: address doesn't contain a section that points to a section in a object file.
I'm calling this function in the init method of a singleton. It DOES let me save the data if I set this in the completion block.
[SingletonClass sharedInstance].contentDictionary = data
But then the app gets stuck forever because sharedInstance hasn't returned yet, so the singleton object is still nil, so sharedInstance in the completion block calls init again, over and over.
EDIT 2: The singleton code looks like this:
+ (SingletonClass*)sharedInstance {
static SingletonClass *instance;
if (!instance) {
instance = [[SingletonClass alloc] init];
}
return instance;
}
- (id)init {
self = [super init];
if (self) {
dataFetcher_ = [[DataFetcher alloc] init];
NSString *testURL = #"..."
[dataFetcher_ fetchDataWithURL:testURL completionHandler:^(NSDictionary *data, NSInteger error) {
[SingletonClass sharedInstance].contentDictionary = data;
}];
}
return self;
}
Like I said, this works fine but repeats the initialize code over and over until the app crashes. This only happens the first time I run the app on a device, because I cache the data returned and it doesn't crash once I have the data cached. I would like to be able to just say self.contentDictionary = data, but that crashes.
Specify a variable to be used in the block with the __block directive outside of the block:
__block NSDictionary *contentDictionary_;
[dataFetcher_ fetchDataWithURL:testURL completionHandler:^(NSDictionary *data, NSInteger error) {
contentDictionary_ = data;
}];
You're invoking recursion before ever setting the "instance". (which I now see you understand from OP).
In your block, you can use the ivar or an accessor instead of
[SingletonClass sharedInstance].contentDictionary
use:
_contentDictionary = [data copy]; or self.contentDictionary=data;
assuming that the ivar backing the contentDictionary property is _contentDictionary.
It sounds like you tried self.contentDictionary and it failed? I got it to work in a test, with ARC turned, so there may be something about your dataFetcher that is affecting this. In my test dataFetcher just returns a dictionary with a single element.
Turns out the issue was with a bunch of different parts. My URL was empty sometimes, and my data fetcher would just fail immediately and call the completion block. In my completion block I hadn't included any error handling, so if the singleton class hadn't initialized, it would repeat forever. With a real URL this doesn't happen.
I still would like to figure out why it crashes when I try to assign the data to an ivar, though.