I've been through a ton of SO posts, and this USED to work, but it stopped working. I'm not sure what happened. I developed this iPhone+WatchKit app with watchOS 1.0 and everything worked fine.
I've upgraded my app, project, and Apple Watch to watchOS 2.0, and now I can't get any data via NSUserDefaults using my App Group.
App Groups is enabled in Xcode on the host App, and WatchKit Extension. I even tried turning it on for the WatchKit App as well.
My group name is called "group.com.mycompany.myapp" (with my real company name and app name) and it's selected on all of the targets.
I've confirmed the Build settings for my host app and WatchKit extension reference the entitlements files and I've checked that those entitlements files contain the app groups security option for my chosen app group.
I've made sure the bundle identifiers are different for the host app, watchkit extension, and watchkit app. I use "com.mycompany.myapp", "com.mycompany.myapp.watchkitextension", and "com.mycompany.myapp.watchkitapp", respectively.
Here are some screenshots of my Xcode config for the host app, watchkit extension, and watchkit app, just in case:
Here is the code in my host app that is able to read the data properly:
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:kAppGroupName];
NSLog(#"app defaults: %#", [defaults dictionaryRepresentation]);
That produces this in my console, with kKeyEmployeeId and kKeyStoreNumber being the data that I'm trying to share between the host and watch.
2015-12-13 13:51:35.618 MyApp[27516:16126253] app defaults: {
AppleLanguages = (
"en-US",
en
);
INNextHearbeatDate = "472211882.83309";
NSInterfaceStyle = macintosh;
NSLanguages = (
"en-US",
en
);
"com.apple.content-rating.AppRating" = 1000;
"com.apple.content-rating.ExplicitBooksAllowed" = 1;
"com.apple.content-rating.ExplicitMusicPodcastsAllowed" = 1;
"com.apple.content-rating.MovieRating" = 1000;
"com.apple.content-rating.TVShowRating" = 1000;
kKeyEmployeeId = Kenny;
kKeyStoreNumber = 001;
}
In my WatchKit Extension, I have the same code, but it doesn't provide me the two key:value pairs that I need:
- (void)awakeWithContext:(id)context {
[super awakeWithContext:context];
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:kAppGroupName];
NSLog(#"defaults: %#", [defaults dictionaryRepresentation]);
}
It produces this, which is very similar to the one above, but without the two keys I really need:
2015-12-13 13:28:29.840 MyApp WatchKit Extension[720:322804] defaults: {
AppleLanguages = (
en
);
INNextHearbeatDate = "472434306.599217";
NSInterfaceStyle = macintosh;
NSLanguages = (
en
);
"com.apple.content-rating.AppRating" = 1000;
"com.apple.content-rating.ExplicitBooksAllowed" = 1;
"com.apple.content-rating.ExplicitMusicPodcastsAllowed" = 1;
"com.apple.content-rating.MovieRating" = 1000;
"com.apple.content-rating.TVShowRating" = 1000;
}
The kAppGroupName and other constants are defined like so:
NSString * const kAppGroupName = #"group.com.inadaydevelopment.MyApp";
NSString * const kKeyStoreNumber = #"kKeyStoreNumber";
NSString * const kKeyEmployeeId = #"kKeyEmployeeId";
In Watch OS1 this worked, but in Watch OS2 there has been some changes. You need to use something called WatchConnectivity to send the data you want to save to the watch. Then when the watch receives the data you sent to it, save it to the Apple watch's default NSUserDefaults.
WCSession.defaultSession() will return the WCSession singleton for transferring data between your iOS and Watch app.
Here is a tutorial and example.
Since watch os 2 apps run on the watch and not the iPhone, I don't think you have access to NSUserDefaults or app groups. You'll have to use WatchConnectivity framework to transfer data to and from the watch.
You can follow this steps
1) set the session
if ([WCSession isSupported])
{
[[WCSession defaultSession] setDelegate:self];
[[WCSession defaultSession] activateSession];
}
2) Prepare data dictionary and send it with below method
[WCSession defaultSession] sendMessage:dataDict replyHandler:^(NSDictionary<NSString *,id> * _Nonnull replyMessage) {
//You task on completion
} errorHandler:^(NSError * _Nonnull error) {
if (error)
{
//Handle the error
}
}];
3) And in watch app code
You can set delegate method
- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *, id> *)message replyHandler:(void(^)(NSDictionary<NSString *, id> *replyMessage))replyHandler
{
//Handle the response
}
Related
I have just set up WCConnectivity in my app using tutorials/sample code and I feel I have it implemented correctly. I am not using the simulator to test. For some reason, the didReceiveApplicationContext is not being called in the watch app, even though everything is set up correctly.
I've tried calling in the the Interface Controller of the WatchKit app, in the ExtensionDelegate and using NSUserDefaults to set the Interface Controller data instead.
iOS App
ViewController.m
- (void) viewDidLoad{
if ([WCSession isSupported]) {
WCSession *session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
}
}
-(void) saveTrip{
NSMutableArray *currentTrips = [NSMutableArray arrayWithArray:[self.sharedDefaults objectForKey:#"UserLocations"]];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:newLocation];
[currentTrips addObject:data];
[self.sharedDefaults setObject:currentTrips forKey:#"UserLocations"];
[self.sharedDefaults synchronize];
WCSession *session = [WCSession defaultSession];
NSDictionary *applicationDict = [[NSDictionary alloc] initWithObjects:#[currentTrips] forKeys:#[#"UserLocations"]];;
[session updateApplicationContext:applicationDict error:nil];
}
Watch Extension Code
ExtensionDelegate.m
- (void)applicationDidFinishLaunching {
if ([WCSession isSupported]) {
WCSession *session = [WCSession defaultSession];
session.delegate = self;
[session activateSession];
}
}
- (void)session:(nonnull WCSession *)session didReceiveApplicationContext:(nonnull NSDictionary<NSString *,id> *)applicationContext {
self.places= [applicationContext objectForKey:#"UserLocations"];
[[NSUserDefaults standardUserDefaults] setObject:self.places forKey:#"UserLocations"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
InterfaceController.m
- (void)willActivate {
[super willActivate];
self.placesData = [NSMutableArray arrayWithArray:[[NSUserDefaults standardUserDefaults] objectForKey:#"UserLocations"]];
[self loadData];
}
For me it was two things:
Passing invalid values in the dictionary. Can't even pass NSNull() to represent nil values. If you have data in there that can't be represented in a plist, it fails.
If the dictionary doesn't change, subsequent calls to updateApplicationContext won't trigger a corresponding call to didReceiveApplicationContext. To force an update—perhaps in debug builds—you could add a UUID to the payload, e.g.
context["force_send"] = UUID().uuidString
It might prove useful to handle any errors, so change:
[session updateApplicationContext:applicationDict error:nil];
to
NSError *error;
if (![session updateApplicationContext:applicationDict error:&error]) {
NSLog(#"updateApplicationContext failed with error %#", error);
}
If it's not working, it's probably not implemented correctly.
A few issues at first glance:
In your iOS app, you get a reference to the shared session and activate it, but don't assign that local variable to any property on your view controller. This means your local variable will go out of scope, once viewDidLoad exits. You also need to correct that in your watch extension.
In saveTrip, you again create another local session, which isn't activated and doesn't have any delegate. You need to use the first session that you set up and activated earlier.
On your watch, you save data that is received but your interface controller won't know that there is new data that it should load and display.
A few tips:
If you setup and activate the session in application:didFinishLaunchingWithOptions, it will be available app-wide (and activated much earlier in your app's lifecycle).
You should check that your session is valid, as a watch may not be paired, or the watch app may not be installed. Here's a good (Swift) tutorial that covers those cases, and also uses a session manager to make it easier to support using Watch Connectivity throughout your app.
You may want to pass less/lighter data across to the watch, than trying to deal with archiving an array of custom objects.
As an aside, what you're trying to do with NSUserDefaults is very convoluted. It's really meant to persist preferences across launches. It's not appropriate to misuse it as a way to pass your model back and forth between your extension and controller.
I had the same issue and fixed.
I forgot to add WatchConnectivity framework in watch extension.
I've built an app for iOS 9 and WatchOS 2. The iOS app will periodically transfer image files from the iPhone to the Watch. Sometimes, these are pushed from the app, sometimes the Watch requests (pulls) them. If pulled, I make the requests asynchronous, and use the exact same iOS code to transfer images in both cases.
About half the time (maybe 2/3), the file transfer works. The other times, it appears that nothing happens. This is the same whether I'm pushing or pulling images.
On the iOS side, I use code similar to this (session activated already):
if ([WCSession isSupported]) {
WCSession *session = [WCSession defaultSession];
if (session.reachable) {
NSData *imgData = UIImagePNGRepresentation(img);
NSURL *tempFile = [[session watchDirectoryURL] URLByAppendingPathComponent: #"camera.png"];
BOOL success = [imgData writeToFile: [tempFile path] atomically: NO];
if (success) {
NSLog(#"transferFile:metadata:");
[session transferFile: tempFile metadata: nil];
} else {
NSLog(#"will not call transferFile:metadata:");
}
} else {
NSLog(#"Camera watch client not reachable.");
}
}
On the watch extension side, I have a singleton that activates the watch session and receives the file:
- (void)session:(WCSession *)session didReceiveFile:(WCSessionFile *)file {
// pass the data file to the data listener (if any)
[self.dataListener session: session didReceiveFile: file];
}
My "data listener" converts the file to a UIImage and displays it on the UI thread. However, that's probably irrelevant, as the unsuccessful operations never get that far.
During unsuccessful transfers, session:didReceiveFile: is never called. If I inspect the iOS app's log, however, I see these messages only during the operations that fail:
Dec 26 15:10:47 hostname companionappd[74893]: (Note ) WatchKit:
application (com.mycompany.MyApp.watchkitapp), install status: 2,
message: application install success
Dec 26 15:10:47 hostname
companionappd[74893]: (Note ) WatchKit: Purging
com.mycompany.MyApp.watchkitapp from installation queue, 0 apps
remaining
What is happening here? It looks like the app is trying to reinstall the Watch app (?). When this is happening, I do not see the watch app crash/close and restart. It simply does nothing. No file received.
On the iOS side, I scale down the image to about 136x170 px, so the PNG files shouldn't be too big.
Any ideas what's going wrong?
Update:
I have posted a complete, minimal project that demonstrates the problem on Github here
I am now under the impression that this is a bug in the simulators. It seems to work more reliably on the Apple Watch hardware. Not sure if it's 100% reliable, though.
Apple bug report filed (#24023088). Will update status if there is any, and leave unsolved for any potential answers that may provide workarounds.
For me, not a single transfer was working anymore. Polling transfer.progress showed isTransferring == true, but I never got beyond 0 completed units.
I ended up:
Deleting apps on watch and iPhone
Rebooting both
Reinstalling
And it works.
This is how I managed to transfer files from phone to watch:
In order for this to work, the file must be locate in appGroupFolder, and "App Groups" must be enabled from Capabilities tab, for phone and watch.
In order to get appGroup folder use following line of code:
NSURL * myFileLocationFolder = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier: #"myGroupID"]; //something like group.bundle.projName
Once you got that use this to send message and handle response from watch:
[session sendMessage:#{#"file":myFileURL.absoluteString} replyHandler:^(NSDictionary<NSString *,id> * _Nonnull replyMessage) {
//got reply
} errorHandler:^(NSError * _Nonnull error) {
//got Error
}];
Even though WCSession *session = [WCSession defaultSession]; I have noticed that sometimes session is deallocated, so you might consider using [WCSession defaultSession]; instead.
To catch this on the phone use:
- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *, id> *)message replyHandler:(void(^)(NSDictionary<NSString *, id> *replyMessage))replyHandler{
//message[#"file"] - addres to my file
//do stuff with it here
replyHandler(#{#"myResponse":#"responseData"}); //this call triggers replyHandler block on the watch
}
Now a if you didn't forget to implement WCSessionDelegate and use
if ([WCSession isSupported]) {
_session = [WCSession defaultSession];
_session.delegate = self;
[_session activateSession];
}
//here session is #property (strong, nonatomic) WCSession * session;
It all should work.
Made a broader answer, hopefully will reach out to more people.
i am trying to create complication for watchOS2. I have created new target for my iOS application - with Glances and Complications
I want to have only one Modular Large Complication.
When I run trying to set complication Watch freezes (on both simulator and real Watch)
Here's my complication code:
-(void)getCurrentTimelineEntryForComplication:(CLKComplication *)complication withHandler:(void (^)(CLKComplicationTimelineEntry * _Nullable))handler {
if (complication.family == CLKComplicationFamilyModularLarge) {
CLKComplicationTemplateModularLargeColumns *template = [[CLKComplicationTemplateModularLargeColumns alloc] init];
NSString *title = NSLocalizedString(#"TODAYINTAKE", nil);
template.row1Column1TextProvider = [CLKSimpleTextProvider textProviderWithText:title];
template.row2Column2TextProvider = [CLKSimpleTextProvider textProviderWithFormat:#"kcal"];
template.row3Column2TextProvider = [CLKSimpleTextProvider textProviderWithFormat:#"ml"];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([self isDateToday:[defaults objectForKey:#"dateSaved"]]) {
template.row2Column1TextProvider = [CLKSimpleTextProvider textProviderWithFormat:#"%#",[defaults objectForKey:#"energy"]];
template.row3Column1TextProvider = [CLKSimpleTextProvider textProviderWithFormat:#"%#", [defaults objectForKey:#"water"]];
} else {
template.row2Column1TextProvider = [CLKSimpleTextProvider textProviderWithFormat:#"0"];
template.row3Column1TextProvider = [CLKSimpleTextProvider textProviderWithFormat:#"0"];
}
template.row2ImageProvider = [CLKImageProvider imageProviderWithOnePieceImage:[UIImage imageNamed:#"energy64"]];
template.row3ImageProvider = [CLKImageProvider imageProviderWithOnePieceImage:[UIImage imageNamed:#"water64"]];
template.row1ImageProvider = [CLKImageProvider imageProviderWithOnePieceImage:[UIImage imageNamed:#"64"]];
template.row1Column2TextProvider = [CLKSimpleTextProvider textProviderWithFormat:#" "];
CLKComplicationTimelineEntry *entry = [CLKComplicationTimelineEntry entryWithDate:[NSDate new] complicationTemplate:template];
handler(entry);
} else handler(nil);
}
-(void)getPlaceholderTemplateForComplication:(CLKComplication *)complication withHandler:(void (^)(CLKComplicationTemplate * _Nullable))handler {
if (complication.family == CLKComplicationFamilyModularLarge) {
CLKComplicationTemplateModularLargeTable *template = [[CLKComplicationTemplateModularLargeTable alloc] init];
NSString *title = NSLocalizedString(#"TODAYINTAKE", nil);
template.headerTextProvider = [CLKSimpleTextProvider textProviderWithText:title];
template.row1Column2TextProvider = [CLKSimpleTextProvider textProviderWithFormat:#"kcal"];
template.row2Column2TextProvider = [CLKSimpleTextProvider textProviderWithFormat:#"ml"];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([self isDateToday:[defaults objectForKey:#"dateSaved"]]) {
template.row1Column1TextProvider = [CLKSimpleTextProvider textProviderWithFormat:#"%#",[defaults objectForKey:#"energy"]];
template.row2Column1TextProvider = [CLKSimpleTextProvider textProviderWithFormat:#"%#", [defaults objectForKey:#"water"]];
} else {
template.row1Column1TextProvider = [CLKSimpleTextProvider textProviderWithFormat:#"0"];
template.row2Column1TextProvider = [CLKSimpleTextProvider textProviderWithFormat:#"0"];
}
handler(template);
} else handler(nil);
}
i am passing CLKComplicationTimeTravelDirectionNone as supported time travel directions
I am helpless since i am can't see any error in console and simulator or device just freezes.
From Carousel crash report I was able to read this information:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Application is required. bundleID: ql.ManaEU.watchkitapp appToReplace: proxy: ql.ManaEU.watchkitapp <(null) Not found in database>'
terminating with uncaught exception of type NSException
abort() called
CoreSimulator 191.4 - Device: Apple Watch - 42mm - Runtime: watchOS 2.0 (13S343) - DeviceType: Apple Watch - 42mm
FYI, I was able to customize the watch face, using the extension code you provided. No problem there.
If you notice the bundle id in the crash log error, the system is reporting a problem with the watchkit app (which contains the watchkit extension).
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Application is required. bundleID: ql.ManaEU.watchkitapp ...
You'll need to track down what's wrong with the watchkit bundle. The first place to start would be the Xcode watchkit app build target log. If there are no errors or warnings there, check the iPhone and Apple Watch console logs.
If that doesn't point you to the problem, check the Info.plist to make sure those values are valid, and the required keys are present. Also check the watchkit app target build settings.
You should be able to use the Version Editor to compare the Xcode project against its initial commit, to see if something was inadvertently changed or deleted.
You are providing a placeholder template of CLKComplicationTemplateModularLargeTable for the current timeline entry of CLKComplicationTemplateModularLargeColumns. The complication placeholder template should match the current timeline entry.
I am testing the execution of some code on the iPhone app. I follow the documentation that Apple suggests (without using background tasks, just a console log). However I do not get anything on the console (I'd like to see the string "howdy").
Is this because I am running the WatchKit Extension app on the simulator? Or is there something that I am missing?
Apple says:
If you are using openParentApplication:reply:, make sure you create a
background task immediately upon entering
application:handleWatchKitExtensionRequest:reply:. This will make sure
that the iPhone app gets time in the background instead of being
suspended again. Additionally, wrap the call to endBackgroundTask: in
a dispatch_after of 2 seconds to ensure that the iPhone app has time
to send the reply before being suspended again.
My implementation on the WatchKit extension (action method linked to a button):
- (IBAction)sendMessageToApp{
NSString *requestString = [NSString stringWithFormat:#"executeMethodA"]; // This string is arbitrary, just must match here and at the iPhone side of the implementation.
NSDictionary *applicationData = [[NSDictionary alloc] initWithObjects:#[requestString] forKeys:#[#"theRequestString"]];
[WKInterfaceController openParentApplication:applicationData reply:^(NSDictionary *replyInfo, NSError *error) {
NSLog(#"\nReply info: %#\nError: %#",replyInfo, error);
}];
NSLog(#"sending message..");
}
My implementation on the AppDelegate.m file:
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo))reply {
NSString * request = [userInfo objectForKey:#"requestString"];
NSLog(#"howdy");
// This is just an example of what you could return. The one requirement is
// you do have to execute the reply block, even if it is just to 'reply(nil)'.
// All of the objects in the dictionary [must be serializable to a property list file][3].
// If necessary, you can covert other objects to NSData blobs first.
NSArray * objects = [[NSArray alloc] initWithObjects:[NSDate date],[NSDate date],[NSDate date], [NSDate date], nil];
NSArray * keys = [[NSArray alloc] initWithObjects:#"objectAName", #"objectdName", #"objectBName", #"objectCName", nil];
NSDictionary * replyContent = [[NSDictionary alloc] initWithObjects:objects forKeys:keys];
reply(replyContent);
}
What I get in the console:
2015-04-16 14:43:44.034 FanLink WatchKit Extension[3324:142904] <InterfaceController: 0x608000081fe0> initWithContext
2015-04-16 14:44:04.848 FanLink WatchKit Extension[3324:142904] sennding message..
2015-04-16 14:44:04.854 FanLink WatchKit Extension[3324:142904]
Reply info: {
objectAName = "2015-04-16 13:44:04 +0000";
objectBName = "2015-04-16 13:44:04 +0000";
objectCName = "2015-04-16 13:44:04 +0000";
objectdName = "2015-04-16 13:44:04 +0000";
}
Error: (null)
This is because you are only debugging the WatchKit extension target. If you wanted to see in the console your main application logs as well you will need to attach the main application's process through the Xcode debugger.
Go to Debug-->Attach a process-->(Then select by identifier and type you applications name)
For a better walk through I found this great resource for you specially for WatchKit/WatchKit extension debugging:
https://mkswap.net/m/blog/How+to+debug+an+iOS+app+while+the+WatchKit+app+is+currently+running+in+the+simulator
Your console logs are correct because you are currently debugging only WatchKit Extension target So you will receive logs which are written in WatchKit extension. NSLog written in iOS App will not be printed in console.
don't know if off topic but there is a simple mistake in the code above
in watch kit there is initWithObjects:#[requestString] forKeys:#[#"theRequestString"]
and in appdelegate NSString * request = [userInfo objectForKey:#"requestString"];
"theRequestString" do not corespondent to "requestString"
took me some time to figure out why my function doesn't fire up
thanks for the code it helped
I have one question near the end.
I am working from the belief/experience that seeding iCloud more than once is a bad idea and that if a user can do the wrong thing, he probably will sooner or later.
What I want to do:
A. When the user changes the app preference "Enable iCloud" from NO to YES, display AlertView asking (Yes or No) if the user wishes to seed the cloud with existing non-iCloud Data.
B. Ensure that the app seeds iCloud only once on an iCloud account, refraining to put up the AlertView once seeding is completed the first time.
My Method:
Following Apple's Docs concerning the proper use of NSUbiquitousKeyValueStore, I am using the following method in, - (void)application: dFLWOptions:
- (void)updateKVStoreItems:(NSNotification*)notification {
// Get the list of keys that changed.
NSDictionary* userInfo = [notification userInfo];
NSNumber* reasonForChange = [userInfo objectForKey:NSUbiquitousKeyValueStoreChangeReasonKey];
NSInteger reason = -1;
// If a reason could not be determined, do not update anything.
if (!reasonForChange)
return;
// Update only for changes from the server.
reason = [reasonForChange integerValue];
if ((reason == NSUbiquitousKeyValueStoreServerChange) ||
(reason == NSUbiquitousKeyValueStoreInitialSyncChange)) { // 0 || 1
// If something is changing externally, get the changes
// and update the corresponding keys locally.
NSArray* changedKeys = [userInfo objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey];
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
// This loop assumes you are using the same key names in both
// the user defaults database and the iCloud key-value store
for (NSString* key in changedKeys) {//Only one key: #"iCloudSeeded" a BOOL
BOOL bValue = [store boolForKey:key];
id value = [store objectForKey:#"iCloudSeeded"];
[userDefaults setObject:value forKey:key];
}
}
}
Include the following code near the top of application: dFLWO:
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(updateKVStoreItems:)
name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
object:store]; // add appDelegate as observer
After loading iCloud Store, then seed it with non-iCloud data ONLY if seeding has never been done
- (BOOL)loadiCloudStore {
if (_iCloudStore) {return YES;} // Don’t load iCloud store if it’s already loaded
NSDictionary *options =
#{
NSMigratePersistentStoresAutomaticallyOption:#YES
,NSInferMappingModelAutomaticallyOption:#YES
,NSPersistentStoreUbiquitousContentNameKey:#"MainStore"
};
NSError *error=nil;
_iCloudStore = [_coordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil URL:[self iCloudStoreURL] options:options error:&error];
if (_iCloudStore) {
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
BOOL iCloudSeeded =
[store boolForKey:#"iCloudSeeded"];//If the key was not found, this method returns NO.
if(!iCloudSeeded) // CONTROL IS HERE
[self confirmMergeWithiCloud]; // Accept one USER confirmation for seeding in AlertView ONCE world wide
return YES; // iCloud store loaded.
}
NSLog(#"** FAILED to configure the iCloud Store : %# **", error);
return NO;
}
Once the seeding is completed do the following to prevent any repeat seeding:
if (alertView == self.seedAlertView) {
if (buttonIndex == alertView.firstOtherButtonIndex) {
[self seediCloud];
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
[store setBool:YES forKey:#"iCloudSeeded"]; // NEVER AGAIN
//[store synchronize];
}
}
}
Be sure to get a total iCloud reset before the above process using:
[NSPersistentStoreCoordinator
removeUbiquitousContentAndPersistentStoreAtURL:[_iCloudStore URL]
options:options
error:&error])
This is a very tidy solution to my problem, IMHO, but I can not quite get it done.
MY QUESTION:
How do I respond to the first notification to updateKVStoreItems: above? It is a notification with bad info. I says the value is TRUE, but I have never set it to TRUE. How do I set default values for a key in NSUbiquitousKeyValueStore?
I find that the first notification is of reason : NSUbiquitousKeyValueStoreInitialSyncChange
When that note comes in, bValue is YES. THIS IS MY PROBLEM. It is as if, iCloud/iOS assumes any new BOOL to be TRUE.
I need this value to be NO initially so that I can go ahead and follow the Apple Docs and set
the NSUserDefault to NO. And then Later when the seeding is done, to finally set the value: YES for the key:#"iCloudSeeded"
I find I can not penetrate the meaning of the following from Apple:
NSUbiquitousKeyValueStoreInitialSyncChange
Your attempt to write to key-value storage was discarded because an initial download from iCloud has not yet happened.
That is, before you can first write key-value data, the system must ensure that your app’s local, on-disk cache matches the truth in iCloud.
Initial downloads happen the first time a device is connected to an iCloud account, and when a user switches their primary iCloud account.
I don't quite understand the implications of number 2 below, which I found online:
NSUbiquitousKeyValueStoreInitialSyncChange – slightly more complicated, only happens under these circumstances:
1. You start the app and call synchronize
2. Before iOS has chance to pull down the latest values from iCloud you make some changes.
3. iOS gets the changes from iCloud.
If this problem was with NSUserDefaults and not NSUbiquitousKeyValueStore, I believe I would need to go to registerDefaults.
I am almost there,
How do I do this please!
Thanks for reading, Mark
The code was looking for both
A. NSUbiquitousKeyValueStoreInitialSyncChange and
B. NSUbiquitousKeyValueStoreServerChange
I was unable to figure out what to do with the notifications. I know see that I did not need to do anything with either. My app only needs to read and write, in order to solve the problem I laid out in my question header.
The app gets the current value with:
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
BOOL iCloudSeeded = [store boolForKey:#"iCloudSeeded"];
The app sets the value in the NSUbiquitousKeyValueStore with:
NSUbiquitousKeyValueStore* store = [NSUbiquitousKeyValueStore defaultStore];
[store setBool:YES forKey:#"iCloudSeeded"];
I believe I am correct in saying the following: Writing is done into memory. Very soon thereafter the data is put by the system onto disk.
From there it is taken and put into iCloud and is made available to the other devices running the same app on the same iCloud account. In the application I have described, no observer needs to be added, and
nothing else needs to be done. This is maybe an "unusual" use of NSUbiquitousKeyValueStore.
If you came here looking for a an more "usual" use, say when a user type something into a textview and it later
appears on a view of other devices running the same app, check out a simple demo I came across at :
https://github.com/cgreening/CMGCloudSyncTest
The better functioning (monitoring only) notification handler follows:
- (void)updateKVStoreItems:(NSNotification*)notification {
NSNumber *reason = notification.userInfo[NSUbiquitousKeyValueStoreChangeReasonKey];
if(!reason) return;
// get the reason code
NSInteger reasonCode = [notification.userInfo[NSUbiquitousKeyValueStoreChangeReasonKey] intValue];
BOOL bValue;
NSUbiquitousKeyValueStore *store;
switch(reasonCode) {
case NSUbiquitousKeyValueStoreServerChange:{ // code 0, monitoring only
store = [NSUbiquitousKeyValueStore defaultStore];
bValue = [store boolForKey:#"iCloudSeeded"];
id value = [store objectForKey:#"iCloudSeeded"];
DLog(#"New value for iCloudSeeded=%d\nNo Action need be take.",bValue);
// For monitoring set in UserDefaults
[[NSUserDefaults standardUserDefaults] setObject:value forKey:#"iCloudSeeded"];
break;
}
case NSUbiquitousKeyValueStoreAccountChange: {// ignore, log
NSLog(#"NSUbiquitousKeyValueStoreAccountChange");
break;
}
case NSUbiquitousKeyValueStoreInitialSyncChange:{ // ignore, log
NSLog(#"NSUbiquitousKeyValueStoreInitialSyncChange");
break;
}
case NSUbiquitousKeyValueStoreQuotaViolationChange:{ // ignore, log
NSLog(#"Run out of space!");
break;
}
}
}
Adding 9/3/14
So sorry but I continued to have trouble using a BOOL, I switched to an NSString and now
all is well.
METHOD TO ENSURE THAT THE "MERGE" BUTTON FOR SEEDING ICOUD IS USED AT MOST ONCE DURING APP LIFETIME
Use NSString and not BOOL in KV_STORE. No need to add observer, except for learning
In Constants.h :
#define SEEDED_ICLOUD_MSG #"Have Seeded iCloud"
#define ICLOUD_SEEDED_KEY #"iCloudSeeded"
Before calling function to seed iCloud with non-iCloud data:
NSUbiquitousKeyValueStore* kvStore = [NSUbiquitousKeyValueStore defaultStore];
NSString* strMergeDataWithiCloudDone =
[kvStore stringForKey:ICLOUD_SEEDED_KEY];
NSComparisonResult *result = [strMergeDataWithiCloudDone compare:SEEDED_ICLOUD_MSG];
if(result != NSOrderedSame)
//put up UIAlert asking user if seeding is desired.
If user chooses YES : set Value for Key after the merge is done.
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (alertView == self.seedAlertView) {
if (buttonIndex == alertView.firstOtherButtonIndex) {
[self seediCloudwithNoniCloudData];
NSUbiquitousKeyValueStore* kvStoretore = [NSUbiquitousKeyValueStore defaultStore];
[store setObject:SEEDED_ICLOUD_MSG forKey:ICLOUD_SEEDED_KEY];
}
}
}
Thereafter on all devices, for all time, the code
NSUbiquitousKeyValueStore* kvStoretore = [NSUbiquitousKeyValueStore defaultStore];
NSString* msg =
[kvStore stringForKey:ICLOUD_SEEDED_KEY];
produces: msg == SEEDED_ICLOUD_MESSAGE