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.
Related
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
I am trying to save a NSDictionary with array values to NSUserDefaults but am having some strange trouble.
My NSDictionary has NSStrings for keys and each value is a NSArray of NSNumbers. When I print the dictionary out, everything is fine. I write this dictionary to NSUserDefaults and if I read it back out right away, everything seams fine. Using this everything seams just fine:
[[NSUserDefaults standardUserDefaults] setObject:self.selectedOptionPositions
forKey:PREF_OPTIONS_KEY];
[[NSUserDefaults standardUserDefaults] synchronize];
//THIS PRINT EVERYTHING OUT EXACTLY AS IT SHOULD!
NSLog(#"read after write: %#", [[NSUserDefaults standardUserDefaults]
objectForKey:PREF_OPTIONS_KEY]);
The problem comes when I create a new instance of the class that handles this. When I make a new instance of the class and in the init method check the NSDictionary like so:
NSLog(#"read initial: %#", [[NSUserDefaults standardUserDefaults]
objectForKey:PREF_OPTIONS_KEY]);
When I print that logging, the NSDictionary contains all of the keys but all of the values are now empty! All newly added keys exist after recreating the class, but no values persist.
What could be wrong here? There are no warnings or errors in the console.
Try this:
You can use NSKeyedArchiver to write out your dictionary to an NSData, which you can store among the preferences.
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self.selectedOptionPositions];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:PREF_OPTIONS_KEY];
For retrieving data:
NSData *dictionaryData = [defaults objectForKey:PREF_OPTIONS_KEY];
NSDictionary *dictionary = [NSKeyedUnarchiver unarchiveObjectWithData:dictionaryData];
As in the iOS Developer Documentation for NSKeyedArchiver it says that:
NSKeyedArchiver, a concrete subclass of NSCoder, provides a way to
encode objects (and scalar values) into an architecture-independent
format that can be stored in a file. When you archive a set of
objects, the class information and instance variables for each object
are written to the archive. NSKeyedArchiver’s companion class,
NSKeyedUnarchiver, decodes the data in an archive and creates a set of
objects equivalent to the original set.
I'm attempting to build an app which will track a route, then store the route in parse.com so I can overlay the route taken by a user using MKpolyline.
I'm very new to Objective-c and IOS development, so please excuse my ignorance!!
I'm stuck when I try to save the route taken, then send/save the location array so I can rebuild the MKpolyline on the next view controller which is opened when the user completes the activity.
I'm not sure whether to save the location array to NSUserDefaults or save it to core data. At the moment I am converting the Array to an NSValue and the saving it to NSUserDefaults like so:
count = [self.locations count];
CLLocationCoordinate2D coordinates[count];
for (NSInteger i = 0; i < count; i++) {
coordinates[i] = [(CLLocation *)self.locations[i] coordinate];
NSValue *locationValue = [NSValue valueWithMKCoordinate:coordinates[i]];
[_locationsArray addObject:locationValue];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:totalDistance forKey:#"totalDistance"];
[defaults setObject:_locationsArray forKey:#"mapOverlay"];
// [defaults setDouble:_totalTime forKey:#"totalTime"];
[defaults setObject:avgSpeedToBeSaved forKey:#"averageSpeed"];
[defaults setObject:totalCalories forKey:#"totalCalories"];
[defaults synchronize];
Is this the right way to do this? And how do I rebuild the locations Array.
If anyone could point me in the right direction it would be greatly appreciated.
I've now changed my code to what was suggested by manecosta to rebuild the CLLocationCoordinates to create an MKPolyline, but my issue now is that the array is Null from where I start to convert into an NSValue. I am unable to figure out why this is, is there something wrong with the way I'm building LocationsArray in the first place?
Yes, I guess you're doing it right and to rebuild just do the opposite, which should be something like:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSArray *locationsArray = [defaults objectForKey:#"mapOverlay"];
CLLocationCoordinate2D coordinates[[locationsArray count]];
NSInteger i = 0;
for(NSValue *locationValue in locationsArray){
coordinates[i] = [locationValue MKCoordinateValue];
i++;
}
About the fact that you're using User Defaults to store tons of data. I don't really know what is correct, but I'll tell you that I've previously used it to store the cache of my app which were quite big arrays and dictionaries and it never failed me.
You can get object from NSUserDefaults by using objectForKey:
As you have [defaults setObject:totalDistance forKey:#"totalDistance"];.
Now if you want to get the totalDistance back from the NSUserDefaults` then you can use following code:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSArray *arrayLocation = [defaults objectForKey:#"totalDistance"];
But a piece of advise for you is not to use NSUserDefaults for your project. NSUserDefaults should only be used to store little amount of data (It's just a personal opinion.). Your locationArray is going to be quite large. Right?
A nice comparison between the two options NSUserDefaults & CoreData
What are the limitations of NSUserDefaults?
I'm posting answer here (from above link - in case the question gets unavailable in future time - who knows !!)
NSUserDefaults offers a trivial learning curve and thread safe
implementation.
Otherwise I've found Core Data superior in every way. Especially with
regards to configuring default values and migration routines.
EDIT: I actually haven't tried but it seems to be this way..
Tell me if it does not work.
To get back NSArray from NSValue
NSValue *value = [defaults objectForKey:#"mapOverlay"];
NSArray *arrayTemp;
[value getValue:&arrayTemp];
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
I try to save my object to NSUserDefaults. But when I call this method again it is not have any info about previous operation.
There is my method below:
- (void)addToCart {
if([[NSUserDefaults standardUserDefaults] objectForKey:kCart]) {
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
NSMutableArray *products = [[NSMutableArray alloc] initWithArray:[prefs objectForKey:kCart]];
[products addObject:self.product];
[prefs setObject:products forKey:kCart];
[prefs synchronize];
[products release];
}
else {
//Saving...
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
[prefs setObject:[NSArray arrayWithObjects:self.product, nil] forKey:kCart];
[prefs synchronize];
}
}
I need to save a collection with a products to NSUserDefault. I wrap my object to NSArray and save it but it doesn't work.
Everything put into NSUserDefaults must be a valid property list object (NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary). All collection elements must themselves also be property list objects.
In order to save non-PL objects into NSUserDefaults, you must first convert the object into a PL object. The most generic way to do this is by serializing it to NSData.
Serializing to NSData is handled with NSKeyedArchiver. See Storing NSColor in User Defaults for the canonical example of this. (That document is very old and still references NSArchiver which will work fine for this problem, but NSKeyedArchiver is now the preferred serializer.)
In order to archive using NSKeyedArchiver, your object must conform to NSCoding as noted by #harakiri.
You need to conform to the <NSCoding> protocol and implement -initWithCoder: and -encodeWithCoder: in your custom object.
See: https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Protocols/NSCoding_Protocol/Reference/Reference.html