NSUserDefaults from AppGroup is empty in Watch App - ios

My app has the following targets:
iOS App
Today Extension
Apple Watch App
Apple Watch Extension
All of these targets have the "App Group" capability with the correct group selected. In the main iOS app, I synchronize the settings like so:
NSUserDefaults * sharedDefaults = [[NSUserDefaults alloc]initWithSuiteName:#"group.foo.bar"];
if([sharedDefaults respondsToSelector:#selector(setObject:forKey:)]){
// Example Value
[sharedDefaults setObject:#"TestString" forKey:#"TestString"];
[sharedDefaults synchronize];
}
The Today Extension uses the following to get the settings, which works just fine:
NSUserDefaults * defaults = [[NSUserDefaults alloc] initWithSuiteName:#"group.foo.bar"];
NSLog(#"String: %#", [defaults stringForKey:#"TestString"]);
// String: TestString
The watch extension uses the same line of code to get the settings as the today extension, however it does not work. In the console I see String: (null). What am I doing incorrectly?

Did you create your watch target as a watchOS WatchKit Application (watchOS 2) or as an iOS Application Extension (watchOS 1)? If targeting for watchOS 2, according to the transition guide: You cannot rely on a shared group container to exchange files with your iOS app. Fetching files involves transferring them wirelessly to Apple Watch.

Please refer to settings bundle not working on watchOS 2 Apparently the docs are not that clear on what can and cannot be done with Preferences, the only way I found was to ensure that I copy the required user defaults over and keep them synced manually (NSUserDefaults not working on Xcode beta with Watch OS2).

Related

How to check in code if current target has watch app version available?

I'm working in a view where I should customize it regarding if there is a Watch Version of the app.
I know that I can use:
[[WCSession defaultSession] isWatchAppInstalled]
(but that is not what I want because the user can uninstall the app in the Apple Watch but the iOS app will still have the Watch Version available to install)
also:
[[WCSession defaultSession] isPaired] is not my case.
Actually isWatchAppInstalled is the correct way to deal with this scenario.
As the documentation states:
The user can choose to install only a subset of available apps on Apple Watch. The value of this property is true when the Watch app associated with the current iOS app is installed on the user’s Apple Watch or false when it is not installed.
https://developer.apple.com/documentation/watchconnectivity/wcsession/1615623-iswatchappinstalled
The property does not look inside the bundle to see if a Watch app is available. It will only return true if the Watch app is installed on the currently paired Apple Watch. If the user deletes the Watch App from the watch it will return false.
After checking the current project that I'm working on and also testing creating two new projects to check the bundles (one project with watch and the another without watch included), I saw the difference between them:
The difference is that the project with the watch included has a Build Phase that has the Watch app embedded in the subdirectory called "Watch".
Also we can see the "Watch" folder when Showing the Package Contents of the build in Finder:
So the condition whenever that iOS app has Watch embedded in code is:
+ (BOOL)isWatchAppEmbedded
{
NSString *watchPath = [NSString stringWithFormat:#"%#/%#", [NSBundle mainBundle].resourcePath, #"Watch"];
BOOL isDirectory = YES;
if ([[NSFileManager defaultManager] fileExistsAtPath:watchPath isDirectory:&isDirectory]) {
return YES;
}
return NO;
}

iOS 10 Today extension can't read shared user defaults any more

I have an application that implements Today extension. Till iOS10 it was working fine, but now I see that it can't read keys stored in shared NSuserDefaults.
My Host app code:
NSUserDefaults *sharedPrefs = [[NSUserDefaults alloc] initWithSuiteName:kSharedGroupIdentifier];
[sharedPrefs setObject:username forKey:#"username"];
[sharedPrefs setObject:#"Test" forKey:#"test"];
[sharedPrefs synchronize];
My extension code:
NSUserDefaults *sharedPrefs = [[NSUserDefaults alloc] initWithSuiteName:kSharedGroupIdentifier];
NSString *str = [sharedPrefs valueForKey:#"test"];
And str var is always nil in extension. I have set up my App Groups in Capabilities for both Host app and extension app, and I have checked it against the settings on Apple developer portal. It is all OK. Can anyone please help, is there something different that I must do?
EDIT
Another thing that I have noticed is that I have a red exclamation mark on: "Add the Add Groups entitlement to your entitlement file", and when I fix the issue, the exclamation mark goes away, but when I close and reopen project it is there again. I have recreated provisional profiles, but still the same issue
It sounds like the issues is that your Entitlements file is missing the relevant line.
To diagnose where the issue is, start with your build settings:
Under Code Signing Entitlements there should be a file listed
Open that file and ensure that it has the App Groups key with the value that matches your kSharedGroupIdentifier
Ensure that the file is not marked read-only (this would potentially cause the "fix" to not work correctly)
Verify that all of the following match:
kSharedGroupIdentifier in both apps
kSharedGroupIdentifier and the value in your entitlements file for App Groups
Next, we can narrow the issue down to the specific key or the entire preferences domain as follows:
NSUserDefaults *sharedPrefs = [[NSUserDefaults alloc] initWithSuiteName:kSharedGroupIdentifier];
if (sharedPrefs == nil) {
NSLog(#"Error loading shared user defaults!");
}
A nil value indicates that the suite name is invalid.
Rebooting the mac solved the issue for me.
Apple RNotes WWDC Seed: Running multiple iOS simulators can cause NSUserDefaults to not work Running an iOS 8 or 9 simulator followed by an iOS 10 simulator will cause NSUserDefaults to stop working in the simulator. This can be worked around by rebooting the host Mac.
I hope it will helps you,
Thanks.

containerURLForSecurityApplicationGroupIdentifier: gives different results on iPhone and Watch simulator

I created a WatchKit Application with the default XCode Template.
I added an app group entitlement to the iOS Target, to the Watchkit App Target and to the Watchkit App Extension Target. (this is the app group name: group.com.lombax.fiveminutes)
Then, I tried to access the shared folder URL with both the iOS App and the WatchKit Extension:
Extension:
#implementation ExtensionDelegate
- (void)applicationDidFinishLaunching {
// Perform any final initialization of your application.
NSURL *test = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:#"group.com.lombax.fiveminutes"];
}
iOS App:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
NSURL *test = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:#"group.com.lombax.fiveminutes"];
// ...
}
However, the test NSURL is different:
On iOS:
file:///Users/Lombardo/Library/Developer/CoreSimulator/Devices/38B983DB-342F-4A47-8C26-5D2C92CDB666/data/Containers/Shared/AppGroup/8DEE182E-AFE6-47DD-BA2B-6B0520158A8B/
on Watch:
file:///Users/Lombardo/Library/Developer/CoreSimulator/Devices/BF52D613-25FF-4092-A5B2-9C3F1B616160/data/Containers/Shared/AppGroup/CECB5EFC-7FBD-4C84-A878-1314CB7CF211/
And for this reason I'm unable to share data between the iOS App and the WatchKit Extension.
I cannon try on a real device since I don't have WatchOS 2.0 on my Apple Watch.
Any advice?
Thanks
UPDATE
I did some other tests:
Installed WatchOS 2, the issue still persists on real devices.
This is the store url for my iPhone:
NSURL
* #"file:///private/var/mobile/Containers/Shared/AppGroup/3D05D159-94D6-409C-9A38-90E0830D0C3F/FiveMinutes.sqlite"
And this is the store url for my Watch:
NSURL
* #"file:///private/var/mobile/Containers/Shared/AppGroup/F1E89377-F456-4FC2-BAAC-3DD705EF381A/FiveMinutes.sqlite"
The two apps reads and write to-from two different .sqlite files.
On simulator, if I hard-code one of the URLs, both iOS simulator and Watch simulator are able to read-write the same .sqlite file and share the content. However, this is not possible on real devices since the Watch extension cannot write to the iOS path:
URL:file:///private/var/mobile/Containers/Shared/AppGroup/3D05D159-94D6-409C-9A38-90E0830D0C3F/FiveMinutes.sqlite options:(null) ... returned error Error Domain=NSCocoaErrorDomain Code=512 "The file couldn’t be saved." UserInfo={reason=Failed to create file; code = 2} with userInfo dictionary {
reason = "Failed to create file; code = 2";
}
Ok, I think I've found my answer. I remembered that with the transition to Watch OS 2 the extension code is now executed directly on the Apple Watch, and no more on the paired iPhone. So it's seems obvious that the two devices doesn't share the same storage.
The first thing I did was to create a new project, starting from a base iOS Project, and then adding a Watch OS 1 (old version) App Target.
In this case, the directories were identical and they could communicate:
Watch Path: file:///Users/Lombardo/Library/Developer/CoreSimulator/Devices/BF52D613-25FF-4092-A5B2-9C3F1B616160/data/Containers/Shared/AppGroup/30B39103-CEEB-4C64-9531-FB27DC40180D/
iOS Path file:///Users/Lombardo/Library/Developer/CoreSimulator/Devices/BF52D613-25FF-4092-A5B2-9C3F1B616160/data/Containers/Shared/AppGroup/30B39103-CEEB-4C64-9531-FB27DC40180D/
Then, I did the first thing every programmer should do: read the docs.
In the FIRST PAGE of the WatchOS 2 transition guide there is this sentence:
Your extension now stores files and data on Apple Watch. Any data that is not part of your Watch app or WatchKit extension bundle must be fetched from the network or from the companion iOS app running on the user’s iPhone. You cannot rely on a shared group container to exchange files with your iOS app. Fetching files involves transferring them wirelessly to Apple Watch.

Can't read WatchKit settings bundle from iPhone app

In watchOS 2, it seems like you can't access the data from the WatchKit settings bundle from the watch extension itself, because it now runs on the watch instead of the host iPhone. A solution, which was proposed here, was to read the data on the iPhone and then transfer it to the watch.
My problem is, that I cannot only read the data from the watch, but even from my phone. In ViewController.m I have the following code to get the value of a switch:
NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:#"group.de.iossoftware.SocialAppDataSharing"];
NSLog(#"%d", [[[NSUserDefaults alloc] initWithSuiteName:#"group.de.iossoftware.SocialAppDataSharing"] boolForKey:#"key_here"]);
It always returns "0", even when the switch in the settings bundle is enabled.
I'm using app groups and I made sure that I use the same identifier in the settings bundle and in the iPhone app:
Why can't I access the bundle data from my phone?
Try this:
NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:#"group.de.iossoftware.SocialAppDataSharing"];
[userDefaults setValue:#55 forKey:#"myNumber"];
[userDefaults synchronize];
Execute one time in iOS simulator and then remove it and add:
NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:#"group.de.iossoftware.SocialAppDataSharing"];
NSLog(#"My number: %#",[userDefaults valueForKey:#"myNumber"]
And execute one more time. You should see the value saved before
NSError *error = nil;
NSLog(#"### Directory path: %#",[[NSFileManager defaultManager]contentsOfDirectoryAtPath:[[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:#"group.de.iossoftware.SocialAppDataSharing"].path error:&error]);
NSLog(#"Error: %#",error);
And in the last line you will log the file path where the group files are saved. Open it in the finder and you should see under ...../Library/Preferences/ a plist containing the NSUserdefaults created
I'm not sure exactly what you are trying to do as your questions isn't clear to me. However, with WatchOS 2, you cannot use shared group containers, you must use Watch Connectivity framework to sync data between the watch and phone. This is similar to the question here: NSUserDefaults not working on Xcode beta with Watch OS2

Access the settings.bundle from today-extension

In the app I can choose in the settings which language of three should be used. If nothing is selected, the iPhone language is detected and selected for english, french or german. If none of this languages is the iPhone language, english is set to be used. Manually changing the language in the settings works as it should. Now I added a today-extension which works nearly well but I need to access the settings-bundle to get the NSUserDefaults for the languages (if manually changed). In both targets I activated the App-Groups with
group.com.companyname.appname
and selected it.
In the app I get the language with
NSString *manualLanguageSet = [[NSUserDefaults standardUserDefaults] valueForKey:SPRACHWAHL];
and in the today-extension I try to get it by:
NSString *manualLanguageSet = [[[NSUserDefaults alloc] initWithSuiteName:#"group.com.companyname.appname"] valueForKey:SPRACHWAHL];
For NSLog(#"Settings-Sprache: %#", manualLanguageSet); when running the today-extension the result is (doesn't matter if and which language is selected in the settings)
2014-09-13 16:48:36.331 HdB today[3734:284836] Settings-Sprache: (null)
What can I do it correct / how can I access to the settings (settings.bundle)?
If you create a WatchKit preferences bundle, it also contains the key
ApplicationGroupContainerIdentifier
at the root level of dictionary of the bundle's Root.plist, with the value being the group identifier (what you provide to NSUserDefaults as suiteName).
I found that you can also use this key in the iOS settings bundle. If added, preferences will go to the respective container.
I verified this to work on iOS 8.3 across the iOS Settings app, the iPhone app and the WatchKit extension.
UPDATE: Apple lists this key as supported in iOS 8.2 and later at
https://developer.apple.com/library/ios/documentation/PreferenceSettings/Conceptual/SettingsApplicationSchemaReference/Articles/RootContent.html
I'm searching for it as well. The only way i know how to do it is via SharedContainer. In your example you should open your
[[[NSUserDefaults alloc] initWithSuiteName:#"group.com.companyname.appname"] valueForKey:SPRACHWAHL];
in container app and then copy the language value from NSUserDefaults of container app like this :
[[[NSUserDefaults alloc] initWithSuiteName:#"group.com.companyname.appname"] setObject:[NSUserDesaults standardUserDefaults] objectForKey:SPRACHWAHL] forKEY:#"Your Key in extension"]];
then you will be able to access your shared container in you extension and get those values.
If someone have a better way of doing it i would be happy to know as well.

Resources