NSKeyedArchiver Doesn't Seem Properly Save an MPMediaItemCollection Object - ios

I am trying to save a playlist of songs created with the media picker. I tried to use the suggestion provided in Persist a MPMediaItemCollection Object Using NSUserDefaults. This solution uses NSKeyedArchiver as follows:
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:mediaItemCollection];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:data forKey:#"someKey"];
[defaults synchronize];
Then when the MPMediaItemCollection needs to be retrieved, the following is performed:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *data = [defaults objectForKey:#"someKey"];
MPMediaItemCollection *mediaItemCollection = [NSKeyedUnarchiver unarchiveObjectWithData:data];
While this seemed to work at first, I found that it doesn't work consistently over time. It seems that after my app is loaded after a new software release, some of the media items in the playlist are corrupted. For example, when I try to load the song title from the retrieved MPMediaItemCollection for display in a tableView as follows:
MPMediaItem *anItem = (MPMediaItem *)[mediaItemCollection.items objectAtIndex: row];
if (anItem) {
cell.textLabel.text = [anItem valueForProperty:MPMediaItemPropertyTitle];
if (cell.textLabel.text == nil) {
NSString * persistentID = [anItem valueForProperty:MPMediaItemPropertyPersistentID];
...
}
the song title is nil for some of the entries. If I try to grab the persistentID for the song at this index in the queue, there is a persistentID, but it doesn't point to a valid song (so its probably just garbage).
SO THE QUESTION IS: Is it possible to save a MPMediaItemCollection that I can be sure is valid across all launches and software upgrades (both my upgrades and iOS upgrades). If so, how?

The answer is not this way! the MPMediaItemCollection object cannot be converted to either NSArray, NSDictionary or any other object that we can archive in NSUserDefaults.
My suggestion is to enumerate through the collection, save an array of [anItem valueForProperty:MPMediaItemPropertyPersistentID] s. save it in the NSUserDefaults
then on the next run of the application, you read and enumerate through the NSArray and create the MPMediaItemCollection back with items with persistentIDs.
I hope it helps

Related

IOS/Objective-C: Save Custom Array as NSUserDefault

I am trying to save an array of objects into an NSUserDefault without success. When I log out the array before the attempt it is full of object. However, when I try to log out the NSUserDefault it is NULL. Can anyone see what I might be doing wrong? Thanks for any suggestions:
Items *myItems = [mutableFetchedObjects mutableCopy];
NSLog(#"my Items%#",myItems);//LOGS OUT LONG LIST OF ITEMS
NSUserDefaults *currentDefaults = [NSUserDefaults standardUserDefaults];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:myItems];
[currentDefaults setObject:data forKey:#"myItems"];
[currentDefaults synchronize];
Items *myRetrievedItems = [[[NSUserDefaults standardUserDefaults] arrayForKey:#"myItems"] mutableCopy];
NSLog(#"my Retrieved Items%#",myRetrievedItems); //LOGS OUT AS NULL
As the other answers mentioned, it is because your array is not complying to the NSDictionary types (string, binary, bool, etc). Your members of array is of custom types therefore it cannot be saved. What you need to do is convert your array to binary first and then save it.
You have to unarchive your data first at the time of retrieving back. You are directly accessing the data. This won't work. You can do it the similar way you are archiving the data
NSData *dataObj = [currentDefaults objectForKey:#"myItems"];
Items *myRetrievedItems = [NSKeyedUnarchiver unarchiveObjectWithData:dataObj];
For more reference, you can consider this answer.
Hope this helps.
Thanks!
Your access value method is wrong.
You can get the array in following code:
Items *myRetrievedItems = [[[NSUserDefaults standardUserDefaults] objectForKey:#"myItems"] mutableCopy];

How can I allow users to duplicate arrays each with a different user selected name?

My app allows users to create and several several arrays, from the media picker, saving each with a different user selected name. How can I allow the user to make a duplicate of one of the arrays and save under a different file name?
example list shows user created arrays
array1
array2
array3
Now the user wants to create a new array just like array3 but they will delete a few items rather than create an entire new array.
So I want the user to be able to make a copy of array3 naming it array4 and then make a be able to a few changes to array4 and save time.
Hope that makes some sense.
After the media picker selection of songs, this is my save playlist method:
- (void)savePlaylist:(MPMediaItemCollection *) mediaItemCollection
{
NSArray* items = [mediaItemCollection items];
if (items == nil)
{
return;
}
NSMutableArray* listToSave = [[NSMutableArray alloc] initWithCapacity:0];
for (MPMediaItem *song in items)
{
NSNumber *persistentId = [song valueForProperty:MPMediaItemPropertyPersistentID];
[listToSave addObject:persistentId];
}
//read playlist title
NSUserDefaults *defaults;
defaults = [NSUserDefaults standardUserDefaults];
NSString *thissongsList;
thissongsList = [defaults objectForKey:#"savetextkey"];
//save playlist
defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject: thissongsList forKey:#"savetextkey"];
[defaults synchronize];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject: listToSave];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:_songsList];
[[NSUserDefaults standardUserDefaults] synchronize];
}
Asking to duplicate an array is fine, but we don't even know what language you're using. If you are using Objective-C, then all you have todo in order to copy an array is send the "copy" command on an array. Example below.
NSArray *myArray = [oldArray copy];
This is called a "shallow copy" which means it'll only copy the pointer references and not the actual objects. If you need to make brand new instances of the objects, then you'll need to adopt the NSCopying protocol in your objects, and override the copyWithZone method.
If you are using Swift then you'll basically do the same thing. To copy the array you'll use the code below.
var myArray = oldArray
Then use the same NSCopying protocol if you want to make a deep copy.
That's it, that is all you've got todo to copy an array.
Thanks

NSUserDefaults save two arrays leading to crash

Recently I was studying NSUserDefaults, then made a demo as follows:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSMutableArray *activity_array = [NSMutableArray array];
NSMutableArray *movie_array = [NSMutableArray array];
[defaults setObject:activity_array forKey:#"activity"];
[defaults setObject:movie_array forKey:#"movie"];
[defaults synchronize];
Then I tried writing the following which I will be calling "code2" for the duration of this post:
NSUserDefaults *userDefault = [NSUserDefaults standardUserDefaults];
NSMutableArray *array = [userDefault objectForKey:#"activity"];
[array addObject:#"123"];
the demo still works.
However the demo crashes when I replace "code2" with the following code:
NSUserDefaults *userDefault = [NSUserDefaults standardUserDefaults];
NSMutableArray *array = [userDefault objectForKey:#"movie"];
[array addObject:#"123"];
As you can see, the difference is the key.
Why does this crash?
NSUserDefaults can store NSMutableArrays, but will turn them into immutable arrays.
Same goes for NSDictionary and NSMutableDictionary.
That means if you want to add an object to an array that you just extracted from the NSUserDefaults, you will have to first make it mutable first, using -mutableCopy for example, or -initWithArray.
NSArray *array = [userDefault objectForKey:#"movie"];
//This works
NSMutableArray *arr = [NSMutableArray alloc]initWithArray:array];
//This works too and is more commonly used.
NSMutableArray *arr = [array mutableCopy];
You can now modify the array arr without any trouble, and you will be able to save it just like you did before. If you retrieve it afterwards, it will be the modified array. But be careful, arrays and dictionaries are always immutable when taken from NSUserDefaults. You will have to do that "trick" everytime you want to modify an array or dictionary from the NSUserDefaults.
EDIT : after testing your code, my only assumption is that your crash-free array is simply nil when you retrieve it. Debug with breakpoints to verify this but I'm close to 101% sure.
EDIT2 : trojanfoe got that faster than I did !
As others have pointed-out the arrays you get back from NSUserDefaults are immutable, so an exception will be thrown when calling addObject: on them, however that won't occur if the array is nil as calling methods (sending messages) to objects that are nil are silently ignored.
Therefore I believe your code2 works as the object #"activity" doesn't exist in the user defaults, while #"movie" does.
Arrays and dictionaries returned from NSUserDefaults are always immutable, even if the one you set was mutable. You'll have to call -mutableCopy.
Try this:
NSUserDefaults *userDefault = [NSUserDefaults standardUserDefaults];
NSMutableArray *array = [[userDefault objectForKey:#"movie"]mutableCopy];
The object from userDefault is not mutable.. Try this
NSMutableArray *arr = (NSMutableArray *)[userDefault objectForKey:#"activity"];
or if you like use id and check its class first to prevent crashing:
id variableName = [userDefault objectForKey:#"activity"];
if ([[variableName class] isEqual:[NSArray class]])
{
NSMutableArray *arr = [[NSMutableArray alloc] initWithArray:(NSArray *)variableName];
NSMutableArray *arr = [(NSArray *)variableName mutableCopy];
}
else if ([[variableName class] isEqual:[NSNull class]])
NSLog(#"no object with key:activity");
else
NSLog(#"not array");
//Happy coding.. :)

Saving a list of indexPaths

I normally save the selected indexPath of a UiTableView in NSUserDefaults, this is how I normally do it:
self.lastIndexPathUsed = indexPath.row;
NSUserDefaults *userdefaults = [NSUserDefaults standardUserDefaults];
[userdefaults setObject:[NSString stringWithFormat:#"%ld",(long)indexPath.row] forKey:#"lastIndexPathUsedForSellerType"];
[[NSUserDefaults standardUserDefaults] synchronize];
Then I restore it:
self.lastIndexPathUsed = [[[NSUserDefaults standardUserDefaults] objectForKey:#"lastIndexPathUsedForSellerType"] floatValue];
So self.lastIndexPathUsed is just a float value.
The above works fine on a single select table view. However on a multi-select tableview I can't see to get it working.
While the UITableView is on screen I save all the indexPaths into an array. However saving that array into NSUserDefaults causes a crash due to indexPath not being an object as such.
How to save an array how indexPaths into NSUserDefaults?
Try to archive your array to NSData before saving them to NSUserDefaults:
NSData *indexPathArrayData = [NSKeyedArchiver archivedDataWithRootObject:_indexPathArray];
and after getting the NSData from NSUserDefaults, you can unarchive them :
_indexPathArray = [NSKeyedUnarchiver unarchiveObjectWithData:indexPathArrayData];
You can try this :
[[NSUserDefaults standardUserDefaults] setObject:yourMutableArray forKey:#"Key"];
And then get it like this :
NSMutableArray *array = [NSMutableArray arrayWithArray:[[NSUserDefaults standardUserDefaults] objectForKey:#"Key"]];
Hope it helps.

iCloud: Is it possible to use KVS for custom objects?

I am trying to wrap my head around iCloud storage. I have read through the iCloud Design Guide and some questions here on SO. Here is my scenario:
I have a class that only has an NSMutableArray...that collection holds my custom objects which all adhere to NSCoding and saving locally works perfectly. That class is MasterList and has the property masterList, brilliant - I know :-p. When I looked on how to start implementing iCloud I thought KVS would be great, since my data footprint is extremely small. When I attempted this, I kept getting:
[NSUbiquitousKeyValueStore setObject:forKey:]: Attempt to insert non-property value <my custom objects>...
After seeing why that was, it seems you can only store scalar and P-List types with KVS. So I moved to using UIDocument and am struggling with it.
Bottom line -
For custom objects, do you have to use UIDocument, or is it possible to use KVS?
and
If I must use UIDocument, did any of you read a tutorial that was simple in nature (maybe stored a few props and loaded them back, maybe in a sample project?) that made it click for you?
Below is my code for using KVS if that helps at all. Not production or anything, just trying to get it to work:
//Load it
+(MasterList *)decodeMasterList:(MasterList *)objectToDecode
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSURL *dataFile = [self pathForDocumentsFile:kFilePathAllLists];
NSString *filePath = [dataFile path];
objectToDecode = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
//First time runnign the app :-)
if (!objectToDecode) objectToDecode = [[MasterList alloc] init];
if ([defaults boolForKey:kIsUsingiCloud])
{
objectToDecode.masterList = [[[NSUbiquitousKeyValueStore defaultStore] arrayForKey:kiCloudPath] mutableCopy];
//If we just started and nothing is there
if (!objectToDecode.masterList)
{
objectToDecode.masterList = [[NSMutableArray alloc] initWithObjects:[CustomList new], nil];
}
}
return objectToDecode;
}
//Save it
+(BOOL)encodeMasterList:(MasterList *)objectToEncode
{
#try
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([defaults boolForKey:kIsUsingiCloud])
{
[[NSUbiquitousKeyValueStore defaultStore] setArray:objectToEncode.masterList forKey:kiCloudPath];
[[NSUbiquitousKeyValueStore defaultStore] synchronize];
}
NSURL *dataFile = [FileSystemHelper pathForDocumentsFile:kFilePathAllLists];
NSString *filePath = [dataFile path];
[NSKeyedArchiver archiveRootObject:objectToEncode toFile:filePath];
}
#catch (NSException *exception)
{
return NO;
}
return YES;
}
It's true that the key-value store can only hold property list types. But you mentioned the objects in your array all conform to NSCoding, so that's not a major problem. You just need to apply NSCoding to convert your objects to/from NSData, and store that.
Encode using
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:myObject];
Decode using
NSData *data = // from key-value store
MyClass *myObject = [NSKeyedUnarchiver unarchiveObjectWithData:data];
However if you're using UIDocument, the key-value store is probably not the best way to go. You can also store and sync documents directly in iCloud via NSFileManager and NSMetadataQuery. Apple provides pretty good documentation on this, and it would sync the document in-place instead of requiring you to convert it back and forth. Plus of course, the key-value store has a very low total size limit while documents are only limited by the capacity of the user's iCloud account.

Resources