NSUserDefaults not working on Xcode beta with Watch OS2
Asked Answered
C

5

23

I just installed the latest beta of Xcode to try Swift 2 and the improvements made to the Apple Watch development section.

I'm actually having an hard time figuring out WHY this basic NSUserDefaults method to share informations between iOS and Watch OS2 isn't working.

I followed this step-by-step tutorial to check if I missed something in the process, like turning on the same group for both the phone application and the extension, but here's what I got: NOTHING.

Here's what I wrote for the ViewController in the iPhone app:

import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var lb_testo: UITextField!
    let shared_defaults:NSUserDefaults = NSUserDefaults(suiteName: "group.saracanducci.test")!
    var name_data:NSString? = ""

    override func viewDidLoad() {
        super.viewDidLoad()

        name_data = shared_defaults.stringForKey("shared")
        lb_testo.text = name_data as? String
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    @IBAction func upgrade_name(sender: AnyObject) {
        name_data = lb_testo.text
        shared_defaults.setObject(name_data, forKey: "shared")

        lb_testo.resignFirstResponder()
        shared_defaults.synchronize()
    }
}

And here's what I have in the InterfaceController for WatchKit:

import WatchKit
import Foundation

class InterfaceController: WKInterfaceController {
    @IBOutlet var lb_nome: WKInterfaceLabel!
    let shared_defaults:NSUserDefaults = NSUserDefaults(suiteName: "group.saracanducci.test")!
    var name_data:NSString? = ""

    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)
    }

    override func willActivate() {
        super.willActivate()

        if (shared_defaults.stringForKey("shared") != ""){
            name_data = shared_defaults.stringForKey("shared")
            lb_nome.setText(name_data as? String)
        }else{
            lb_nome.setText("No Value")
        }
    }

    override func didDeactivate() {
        super.didDeactivate()
    }
}

I made some tests and it seems like the iOS app and the Watch OS one take advantage of different groups...they're not sharing information, they store them locally.

Is someone having the same issue? Any idea how to fix it?

Cyclopedia answered 15/6, 2015 at 17:50 Comment(5)
Please include your code with the question so that a broken link doesn't render this question useless for future readers.Zeitler
.stringForKey("shared") already returns a string no need for casting. You should use nil coalescing operator to unwrap it name_data = NSUSerDefaults().stringForKey("shared") ?? "no Value"Auberta
The problem must lie elsewhere. The code you have is correct; I've done this pattern many times in 3 of my watch apps. I don't think this will help, but try moving the NSUserDefaults call to your data source, so that you get the NSUserDefaults reference from a single place rather than initializing 2 different copies of it.Scrubland
I'd add xcode 7 / watch OS2 to title.Darrendarrey
For those interested, settings are still pushed from iOS to Apple Watch even in watchOS 2. See my answer at https://mcmap.net/q/473455/-settings-bundle-not-working-on-watchos-2Encroach
S
43

With watch OS2 you can no longer use shared group containers. Apple Docs:

Watch apps that shared data with their iOS apps using a shared group container must be redesigned to handle data differently. In watchOS 2, each process must manage its own copy of any shared data in the local container directory. For data that is actually shared and updated by both apps, this requires using the Watch Connectivity framework to move that data between them.

Steverson answered 15/6, 2015 at 20:5 Comment(4)
Thanks man, I was going crazy with this code! I'll figure out another way to make it work using the Watch Connectivity :)Cyclopedia
I'll be doing the same ;)Steverson
OMG - what a waste of time the last few days have been.Darrendarrey
DOH! - Tonight I thought "hey, why don't I redesign my app to use shared app groups" and then I spent hours trying to find out why it wasn't working. The worst part? Apparently I saw and accepted this answer two weeks ago!Unswerving
F
18

NSUserDefaults (even with an App Groups) don't sync between the iPhone and the Watch in watchOS 2. If you want to sync settings from either your iPhone app or the Settings-Watch.bundle, you have to handle the syncing yourself.

I've found that using WatchConnectivity's user info transfers works really well in this case. Below you'll find an example of how you could implement this. The code only handles one-way syncing from the phone to the Watch, but the other way works the same.

In the iPhone app:
1) Prepare dictionary of settings that need to be synced

- (NSDictionary *)exportedSettingsForWatchApp  
{  
    NSUserDefaults *userDefaults = [self userDefaults]; // the user defaults to sync  

    NSSet *keys = [self userDefaultKeysForWatchApp]; // set of keys that need to be synced  
    NSMutableDictionary *exportedSettings = [[NSMutableDictionary alloc] initWithCapacity:keys.count];  

    for (NSString *key in keys) {  
        id object = [userDefaults objectForKey:key];  

        if (object != nil) {  
            [exportedSettings setObject:object forKey:key];  
        }  
    }  

    return [exportedSettings copy];  
}  

2) Determine when the settings need to be pushed to the Watch
(not shown here)

3) Push the settings to the Watch

- (void)pushSettingsToWatchApp  
{  
    // Cancel current transfer  
    [self.outstandingSettingsTransfer cancel];  
    self.outstandingSettingsTransfer = nil;  

    // Cancel outstanding transfers that might have been started before the app was launched  
    for (WCSessionUserInfoTransfer *userInfoTransfer in self.session.outstandingUserInfoTransfers) {  
        BOOL isSettingsTransfer = ([userInfoTransfer.userInfo objectForKey:@"settings"] != nil);  
        if (isSettingsTransfer) {  
            [userInfoTransfer cancel];  
        }  
    }  

    // Mark the Watch as requiring an update  
    self.watchAppHasSettings = NO;  

    // Only start a transfer when the watch app is installed  
    if (self.session.isWatchAppInstalled) {  
        NSDictionary *exportedSettings = [self exportedSettingsForWatchApp];  
        if (exportedSettings == nil) {  
            exportedSettings = @{ };  
        }  

        NSDictionary *userInfo = @{ @"settings": exportedSettings };  
        self.outstandingSettingsTransfer = [self.session transferUserInfo:userInfo];  
     }  
}  

In the Watch extension:
4) Receive the user info transfer

- (void)session:(WCSession *)session didReceiveUserInfo:(NSDictionary<NSString *, id> *)userInfo  
{  
    NSDictionary *settings = [userInfo objectForKey:@"settings"];  
    if (settings != nil) {  
        // Import the settings  
        [self importSettingsFromCompanionApp:settings];  
     }  
} 

5) Save the received settings to the user defaults on the Watch

- (void)importSettingsFromCompanionApp:(NSDictionary *)settings  
{  
    NSUserDefaults *userDefaults = [self userDefaults]; // the user defaults to sync  

    NSSet *keys = [self userDefaultKeysForWatchApp]; // set of keys that need to be synced  
    for (NSString *key in keys) {  
        id object = [settings objectForKey:key];  
        if (object != nil) {  
            [userDefaults setObject:object forKey:key];  
        } else {  
            [userDefaults removeObjectForKey:key];  
        }  
    }  

    [userDefaults synchronize];  
}  
Frederico answered 12/8, 2015 at 8:1 Comment(5)
Any chance of posting a swift version?Developing
No, I'm sorry. But it shouldn't be hard to port it yourself.Frederico
see here for swift connectivity code developer.apple.com/videos/play/wwdc2015-713Holey
I see [self userdefaults]; can i use [NSUserDefaults standardUserDefaults] here?Min
Yes. In my case I was using group user defaults, that's why [self userDefaults] is there.Frederico
F
12

Theres a simple way to reproduce the old functionality, I export the old group user defaults into a dictionary, send that across WatchConnectivity framework and then reimport them into user defaults on the other side:

In both Phone and Watch apps:

  1. Add the WatchConnectivty framework
  2. #import <WatchConnectivity/WatchConnectivity.h> and declare as WCSessionDelegate
  3. Add code to start session after the app has launched:

    if ([WCSession isSupported]) {
            WCSession* session = [WCSession defaultSession];
            session.delegate = self;
            [session activateSession];
        }
    
  4. Use this to send the updated defaults to the other device (call after your current [defaults synchronize] ):

[[WCSession defaultSession] updateApplicationContext:[[[NSUserDefaults alloc] initWithSuiteName:@"group.com.company.myapp"] dictionaryRepresentation] error:nil];

  1. Receive and save the settings back to the default - add this to the WCDelegate:

    -(void)session:(WCSession *)session didReceiveApplicationContext:(NSDictionary<NSString *,id> *)applicationContext {
        NSLog(@"New Session Context: %@", applicationContext);
    
        NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.company.myapp"];
    
        for (NSString *key in applicationContext.allKeys) {
            [defaults setObject:[applicationContext objectForKey:key] forKey:key];
        }
    
        [defaults synchronize];
    }
    

Be careful to maintain support for non WC devices - wrap your updateApplicationContext calls with if ([WCSession isSupported])

Florina answered 17/9, 2015 at 10:28 Comment(3)
What if you change settings on the iPhone app while the watch app is not running (isReachable will be false)? The watch app won't be listening to the session. Later, when the watch app launches, it will have the old values. The watch app needs to wake the iPhone app and then arrange for some magic to happen.Bromberg
Excellent answer. Note that applicationContext includes quite a few keys relating to Apple things. To make this safer I recommend only adding data for a verified subset (e.g. NSArray) of key names that you are expecting.Mulvey
The best and concise answer.Viaticum
D
7

As mentioned already, shared NSUserDefaults no longer work on WatchOS2.

Here's the swift version of @RichAble's answer with a few more notes.

In your iPhone App, follow these steps:

Pick the view controller that you want to push data to the Apple Watch from and add the framework at the top.

import WatchConnectivity

Now, establish a WatchConnectivity session with the watch and send some data.

if WCSession.isSupported() { //makes sure it's not an iPad or iPod
    let watchSession = WCSession.defaultSession()
    watchSession.delegate = self
    watchSession.activateSession()
    if watchSession.paired && watchSession.watchAppInstalled {
        do {
            try watchSession.updateApplicationContext(["foo": "bar"])
        } catch let error as NSError {
            print(error.description)
        }
    }
}

Please note, this will NOT work if you skip setting the delegate, so even if you never use it you must set it and add this extension:

extension MyViewController: WCSessionDelegate {

}

Now, in your watch app (this exact code works for Glances and other watch kit app types as well) you add the framework:

import WatchConnectivity

Then you set up the connectivity session:

override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)
    let watchSession = WCSession.defaultSession()
    watchSession.delegate = self
    watchSession.activateSession()
}

and you simply listen and handle the messages from the iOS app:

extension InterfaceController: WCSessionDelegate {

    func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject]) {
        print("\(applicationContext)")
        dispatch_async(dispatch_get_main_queue(), {
            //update UI here
        })
    }

}

That's all there is to it.

Items of note:

  1. You can send a new applicationContext as often as you like and it doesn't matter if the watch is nearby and connected or if the watch app is running. This delivers the data in the background in an intelligent way and that data is sitting there waiting when the watch app is launched.
  2. If your watch app is actually active and running, it should receive the message immediately in most cases.
  3. You can reverse this code to have the watch send messages to the iPhone app the same way.
  4. applicationContext that your watch app receives when it is viewed will ONLY be the last message you sent. If you sent 20 messages before the watch app is viewed, it will ignore the first 19 and handle the 20th one.
  5. For doing a direct/hard connection between the 2 apps or for background file transfers or queued messaging, check out the WWDC video.
Domitiladomonic answered 26/1, 2016 at 19:47 Comment(1)
This is a great example, and I got it to work on the new watchOS 3, with swift 3 etc. Just a quick note, you must change the value you are sending - not just "bar", otherwise your message will only be sent once. This took me an hour to figure out :(Webbing
H
1

It took me hours and hours to get this. Watch this very useful video! It gives you the basic idea of how to use WatchConnectivity to share NSUserDefault between the iPhone app and wacth!

https://www.youtube.com/watch?v=VblFPEomUtQ

Holey answered 18/12, 2015 at 1:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.