Help creating custom iPhone Classes - iphone

This should be a simple question, but I just can't seem to figure it out.
I'm trying to create my own class which will provide a simpler way of playing short sounds using the AudioToolbox framework as provided by Apple. When I import these files into my project and attempt to utilize them, they just don't seem to work. I was hoping someone would shed some light on what I may be doing wrong here.
simplesound.h
#import <Foundation/Foundation.h>
#interface simplesound : NSObject {
IBOutlet UILabel *statusLabel;
}
#property(nonatomic, retain) UILabel *statusLabel;
- (void)playSimple:(NSString *)url;
#end
simplesound.m
#import "simplesound.h"
#implementation simplesound
#synthesize statusLabel;
- (void)playSimple:(NSString *)url {
if (url = #"vibrate") {
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
statusLabel.text = #"VIBRATED!";
} else {
NSString *paths = [[NSBundle mainBundle] resourcePath];
NSString *audioF1ile = [paths stringByAppendingPathComponent:url];
NSURL *audioURL = [NSURL fileURLWithPath:audioFile isDirectory:NO];
SystemSoundID mySSID;
OSStatus error = AudioServicesCreateSystemSoundID ((CFURLRef)audioURL,&mySSID);
AudioServicesAddSystemSoundCompletion(mySSID,NULL,NULL,simpleSoundDone,NULL);
if (error) {
statusLabel.text = [NSString stringWithFormat:#"Error: %d",error];
} else {
AudioServicesPlaySystemSound(mySSID);
}
}
static void simpleSoundDone (SystemSoundID mySSID, void *args) {
AudioServicesDisposeSystemSoundID (mySSID);
}
}
- (void)dealloc {
[url release];
}
#end
Does anyone see what I'm trying to accomplish here? Does anyone know how to remedy this code that is supposedly wrong?

In C based languages, = is an assignment operator, and == is an equality operator.
So when you write this:
if (url = #"vibrate") {
That will always return true, since in C (and hence Obj-C), if statements are 'true' if what's in the brackets is not 0, and an = operation returns the assigned value, which in this case is a pointer to the NSString #"vibrate" (which is definitely not zero).
I don't know exactly why you're trying to compare a URL string to #"vibrate", but the correct way to compare NSString objects is to do something like:
if ([url isEqualToString:#"vibrate"])

if (url = #"vibrate") {
See also Tell The Program What To Do When No Save Data Is Found NSUserDefaults, iPhone.

Related

Access NSDictionary via dot notation?

Is there a way via dot notation to access the values of keys in an NSDictionary like this?
NSDictionary *returnVal = [NSDictionary dictionaryWithObjectsAndKeys:#"Saturn", #"name", #"Gas Giant", #"type", nil];
NSLog(#"VALUE: %#", [returnVal valueForKey:#"name"]); // This is how I am doing it now.
There is no dot syntax for NSDictionary, but should consider using objectForKey: instead of valueForKey:
Difference between objectForKey and valueForKey?
Not really, no.
The dot notation is a shorthand way of calling a method with that selector name. In other words, this...
NSLog(#"Hello, %#", foo.bar.name);
...is the same as this...
NSLog(#"Hello, %#", [[foo bar] name]);
When I say "same", I mean they are compiled down to the same code. It's just syntactic sugar.
A plain NSDictionary won't act that way. You could sort of fake it with Key Value Coding, which lets you call valueForKeyPath to get properties like this:
NSLog(#"Hello, %#", [foo valueForKeyPath:#"bar.name"]);
If you really wanted to be able to write foo.bar.name in your code, however, you'd have to make a custom class that overrides forwardInvocation:; this lets you catch an unknown message to an object and do something else with it besides throw an error. In this case, you could change the unknown selector to a lookup on an NSDictionary instance it contains.
But even if you did that, the compiler would probably still generate warnings unless you made header files that declared those property names to exist.
I agree with most of the answers that NSDictionary should be accessed with objectForKey: or similar methods. However it is possible to allow for dot notation access to a NSDictionary, and for learning purposes this might be interesting for someone. Also when for example your are retrieving large JSON dictionaries via AFNetworking, this method can ease the access and readability of your code.
This is my solution:
DictionaryProperties.h: (class wrapping the NSDictionary for property access)
#interface DictionaryProperties : NSObject{
NSMutableDictionary* _backingDict;
}
#property (nonatomic, strong) NSMutableDictionary* backingDict;
+ (DictionaryProperties*) allocWithDictionary:(NSDictionary*)dict;
#end
DictionaryProperties.m:
#import "DictionaryProperties.h"
#implementation DictionaryProperties
#synthesize backingDict = _backingDict;
- (id) initWithDictionary:(NSDictionary*)dict {
if (self) {
if ([dict isKindOfClass:[NSMutableDictionary class]]) {
self.backingDict = (id)dict;
} else {
self.backingDict = [[NSMutableDictionary alloc] initWithDictionary:dict];
}
}
return self;
}
+ (DictionaryProperties*) allocWithDictionary:(NSDictionary*)dict {
return [[DictionaryProperties alloc] initWithDictionary:dict];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
NSString* key = NSStringFromSelector(invocation.selector);
invocation.selector = #selector(objectForKey:);
[invocation setArgument:&key atIndex:2];
if ([self.backingDict objectForKey:key]) {
[invocation invokeWithTarget:self.backingDict];
} else {
[self doesNotRecognizeSelector:invocation.selector];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
return [self.backingDict methodSignatureForSelector:#selector(objectForKey:)];
}
#end
ExampleDictContent.h: (class declaring what is inside the dictionary)
#import "DictionaryProperties.h"
#interface ExampleDictContent : DictionaryProperties
#property (strong, nonatomic) NSString* someData;
#property (strong, nonatomic) NSString* someOtherData;
#end
#implementation ExampleDictContent
#end
Usage: (simple declaration of a dictionary, allocation of wrapper and property access)
#import "ExampleDictContent.h"
NSDictionary* d = [NSDictionary dictionaryWithObjects:NSArray arrayWithObjects:#"someData content", #"someOtherData content", nil
forKeys:NSArray arrayWithObjects:#"someData", #"someOtherData", nil];
ExampleDictContent* dictWProps = [ExampleDictContent allocWithDictionary:d];
NSLog(dictWProps.someData);
NSLog(dictWProps.someData);
This will print:
someData content
someOtherData content
So basically DictionaryProperties works as a facade for accessing the NSDictionary. It uses forwardInvocation to convert a get-property method call into a getObjectForKey: call on the dictionary. What I like about it, is that it allows for autocompletion on the dictionary, and also allows me to explicitly declare what keys I want to access (in the ExampleDictContent.h file). Note that this solution does not allow for write access to the properties, but that can be added as shown in the link below.
This solution has partly been inspired by karstenlitsche's solution. The main difference is that this solution is based on sub-classing instead of categories.
No, I don't think so.
From the reference manual.
Accessing Keys and Values
– allKeys
– allKeysForObject:
– allValues
– getObjects:andKeys:
– objectForKey:
– objectsForKeys:notFoundMarker:
– valueForKey:
That's listed as the only way to access the keys and the values. So you are doing it alright.
You would be able to access it if the keys were a public property and it was readable.
The way that you have mentioned for accessing element of dictionary is ideal way(using keys).
If you want to do something else, might be you can use-
NSArray *allValues = [returnVal allValues];
Now using this array as well you can perform tasks.
And if you want something specific then mention that, might be for that there can be some other way.
Also as NSDictionary class won't have any property defined, so dot notation is directly not possible.
No, you are doing it the correct way. In the iOS world, often the correct way is the only way. :)
If you really want dot notation (and other nice things you get with typed objects), you're going to have to stuff the dictionary representation into an object. Most commonly my interface will look like:
#interface FooBar : NSObject {
NSString *someData;
int someNumber;
}
#property (nonatomic, copy) NSString *someData;
#property (nonatomic, assign) int someNumber;
+ (FooBar *)FooBarFromDictionary:(NSDictionary *)dataDict;
#end
The implementation should be clear. Then you can
FooBar *fb = [FooBar FooBarFromDictionary:data];
NSLog(#"fb.someData = %#", fb.someData);
Technically, you can do something like this:
typedef id (^valueBlock)(id);
#interface NSDictionary(dotNotationAddons)
#property(nonatomic, readonly) valueBlock value;
#end
#implementation NSDictionary(dotNotationAddons)
-(valueBlock) value
{
return [[^(id key) {
return [self objectForKey:key];
} copy] autorelease];
}
#end
int main (int argc, const char * argv[])
{
#autoreleasepool {
NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:#"1", #"One", #"2", #"Two", #"3", #"Three", #"4", #"Four", nil];
id value = dictionary.value(#"One");
NSLog(#"%#", value);
}
return 0;
}
I don't know if that is what you were looking for, but I hope it helps!
The answer's still no, but you can use the shorthand
myDictionary[#"key"]
instead of
[myDictionary objectForKey:#"key"]
In Swift, there is a solution that may not seem very elegant but does the trick.
It will require a typeAlias for each specific type of Dictionary and also an extension with variables (with getter/setter) for each of the expected keys in your dictionary. Not a good practice at all
It may be easier wrap your dict object in an object (class/struct) with the same treatment.
typealias MyDict = [String:AnyObject]
extension MyDict {
var key: AnyObject? {
get { return self["key"] }
set { self["key"] = newValue }
}
}
// Usage
var myDict = MyDict()
// Get the value
myDict["key"] = "value1" as AnyObject
if let str = myDict.key {
print(str) // prints "value1"
}
// Set the value
myDict.key = "value2" as AnyObject
if let str = myDict["key"] {
print(str) // prints "value2"
}

read from plist and load into image name/label text

I have no idea where this code is wrong. Please help, it is supposed to read a value from a dictionary and I use the value to call an image. I've tried to read the value as label.text but I got no result.
The only one I can call is from the nslog.
for (id key1 in dictionary)
{
NSMutableString *textnamed = [dictionary objectForKey:key1];
NSMutableString *imageDisplay =[NSMutableString stringWithFormat:#"%#.png",[dictionary objectForKey:key1]];
eyeImageSaved.image = [UIImage imageNamed:imageDisplay];
labelSaved.text = textnamed;
NSLog(#"%#",textnamed);
}
There are a few issues with your code. First of all, you should consider using NSString instead of NSMutableString because it should be faster. Second, why are you putting the dictionary object in a string then calling the dictionary object again in the next line? That first line is unnecessary. It's entirely possible that the dictionary entry is not a string, and that is why you are having issues. You should write it like this NSString *textnamed = (NSString *)[dictionary objectForKey:key1]. Also, imageNamed can only be used for files in the file bundle. Are you sure those pictures are stored there? There are a few other issues you could be having. What exactly is going wrong here?
I solved my problem. There is nothing wrong with my code above, the mistake I made is that the object that is supposed to hold my variable and store in plist as the name of the image became null. So I added the object to appdelegate.h as seen below:
AppDelegate
#interface AppDelegate : UIResponder <UIApplicationDelegate>
{
NSString *imageNameHolder;
}
#property (retain, nonatomic) NSString *StorageDecider;
View Controller
- (IBAction)lips2:(id)sender {
imageNameHolder = #"lips_blue";
}
-(void)writeNewPlist
{
AppDelegate* ref = (AppDelegate*) [[UIApplication sharedApplication] delegate];
[dictionary setObject:imageNameHolder forKey:#"image1"];
[dictionary writeToFile:finalPath atomically:YES];
}
nameDataFormArray_ & imageDataFormArray_ are NSMutableArray. Both have string and find image as per string (name of image).
- (void)readInfoFromThePlist
{
NSString* PListPath=[[NSBundle mainBundle] pathForResource:#"dataList" ofType:#"plist"];
NSMutableArray* tempDataList=[[NSMutableArray alloc ] initWithContentsOfFile:PListPath];
for(NSDictionary *dataDict in tempDataList)
{
[nameDataFormArray_ addObject:[dataDict objectForKey:#"name"]];
[imageDataFormArray_ addObject:[dataDict objectForKey:#"imagepath"]];
}
[tempDataList release];
}

Loading different media files for each view subclass using NSString *const

I'm making a subclass of UIView completely in code (no IB), let's call it ContentView. In this view I've already set up several players for sounds and video, as well as several imageViews (nothing special).
Next, I was planning to subclass ContentView several times in order to load different media for each view. All of these views would have the same view controller since the interface would be the same for all of them, only the content (sounds, video and images) would change.
So my approach to this problem was to declare several NSString *const in ContentView.h and specify their keys/values in the implementation file of each subclass view of ContentView, in the form of static NSString *const, since I would be reusing them to load different media for each view and did not want them in the global name space.
Here is some mockup code that illustrates what I'm talking about:
In ContentView.h
#interface ContentView : UIView {
NSString *const kSoundFile1;
NSString *const kSoundFile2;
NSString *const kSoundFileType;
NSString *const kMovieFile1;
NSString *const kMovieFile2;
NSString *const kMovieFileType;
NSString *const kImage1;
NSString *const kImage2;
and in ContentView.m, something of the sort,
#implementation ContentView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
NSString *filePath1 = [[NSBundle mainBundle] pathForResource: kSoundFile1
ofType: kSoundFileType;
NSURL *url1 = [[NSURL alloc] initFileURLWithPath:filePath1];
AVAudioPlayer *audioPlayer1 = [AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
[audioPlayer1 prepareToPlay];
[url1 release];
... and so on, for the rest of the sound files and movies (except for the images, for which i'm using imageNamed:).
Then on the implementation file of each subclass of ContentView I have just this:
#implementation ContentViewSubclass
static NSString *const kSoundFile1 = #"sound1";
static NSString *const kSoundFile2 = #"sound2";
static NSString *const kSoundFileType = #"wav";
static NSString *const kMovieFile1 = #"movie1";
static NSString *const kMovieFile2 = #"movie2";
static NSString *const kMovieFileType = #"mov";
static NSString *const kImage1 = #"image1.png";
static NSString *const kImage2 = #"image2.png";
#end
I can't make this work. There are no compiler errors or warnings, simply nothing plays or shows. Am I doing something wrong, or is this just not the right approach to the problem?
I would really appreciate some insights. Thanks in advance.
Although the answer below from #deanWombourne makes perfect sense, I had an issue with his solution. But I have found out what was wrong with it (at least this is my take on it and it's working now).
A UIView subclass already has its own designated initializer, which is -(id)initWithFrame, so calling -(id)init on any subsequent subclass of ContentView is not going to update any instance variables since [super init] directs to nowhere (or better, first the superclass runs initWithFrame and only then it runs init, which is the same as having done nothing).
So my solution is the following:
(after changing the ivars on ContentView.h to the form of NSString *pointer, for example, NSString *kSoundFile1),
#implementation ContentViewSubclass
- (id)initWithFrame:(CGRect)frame {
kSoundFile1 = #"sound1";
kSoundFile2 = #"sound2";
kSoundFileType = #"wav";
kMovieFile1 = #"movie1";
kMovieFile2 = #"movie2";
kMovieFileType = #"mov";
kImage1 = #"image1.png";
kImage2 = #"image2.png";
self = [super initWithFrame:frame];
if (self) {
}
return self;
}
#end
First update the strings on the designated initializer -(id)initWithFrame, and only then call super.
It works just fine now.
My sincere thanks to #deanWombourne for the help given in solving this problem.
I think I might have worked it out, ignore my other answer :)
You've set the strings up as members of ContentView (in ContentView.h). They start out initialised to nil.
Then, in each subclass you create new strings with the same name but only in that .m file (the static keyword does that!). Just beacuse they have the same name in the C code doesn't mean they're pointing to the same object :)
Your ContentView.m file can't see the new strings, it just uses the ones defined in your .h file, which have never been set to anything so I bet they're still nil!
You need to define the strings in your subclasses like this :
#implementation ContentViewSubclass
- (id)init {
self = [super init];
if (self) {
kSoundFile1 = #"sound1";
kSoundFile2 = #"sound2";
kSoundFileType = #"wav";
kMovieFile1 = #"movie1";
kMovieFile2 = #"movie2";
kMovieFileType = #"mov";
kImage1 = #"image1.png";
kImage2 = #"image2.png";
}
return self;
}
#end
PS The k in front of these names doesn't make much sense anymore - I'd get rid of it)
EDIT: This answer is completely barking up the wrong tree. Try this one instead :)
I instantly look for this line :
AVAudioPlayer *audioPlayer1 = [AVAudioPlayer alloc] initWithContentsOfURL:url error:nil];
The SDK wants to tell you the error but you ask it not to. Then you complain that there is no error!
Try this:
NSError *error = nil;
AVAudioPlayer *audioPlayer1 = [AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
if (error) {
NSLog(#"error : %#", error);
}
and let us know what you see.

How to know if user has updated app or installed a fresh copy?

I will be sending out an update to my app with a new data structure, therefore if a user is updating my app I need to update their current data. So I was wondering how can I programatically tell if the user updated my app or installed a new copy (if a new copy is installed I don't need to update anything) ?
Checking the data structure is a solid solution. I began to worry in my own apps about folks who don't upgrade for several versions. I felt this would lead to a myriad of structure checks. The code I show below determines and stores the version and previous version in the NSUserDefaults. You could code for those varying version difference scenarios if needed.
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
BOOL versionUpgraded;
NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:#"CFBundleVersion"];
NSString *preVersion = [prefs stringForKey:#"appVersion"];
if ([prefs stringForKey:#"appVersion"] != nil) {
//see if version is the same as prior
//if not it is an Upgraded
versionUpgraded = !([preVersion isEqualToString: version]);
} else {
//nil means new install
//This needs to be YES for the case that
//"appVersion" is not set anywhere else.
versionUpgraded = YES;
}
if (versionUpgraded) {
[prefs setObject:version forKey:#"appVersion"];
[prefs setObject:preVersion forKey:#"prevAppVersion"];
[prefs synchronize];
}
That depends on the kind of data structure you're using.
In general, I would advise you against relying on checking your application version: a user using 2.0 might have just upgraded or it might be a new user.
I'd rather check if there's a data structure already, and act accordingly. Assuming that you're using a Sqlite-backed Core Data storage, you can either check whether the .sqlite file exists, or check if there are objects in your storage.
Just save the bundle version somewhere and check if it differs from
[[NSBundle mainBundle] objectForInfoDictionaryKey:#"CFBundleVersion"]]
on each app startup.
I have created a category for this. Just implement the two new delegate calls found in the header. It relies quite heavily on the obj-c runtime libraries, so make sure you are confident with them before using this.
.h
#import <UIKit/UIKit.h>
#protocol UIApplicationDelegate <UIApplicationDelegate>
#optional
- (void) application:(UIApplication *)application willUpdateToVersion: (NSString*) newVersion fromVersion: (NSString*) previousVersion;
- (void) application:(UIApplication *)application didUpdateToVersion: (NSString*) newVersion fromVersion: (NSString*) previousVersion;
#end
#interface UIApplication (Versioning)
#end
.m
#import "UIApplication+Versioning.h"
#import <objc/message.h>
#import <objc/runtime.h>
static NSString* UIApplicationVersionFileName = #"app.ver";
#implementation UIApplication (Versioning)
+ (void) load {
Method original, swizzled;
original = class_getInstanceMethod(self, #selector(setDelegate:));
swizzled = class_getInstanceMethod(self, #selector(swizzled_setDelegate:));
method_exchangeImplementations(original, swizzled);
}
- (void) swizzled_setDelegate: (id<UIApplicationDelegate>) delegate {
IMP implementation = class_getMethodImplementation([self class], #selector(swizzled_application:didFinishLaunchingWithOptions:));
class_addMethod([delegate class], #selector(swizzled_application:didFinishLaunchingWithOptions:), implementation, "B#:##");
Method original, swizzled;
original = class_getInstanceMethod([delegate class], #selector(application:didFinishLaunchingWithOptions:));
swizzled = class_getInstanceMethod([delegate class], #selector(swizzled_application:didFinishLaunchingWithOptions:));
method_exchangeImplementations(original, swizzled);
[self swizzled_setDelegate: delegate];
}
- (BOOL)swizzled_application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//Check for a version change
NSError* error;
NSArray* directories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString* versionFilePath = [[directories objectAtIndex: 0] stringByAppendingPathComponent: UIApplicationVersionFileName];
NSString* oldVersion = [NSString stringWithContentsOfFile: versionFilePath
encoding: NSUTF8StringEncoding
error: &error];
NSString* currentVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey: #"CFBundleVersion"];
switch (error.code) {
case NSFileReadNoSuchFileError:
{
//Delegate methods will not be called first time
oldVersion = [currentVersion copy];
[currentVersion writeToFile: versionFilePath
atomically: YES
encoding: NSUTF8StringEncoding
error: &error];
break;
}
default:
{
NSLog(#"Warning: An error occured will loading the application version file -> Recreating file");
[[NSFileManager defaultManager] removeItemAtPath: versionFilePath
error: nil];
oldVersion = [currentVersion copy];
[currentVersion writeToFile: versionFilePath
atomically: YES
encoding: NSUTF8StringEncoding
error: &error];
break;
}
}
if( ![oldVersion isEqualToString: currentVersion] ) {
if ([[application delegate] respondsToSelector: #selector(application:willUpdateToVersion:fromVersion:)]) {
objc_msgSend([application delegate], #selector(application:willUpdateToVersion:fromVersion:), currentVersion, oldVersion);
}
[currentVersion writeToFile: versionFilePath
atomically: YES
encoding: NSUTF8StringEncoding
error: &error];
if ([[application delegate] respondsToSelector: #selector(application:didUpdateToVersion:fromVersion:)]) {
objc_msgSend([application delegate], #selector(application:willUpdateToVersion:fromVersion:), currentVersion, oldVersion);
}
}
SEL realSelector = #selector(swizzled_application:didFinishLaunchingWithOptions:);
return (BOOL) objc_msgSend([application delegate], realSelector, application, launchOptions);
}
#end

How to save a NSMutableArray (containing other arrays) to file

This has been asked before and people have given very good instructions on how to do this, e.g. here.
However, I was wondering if I really need to work with NSCoder if I simply wanted to save one NSMutableArray (containing various instances of another NSMutableArray) to a file? I tried this but only got an error message:
-(void)saveLibraryDat {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0]; // Get documents directory
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:#"myLibrary.dat"];
NSError *error;
[myLibrary writeToFile:filePath atomically:YES];
if (error) {
NSLog(#"There was an error saving myLibrary.dat: %#", error);
}
}
My error message:
2011-05-13 22:00:47.840 MoleNotes[15437:207] There was an error saving myLibrary.dat: (
1,
2
)
So I guess I have to work with NSCoder, right? If so, I was wondering how to go about this. People have explained how to do this with a class, but in my case, I have a NSMutableArray (myLibrary) which contains various instances of a class. Will I have to implement the NSCoder in this class and the NSMutableArray?
I alloc my library like this:
myLibrary = [[NSMutableArray alloc] init];
And then add instances of a class called NoteBook.m like this:
NoteBook *newNoteBook = [[NoteBook alloc] init];
newNoteBook.titleName = #"Notes"; // etc.
[myLibrary addObject:newNoteBook];
So where exactly do I put the NSCoder commands? Only into my NoteBook.m class? Will this automatically take care of myLibrary?
Thanks for any suggestions.
EDIT:
So I've updated my code, but I guess the big problem is that my NSMutableArray myLibrary contains several instances of a custom class I've set up (called notebook). I have set up NSCoding for this class (and all its variables) so that I can save it and load it.
Now my app works totally fine if I create the NSMutableArray in the app (i.e. when the app is started for the very first time, no file exists), instead of loading it from disk:
-(void) setupLibrary {
myLibrary = [[NSMutableArray alloc] init];
NoteBook *newNoteBook = [[NoteBook alloc] init];
newNoteBook.titleName = #"Notes";
/...
If I load it from disk, it works fine as well:
-(void)loadLibraryDat {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0]; // Get documents directory
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:#"myLibrary.dat"];
myLibrary = [[NSMutableArray alloc] init];
myLibrary = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
if (!myLibrary) {
// if it couldn't be loaded from disk create a new one
NSLog(#"myLibrary.dat empty... set up new one");
[self setupLibrary];
} else { NSLog(#"Loading myLibrary.dat successful."); }
}
If I log everything which is contained in my library after loading it, everything is still fine. E.g. the following works totally fine:
NSLog(#"%#", [[self.myLibrary objectAtIndex:0] titleName]);
The big problem is, however, if any other method tries to access myLibrary. For instance, if I call the very same log command from another method, the app will crash and I get this error message:
[NSCFString objectAtIndex:]: unrecognized selector sent to instance 0x4b38510
2011-05-14 14:09:10.490 Notes[17091:207] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSCFString objectAtIndex:]: unrecognized selector sent to instance 0x4b38510'
This sounds to me as if myLibrary has become deallocated somehow, but I can't see why. How could this have happened? I have the feeling that I did something wrong in my NSCoding set up... because if I simply create myLibrary in code, everything works like wonderfully. It's only if I load it from the disk, that the app will crash.
Here is the class setup:
#import <Foundation/Foundation.h>
#interface NoteBook : NSObject <NSCoding> {
NSString *titleName;
NSString *fileName;
NSMutableArray *tabTitles;
NSMutableArray *tabColours;
NSMutableArray *tabReference;
}
#property (nonatomic, retain) NSString *titleName;
#property (nonatomic, retain) NSString *fileName;
#property (nonatomic, retain) NSMutableArray *tabTitles;
#property (nonatomic, retain) NSMutableArray *tabColours;
#property (nonatomic, retain) NSMutableArray *tabReference;
-(id)initWithCoder:(NSCoder *)aDecoder;
-(void)encodeWithCoder:(NSCoder *)aCoder;
#end
//
// NoteBook.m
#import "NoteBook.h"
#implementation NoteBook
#synthesize titleName, fileName, tabTitles, tabColours, tabReference;
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
self.titleName = [aDecoder decodeObjectForKey:#"titleName"];
self.fileName = [aDecoder decodeObjectForKey:#"fileName"];
self.tabTitles = [aDecoder decodeObjectForKey:#"tabTitles"];
self.tabColours = [aDecoder decodeObjectForKey:#"tabColours"];
self.tabReference = [aDecoder decodeObjectForKey:#"tabReference"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:titleName forKey:#"titleName"];
[aCoder encodeObject:fileName forKey:#"fileName"];
[aCoder encodeObject:tabTitles forKey:#"tabTitles"];
[aCoder encodeObject:tabColours forKey:#"tabColours"];
[aCoder encodeObject:tabReference forKey:#"tabReference"];
}
#end
EDIT:
I think I've solved it... I forgot a little 'self'... which messed it all up and deallocated myLibrary:
self.myLibrary = [NSKeyedUnarchiver
unarchiveObjectWithFile:filePath];
if (self.myLibrary == nil) {
NSLog(#"myLibrary.dat empty... set up new one");
[self setupLibrary];
} else { NSLog(#"Loading myLibrary.dat successful."); }
Your code is busted. The "error" variable is uninitialized and never set, so when you check it, you're just seeing random garbage data. If you want to know whether the write was successful, check the return value of writeToFile:atomically:. It will be YES if the write succeeded and NO if it didn't.
However, NSArray's writeTo… methods are for creating plists. If non-property-list objects are in your array, that method isn't appropriate, and an archiver is what you want. Just do something like [[NSKeyedArchiver archivedDataWithRootObject:myLibrary] writeToFile:writeToFile:filePath atomically:YES].
To make your objects conform to NSCoding correctly, just have them implement initWithCoder: and encodeWithCoder:, and in those methods, use NSCoder's storage methods to store the object's instance variables (and the retrieval methods to get them back out).
NSCoder is a protocol that your class must conform to in order to be archived to data/file. Works something like Serealizabe in Java.
Add conformance to the class header like this:
#interface NoteBook : NSObject <NSCoder> { // …
And then you must implement two methods:
-(id)initWithCoder:(NSCoder)decoder;
{
self = [super initWithCoder:decoder];
if (self) {
_someIvar = [decoder decodeObjectForKey:#"someKey"];
// And more init as needed…
}
return self;
}
-(void)encodeWithCoder:(NSCoder)coder;
{
[super encodeWithCoder:coder];
[coder encodeObject:_someIvar forKey#"someKey"];
/// Etc…
}
I would also advice against using -[NSArray writeToFile:atomically:] since in work with property list compliant objects only, not coding compliant classes. The property list object are NSString, NSData, NSArray, or NSDictionary, NSDate, and NSNumber. The list can not be extended.
Instead use NSKeyedArchiver/NSKeyedUnarchiver. Almost as simple to use:
if (![NSKeyedArchive archiveRootObject:yourArrat toFile:path]) {
// It failed.
}