Objective C: save array of items (NSCoder) - ios

I am working on challenges from iOS Big Nerd Ranch book of 12th chapter and there is a problem of saving an array of items to the disk. I have BNRDrawView UIView that has an array finishedLines that holds items BNRLine defined by me. I want to use NSCoder for the purpose. So, I implemented inside BNRDrawView two methods:
- (void) encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:_finishedLines forKey:finishedLinesKey];
}
- (id)initWithCoder:(NSCoder *)decoder {
self = [super initWithCoder:decoder];
if (self) {
_finishedLines = [decoder decodeObjectForKey:finishedLinesKey];
}
return self;
}
and trying to save finishedLines array whenever it is changed like this:
[NSKeyedArchiver archiveRootObject:self.finishedLines
toFile:self.finishedLinesPath];
Loading I am trying to do inside initWithFrame BNRDrawView method:
- (instancetype)initWithFrame:(CGRect)r
{
...
self.finishedLinesPath = #"/Users/nikitavlasenko/Desktop/XCodeProjects/MyFirstApp/TouchTracker/savedLines/finishedLines";
BOOL fileExists = [[NSFileManager defaultManager]
fileExistsAtPath:self.finishedLinesPath];
if (fileExists) {
self.finishedLines = [NSKeyedUnarchiver
unarchiveObjectWithFile:self.finishedLinesPath];
}
...
It gives me the error:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[BNRLine encodeWithCoder:]: unrecognized selector sent to instance 0x7fbd32510c40'
From here I have two questions:
Do I need to implement encodeWithCoder: and initWithCoder: for ALL of the classes - let's suppose that I have a plenty of different ones, defined by me - that are inside my array that I am trying to save?
If you look above to my initWithCoder: method, you can see that I need to call [super initWithCoder:decoder], but do I also need to call BNRDrawView's initializer method initWithFrame: there?

Yes, you need to implement NSCoder in all objects that are stored inside the array.
No, you don't need to call initWithFrame - initWithCoder is sufficient.

Related

Inherit from NSNotification

I want to create a subclass of NSNotification. I don't want to create a category or anything else.
As you may know NSNotification is a Class Cluster, like NSArray or NSString.
I'm aware that a subclass of a cluster class needs to:
Declare its own storage
Override all initializer methods of the superclass
Override the superclass’s primitive methods (described below)
This is my subclass (nothing fancy):
#interface MYNotification : NSNotification
#end
#implementation MYNotification
- (NSString *)name { return nil; }
- (id)object { return nil; }
- (NSDictionary *)userInfo { return nil; }
- (instancetype)initWithName:(NSString *)name object:(id)object userInfo:(NSDictionary *)userInfo
{
return self = [super initWithName:name object:object userInfo:userInfo];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
return self = [super initWithCoder:aDecoder];
}
#end
When I use it, I get an extraordinary:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** initialization method -initWithName:object:userInfo: cannot be sent to an abstract object of class MYNotification: Create a concrete instance!'
What else do I have to do in order to inherit from NSNotification?
The problem was that was trying to call the superclass initialiser. You can't do that because is an abstract class. So, in the initializer you just have to init your storage.
Because this is horrible, I end up creating a category for NSNotification instead. There I added three kind methods:
Static constructor for my custom notification: Here I configure userInfo to be used as the storage.
Method to add information to the storage: The notification observer will call this to update userInfo.
Method to process the information submitted by the observes: After the post method has finished, the notification has collected all the information needed. We just have to process it and return it. This is optional if you are not interested in collecting data.
In the end, the category it's just a helper to deal with userInfo.
Thanks #Paulw11 for your comment!

Can NSManagedObject conform to NSCoding

I need to transfer a single object across device. Right now I am converting my NSManagedObject to a dictionary , archiving it and sending as NSData. Upon receiving I am unarchiving it. But I would really like to transfer the NSManagedObject itself by archiving and unarchiving instead of creating an intermediate data object.
#interface Test : NSManagedObject<NSCoding>
#property (nonatomic, retain) NSString * title;
#end
#implementation Test
#dynamic title;
- (id)initWithCoder:(NSCoder *)coder {
self = [super init];
if (self) {
self.title = [coder decodeObjectForKey:#"title"]; //<CRASH
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:self.title forKey:#"title"];
}
#end
NSData *archivedObjects = [NSKeyedArchiver archivedDataWithRootObject:testObj];
NSData *objectsData = archivedObjects;
if ([objectsData length] > 0) {
NSArray *objects = [NSKeyedUnarchiver unarchiveObjectWithData:objectsData];
}
The problem with the above code is. It crashes at self.title in initWithCoder saying unrecognized selector sent to instance.
Why is title not being recognized as a selector.
Should unarchive use a nil managed object context somehow before creating the object in initWithCoder?
Do i need to override copyWithZone?
This snippet below should do the trick. The main difference is to call super initWithEntity:insertIntoManagedObjectContext:
- (id)initWithCoder:(NSCoder *)aDecoder {
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Test" inManagedObjectContext:<YourContext>];
self = [super initWithEntity:entity insertIntoManagedObjectContext:nil];
NSArray * attributeNameArray = [[NSArray alloc] initWithArray:self.entity.attributesByName.allKeys];
for (NSString * attributeName in attributeNameArray) {
[self setValue:[aDecoder decodeObjectForKey:attributeName] forKey:attributeName];
}
return self;
}
Above snippet will handle only the attributes, no relationships. Dealing with relationships as NSManagedObjectID using NSCoding is horrible. If you do need to bring relationships across consider introducing an extra attribute to match the two (or many) entities when decoding.
how to obtain <YourContext>
(based on a now unavailable post by Sam Soffes, code taken from https://gist.github.com/soffes/317794#file-ssmanagedobject-m)
+ (NSManagedObjectContext *)mainContext {
AppDelegate *appDelegate = [AppDelegate sharedAppDelegate];
return [appDelegate managedObjectContext];
}
Note: replace <YourContext> in the first snippet with mainContext
Obviously NSManagedObject does not conform to NSCoding. You could try to make a custom managed object subclass conform, but it would be a dicey proposition at best. An NSManagedObject must have a related NSManagedObjectID. And, you don't get to assign the object ID-- that happens automatically when the object is created. Even if you made your subclass conform to NSCoding, you'd have to find a way to unarchive the object while also allowing the local managed object context to assign an object ID.
And even that ignores the question of how you'd handle relationships on your managed objects.
Converting to/from an NSDictionary is really a much better approach. But you can't just unarchive the data and be finished. On the receiving end, you need to create a new managed object instance and set its attribute values from the dictionary. It might be possible to get your approach to work, but by the time you're done it will be more work and more code than if you just used an NSDictionary.
Seriously: NSCoding, initWithCoder:, copyWithZone:, etc, are a really bad idea for the problem you're trying to solve. NSCoding is nice for many situations but it's not appropriate here.
The problem is obviously the unarchiver. In the end there is no way to use both initWithEntity: and initWithCoder: in the same object. However, I suspect that with some trickery you may be able to make this work. For instance, implement initWithCoder: as you have done, and in that create another managed object with initWithEntity: (this means you will need unmanaged ivars that can hold such a reference. Implement forwardingTargetForSelector:, and if the object is the one being created using initWithCoder:, forward it to the shadow object you created with initWithEntity: (otherwise, forward that selector to super). When the object is decoded fully, then ask it for the real managed object, and you're done.
NOTE: I have not done this but have had great success with forwardingTargetForSelector:.

heightForRowAtIndexPath occasionally crashes when loading, NSCoding - iOS

Some information about the way I am saving my data: I have an array of View Controllers that are added and deleted by the user (this is basically a note taking app and the View Controllers are folders). The View Controllers have several dynamic properties that the app needs to save as well as the notes array within them, and then the Note objects themselves have a few properties that need to be saved. The View Controllers and the Notes both of course have the proper NSCoding stuff, this is the one on the View Controller for example:
- (void) encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:self.folderName forKey:#"lvcTitle"];
[encoder encodeObject:[NSNumber numberWithInt:self.myPosition] forKey:#"myPosition"];
[encoder encodeObject:self.notes forKey:#"notes"];
}
- (id)initWithCoder:(NSCoder *)decoder {
self.folderName = [decoder decodeObjectForKey:#"lvcTitle"];
NSNumber *gottenPosition = [decoder decodeObjectForKey:#"myPosition"];
int gottenPositionInt = [gottenPosition intValue];
self.myPosition = gottenPositionInt;
self.notes = [decoder decodeObjectForKey:#"notes"];
return self; }
The array of Controllers belongs to a Singleton class. NSCoding is pretty confusing to me even though it's considered to be simple stuff, but so far I've had success with only telling the Singleton to save the Controllers array - which then (successfully) saves all of the contained properties of the View Controllers, their properties and all of the Notes' properties as well. Here is the code in the Singleton:
- (void) saveDataToDisk:(id)object key:(NSString *)key {
NSString *path = [self pathForDataFile];
NSMutableDictionary *rootObject;
rootObject = [NSMutableDictionary dictionary];
[rootObject setValue:object forKey:key];
[NSKeyedArchiver archiveRootObject:rootObject toFile:path]; }
- (void) loadDataFromDisk {
NSString *path = [self pathForDataFile];
NSDictionary *rootObject;
rootObject = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
if ([rootObject valueForKey:#"controllers"] != nil) {
self.controllers = [NSMutableArray arrayWithArray:[rootObject valueForKey:#"controllers"]];
firstRun = false;
LabeledViewController *lastOneThere = [self.controllers objectAtIndex:self.controllers.count-1];
lastOneThere.isFolderAddView = TRUE;
}else{
firstRun = true;
}
}
I then call the save method several times in the Folder View Controllers:
[singleton saveDataToDisk];
And this will work well several times, until I randomly get a crash right when the app is loading up. The culprit is heightForRowAtIndexPath:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
Note *currentNote = [self.notes objectAtIndex:indexPath.row];
if (currentNote.associatedCellIsSelected) {
return currentNote.myHeight + NOTE_BUTTON_VIEW_HEIGHT;
}
return NORMAL_CELL_FINISHING_HEIGHT; }
I get the following error:
2012-06-07 08:28:33.694 ViewTry[1415:207] -[__NSCFString associatedCellIsSelected]: unrecognized selector sent to instance 0x8904710
2012-06-07 08:28:33.696 ViewTry[1415:207] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFString associatedCellIsSelected]: unrecognized selector sent to instance 0x8904710'
*** First throw call stack:
I understand that "__NSCFString" and "unrecognized selector sent to instance" means that there is a string somewhere there shouldn't be, as associatedCellIsSelected is a bool. However, if I only return "currentNote.myHeight" in heightForRow, I also get the same __NSCF error with myHeight, which is a float. If I take out heightForRow all together, everything works except for the appropriate height definitions.
BTW, the table view that heightForRowAtIndexPath is referencing is made in loadView AFTER the notes array is made and populated. I just don't understand why this error would only pop up every once in a while (like 5-10 opens, savings, closings and reopenings of app), seemingly random - I cannot find the pattern that causes this behavior. Any pointers?
Sorry for the mess, I'm new to iOS programming and I'm sure I'm doing a lot of things wrong here.
Edit - Also, once the app has crashed, it stays crashed every time I reopen it (unless I disable heightForRow) until I uninstall and reinstall it.
When you see an "unrecognized selector" error and the receiver type is not the kind of object that you coded (in this case __NSCFString instead of Note), the odds are that you have a problem where the object you intended to use has been prematurely released and its address space is being reused to allocate the new object.
The fix depends on tracking down where the extra release is happening (or retain is not happening). If you can show the #property declaration for notes it might shed more light on the situation.
One quick thing to do is choose Product->Analyze from the menu and fix anything it flags. It won't catch everything but it's a good sanity check to start.

NSDictionary: method only defined for abstract class. My app crashed

My app crashed after I called addImageToQueue. I added initWithObjects: forKeys: count: but it doesn't helped me.
Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '*** -[NSDictionary initWithObjects:forKeys:count:]:
method only defined for abstract class.
Define -[DictionaryWithTag initWithObjects:forKeys:count:]!'
my code
- (void)addImageToQueue:(NSDictionary *)dict
{
DictionaryWithTag *dictTag = [DictionaryWithTag dictionaryWithDictionary:dict];
}
#interface DictionaryWithTag : NSDictionary
#property (nonatomic, assign) int tag;
- (id)initWithObjects:(id *)objects forKeys:(id *)keys count:(NSUInteger)count;
#end
#implementation DictionaryWithTag
#synthesize tag;
- (id)initWithObjects:(id *)objects forKeys:(id *)keys count:(NSUInteger)count
{
return [super initWithObjects:objects forKeys:keys count:count];
}
#end
Are you subclassing NSDictionary? That's not a common thing to do in Cocoa-land, which might explain why you're not seeing the results you expect.
NSDictionary is a class cluster. That means that you never actually work with an instance of NSDictionary, but rather with one of its private subclasses. See Apple's description of a class cluster here. From that doc:
You create and interact with instances of the cluster just as you would any other class. Behind the scenes, though, when you create an instance of the public class, the class returns an object of the appropriate subclass based on the creation method that you invoke. (You don’t, and can’t, choose the actual class of the instance.)
What your error message is telling you is that if you want to subclass NSDictionary, you have to implement your own backend storage for it (for example by writing a hash table in C). It's not just asking you to declare that method, it's asking you to write it from scratch, handling the storage yourself. That's because subclassing a class cluster directly like that is the same as saying you want to provide a new implementation for how dictionaries work. As I'm sure you can tell, that's a significant task.
Assuming you definitely want to subclass NSDictionary, your best bet is to write your subclass to contain a normal NSMutableDictionary as a property, and use that to handle your storage. This tutorial shows you one way to do that. That's not actually that hard, you just need to pass the required methods through to your dictionary property.
You could also try using associative references, which "simulate the addition of object instance variables to an existing class". That way you could associate an NSNumber with your existing dictionary to represent the tag, and no subclassing is needed.
Of course, you could also just have tag as a key in the dictionary, and store the value inside it like any other dictionary key.
From https://stackoverflow.com/a/1191351/467588, this is what I did to make a subclass of NSDictionary works. I just declare an NSDictionary as an instance variable of my class and add some more required methods. It's called "Composite Object" - thanks #mahboudz.
#interface MyCustomNSDictionary : NSDictionary {
NSDictionary *_dict;
}
#end
#implementation MyCustomNSDictionary
- (id)initWithObjects:(const id [])objects forKeys:(const id [])keys count:(NSUInteger)cnt {
_dict = [NSDictionary dictionaryWithObjects:objects forKeys:keys count:cnt];
return self;
}
- (NSUInteger)count {
return [_dict count];
}
- (id)objectForKey:(id)aKey {
return [_dict objectForKey:aKey];
}
- (NSEnumerator *)keyEnumerator {
return [_dict keyEnumerator];
}
#end
I just did a little trick.
I'm not sure that its the best solution (or even it is good to do it).
#interface MyDictionary : NSDictionary
#end
#implementation MyDictionary
+ (id) allocMyDictionary
{
return [[self alloc] init];
}
- (id) init
{
self = (MyDictionary *)[[NSDictionary alloc] init];
return self;
}
#end
This worked fine for me.

Subclassed UILabel stored within serialized view not storing custom vars

So, I have a view that is being serialized and stored in a file. Within that view, is n subclassed UILabels, with the only difference being a live property. I have the initwithcoder and encodewithcoder within the UILabel subclass, but I am still unable to get the custom variable within the label. I have included my subclass' methods and the below. Any help is appreciated.
Custom UILabel:
- (id) initWithCoder:(NSCoder *)decoder
{
self = [super initWithCoder:decoder];
if (self != nil) {
self.live = [decoder decodeBoolForKey:#"live"];
}
return self;
}
-(void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeBool:self.live forKey:#"live"];
}
Because I am only unarchiving the view, which contains the labels within, I assume that ios does not unarchive the custom labels?
Thanks
You are missing a call to [super encodeWithCoder:aCoder]. This will result in an incorrectly serialised object, I'm not sure what the consequences are, but you seem to have found one of them!

Resources