Xcode beta配合Watch OS2使用时,NSUserDefaults无法正常工作

23

我刚刚安装了最新的Xcode beta版本,以尝试 Swift 2 和改进的苹果手表开发部分。

实际上,我很难理解为什么这个基本的NSUserDefaults方法在iOSWatch OS2之间共享信息时不起作用。

我按照这个一步一步的教程进行操作,检查我是否在过程中遗漏了某些步骤,例如在手机应用程序和扩展程序上同时启用相同的组,但这是我得到的结果:什么也没有

以下是我为iPhone应用程序的ViewController编写的内容:

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()
    }
}

这是我在WatchKit的InterfaceController中拥有的内容:

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()
    }
}

我进行了一些测试,似乎iOS应用程序和Watch OS应用程序利用不同的组......它们不共享信息,而是在本地存储。

有人遇到同样的问题吗?有什么解决办法吗?


请在问题中包含您的代码,以便于未来读者不会因为无法访问链接而使该问题变得无用。 - Caleb
1
.stringForKey("shared") 方法已返回字符串,不需要进行类型转换。您应该使用 nil 合并运算符来解包它。name_data = NSUSerDefaults().stringForKey("shared") ?? "no Value" - Leo Dabus
问题必须出在别的地方。你的代码是正确的;我已经在我的三个手表应用程序中多次使用了这种模式。我不认为这会有帮助,但尝试将NSUserDefaults调用移动到数据源中,这样你就可以从一个单一的位置获取NSUserDefaults引用,而不是初始化两个不同的副本。 - Kelvin Lau
我会在标题中加入Xcode 7 / Watch OS2。 - johndpope
对于那些感兴趣的人,即使在watchOS 2中仍然可以从iOS向Apple Watch推送设置。请参见我的回答https://dev59.com/yI_ea4cB1Zd3GeqPLTm8#32707727 - Charlie Schliesser
5个回答

43

使用watch OS2后,您将无法再使用共享组容器。 苹果文档:

在watchOS 2中,那些通过共享组容器与其iOS应用程序共享数据的手表应用程序必须重新设计以不同方式处理数据。每个进程都必须管理本地容器目录中任何共享数据的自己的副本。对于实际由两个应用程序共享和更新的数据,这需要使用Watch Connectivity框架在它们之间移动数据。


谢谢你,我用这个代码快疯了!我会想出另一种使用观察连接(Watch Connectivity)的方法让它起作用。 - Sara Canducci
我也会做同样的事情 ;) - rmp
17
天啊-过去几天真是浪费时间。 - johndpope
2
谢谢你救了我的命。 - Murali
2
糟糕!今晚我想:“嘿,为什么不重新设计我的应用程序以使用共享应用组”,然后我花了几个小时试图找出为什么它不起作用。最糟糕的部分是什么?显然,两周前我已经看到并接受了这个答案! - Dan

18

即使在使用应用程序组的情况下,NSUserDefaults也不能在watchOS 2中在iPhone和Watch之间同步。如果您想要从iPhone应用程序或Settings-Watch.bundle同步设置,则必须自行处理同步。

我发现在这种情况下使用WatchConnectivity的用户信息传输非常有效。 以下是一个示例,说明如何实现此操作。该代码仅处理从手机到Watch的单向同步,但另一种方式也是相同的。

iPhone应用程序 中:
1)准备需要同步的设置字典

- (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) 确定何时需要将设置推送到手表
(此处未显示)

3) 将设置推送到手表

- (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];  
     }  
}  
手表扩展中:
4)接收用户信息传输
- (void)session:(WCSession *)session didReceiveUserInfo:(NSDictionary<NSString *, id> *)userInfo  
{  
    NSDictionary *settings = [userInfo objectForKey:@"settings"];  
    if (settings != nil) {  
        // Import the settings  
        [self importSettingsFromCompanionApp:settings];  
     }  
} 

5) 将接收到的设置保存到手表用户默认设置中

- (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];  
}  

1
有没有发布 Swift 版本的机会? - MwcsMac
不好意思,但是你自己移植应该不难。 - Fabian Kreiser
请查看以下链接获取Swift连接代码:https://developer.apple.com/videos/play/wwdc2015-713/ - Mona
我看到了 [self userdefaults],我能在这里使用 [NSUserDefaults standardUserDefaults] 吗? - GeneCode
是的。在我的情况下,我正在使用组用户默认设置,这就是为什么有[self userDefaults]的原因。 - Fabian Kreiser

12

有一种简单的方法可以重现旧功能,我将旧组用户默认设置导出为字典,通过WatchConnectivity框架将其发送到另一侧,然后重新导入到用户默认设置中:

在手机和手表应用程序中都需要进行以下操作:

  1. 添加WatchConnectivty框架
  2. #import <WatchConnectivity/WatchConnectivity.h>并声明为WCSessionDelegate
  3. 添加代码以在应用程序启动后启动会话:

    if ([WCSession isSupported]) {
            WCSession* session = [WCSession defaultSession];
            session.delegate = self;
            [session activateSession];
        }
    
  4. 使用以下代码将更新后的默认设置发送到其他设备(在当前[defaults synchronize]之后调用):

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

  1. 接收并将设置保存回默认设置 - 将以下内容添加到WCDelegate中:

  2. -(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];
    }
    

    请注意维护对非 WC 设备的支持 - 在你的 updateApplicationContext 调用周围加上 if ([WCSession isSupported])


如果在手表应用程序未运行时更改iPhone应用程序的设置(isReachable将为false),会发生什么?手表应用程序将无法侦听会话。稍后,当手表应用程序启动时,它将具有旧值。手表应用程序需要唤醒iPhone应用程序,然后安排一些魔法发生。 - Jeff
1
优秀的回答。请注意,applicationContext 包括许多与 Apple 相关的键。为了使其更安全,我建议仅添加您期望的键名的已验证子集(如 NSArray)的数据。 - gpdawson
最好且简明扼要的答案。 - Dmitry

7

如前所述,共享的NSUserDefaults在WatchOS2上不再起作用。

这是@RichAble答案的Swift版本,并附有更多注释。

在您的iPhone应用程序中,请按照以下步骤操作:

选择要从中向Apple Watch推送数据的视图控制器,并在顶部添加框架。

import WatchConnectivity

现在,与手表建立WatchConnectivity会话并发送一些数据。
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)
        }
    }
}

请注意,如果您跳过设置委托,这将无法正常工作,因此即使您从未使用它,也必须设置委托并添加此扩展:
extension MyViewController: WCSessionDelegate {

}

现在,在您的手表应用程序中(这段代码对Glances和其他watch kit应用程序类型同样有效),您需要添加框架:

import WatchConnectivity

然后您设置连接会话:
override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)
    let watchSession = WCSession.defaultSession()
    watchSession.delegate = self
    watchSession.activateSession()
}

你只需要监听并处理来自iOS应用程序的消息:

extension InterfaceController: WCSessionDelegate {

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

}

那就是所有的内容了。
需要注意的事项:
  1. 您可以随时发送新的应用程序上下文,而不必考虑手表是否附近且连接或手表应用程序是否正在运行。这以智能方式在后台传递数据,并且数据在启动手表应用程序时等待。
  2. 如果您的手表应用程序实际上是活动的并在运行,则大多数情况下应立即接收到消息。
  3. 您可以反转此代码,使手表以同样的方式向iPhone应用程序发送消息。
  4. 当查看手表应用程序时,手表应用程序接收到的applicationContext将仅是您发送的最后一条消息。如果在查看手表应用程序之前发送了20条消息,它将忽略前19条并处理第20条。
  5. 要进行两个应用程序之间的直接/硬连接、后台文件传输或排队消息,请查看WWDC视频

1
这是一个很好的例子,我已经在新的watchOS 3上使用swift 3等使它工作。只是一个快速提示,你必须更改你发送的值 - 不仅仅是“bar”,否则你的消息只会被发送一次。这让我花了一个小时才弄明白 :( - Jens Peter

1

我花了很多时间才学会这个。看这个非常有用的视频!它向你介绍如何使用WatchConnectivity在iPhone应用程序和手表之间共享NSUserDefault的基本思路!

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接