肇鑫的技术博客

业精于勤,荒于嬉

不使用第三方插件情况下,iCloud同步键值的方法

我们已经很习惯使用第三方的库来调用UserDefaults了。但是有时我们也需要只使用苹果官方的框架来实现相同的功能。

  1. 创建Key
  2. 注册iCloud的变化
  3. 更新userdefaults
  4. 注册Key的变化
  5. 更新iCloud

创建Key

extension UserDefaults {
  static let text = "text"
}

注册iCloud的变化

class AppDelegate: NSObject, NSApplicationDelegate {
  @AppStorage(UserDefaults.text) private var text: String = "Hello, Zhao!"

  func applicationDidFinishLaunching(_ notification: Notification) {
    registerKeyValueSyncing()
  }
}

extension AppDelegate {
  private func registerKeyValueSyncing() {
    NotificationCenter.default.addObserver(forName: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: NSUbiquitousKeyValueStore.default, queue: nil) { [self] notification in
      guard let userInfo = notification.userInfo else { return }
      guard let reasonForChange = userInfo[NSUbiquitousKeyValueStoreChangeReasonKey] as? Int else { return }
      guard let keys = userInfo[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String] else { return }
      guard keys.contains(UserDefaults.text) else { return }

      if reasonForChange == NSUbiquitousKeyValueStoreAccountChange {
        text = "Hello, Zhao!"
        return
      }

      if let newText = NSUbiquitousKeyValueStore.default.string(forKey: UserDefaults.text),
         newText != text {
        text = newText
        print("update text to \(text)")
      }
    }

    UserDefaults.standard.addObserver(self, forKeyPath: UserDefaults.text, options: [.new, .initial], context: nil)
  }

  override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    guard let keyPath, keyPath == UserDefaults.text else { return }

    if let change {
      print("[debug] change is \(change)")

      if let newText = change[.newKey] as? String {
        NSUbiquitousKeyValueStore.default.set(newText, forKey: keyPath)
      }
    }
  }
}

主程序

@main
struct KeyValueSyncingApp: App {
  #if os(macOS)
  @NSApplicationDelegateAdaptor private var appDelegate: AppDelegate
  #else
  @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
  #endif

  var body: some Scene {
    WindowGroup {
      ContentView()
    }
  }
}