sunshinejr / SwiftyUserDefaults

Modern Swift API for NSUserDefaults
http://radex.io/swift/nsuserdefaults/static
MIT License
4.85k stars 365 forks source link

Add iCloud syncing #229

Open StevenSorial opened 4 years ago

StevenSorial commented 4 years ago

Adds #228

Usage:

// Prefs.swift
extension DefaultsKeys {
  var username: DefaultsKey<String?> { return .init("username") }
  var launchCount: DefaultsKey<Int> { return .init("launchCount", defaultValue: 0) }
}

// AppDelegate.swift

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  Defaults.syncKeys([
    { $0.launchCount },
    { $0.username }
  ])
  return true
}

// ViewController.swift

@IBAction
func stopSyncingUsername(_ sender: UIButton) {
  Defaults.syncKeys([
    { $0.launchCount },
  ])
}

Obviously the worst part of this implementation is syncKeys syntax and that it takes an array of closures. I tried to make it accept an array of KeyPath or even PartialKeyPath but failed so this was the best alternative. The other approach that I could have taken is for it to be without parameters and use Mirror internally:

func syncKeys() {
  let keys = Mirror(reflecting: keyStore).children.compactMap { $0 as? RawKeyRepresentable }.filter { $0.isSynced }.map { $0._key }
  DefaultsSyncer.shared.syncedKeys = Set(keys)
}

but i don't like using Mirror because i feel it's very hacky

The whole implementation is not perfect but it's acceptable given the current structure.

@sunshinejr I'm willing to take another run at it if it's not acceptable for you.

Edit: the Mirror implementation does not even work because it ignores computed properties but I'm sure we can runtime-hack it to get the keys list. still not preferred.

StevenSorial commented 4 years ago

Changed it so each DefaultsAdapter has its own syncer.

Edit: This change supports use cases where there are multiple DefaultsAdapters in the same app. This has one caveat where if there is a DefaultsKey with the same key in two or more DefaultsAdapters they would overwrite each other in iCloud. this is a very rare behavior and should be clarified in the Readme/Docs.

StevenSorial commented 4 years ago

Got PartialKeyPath working 🎉🎉 It was a Swift type inference bug. SR-5667 - forums

Usage:

// Prefs.swift
extension DefaultsKeys {
  var username: DefaultsKey<String?> { return .init("username") }
  var launchCount: DefaultsKey<Int> { return .init("launchCount", defaultValue: 0) }
}

// AppDelegate.swift

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  Defaults.startSyncing(for: \DefaultsKeys.launchCount, \DefaultsKeys.username)
  return true
}

// ViewController.swift

@IBAction
func stopSyncingUsername(_ sender: UIButton) {
  Defaults.stopSyncing(for: \DefaultsKeys.username)
}

@IBAction
func stopSyncingCompletely(_ sender: UIButton) {
  Defaults.stopSyncingAll()
}

@sunshinejr I think I'm now satisfied with this implementation. 😄

sunshinejr commented 4 years ago

Hey @StevenMagdy, the implementation (and the API!) looks really clean, thank you! Can you please:

I'll try to play with the sync as soon as possible as well!

StevenSorial commented 4 years ago

@sunshinejr Thanks for the reply

  • add checks on watchOS (since the NSUbiquitousKeyValueStore is not available on watchOS)

Done.

  • add some tests for syncing?

I'm having a hard time imagining how to test syncing. I'm very inexperienced in testing, let alone sync testing. I would appreciate any help.

Unrelated question: what is UserDefaults doing on Linux? 🤔

StevenSorial commented 4 years ago

hey @sunshinejr, I think I'm done with this PR. I tried different approaches for testing with no luck. I hope this feature will get implemented one day 🙏, either building on this PR or from scratch.

sunshinejr commented 4 years ago

hey @StevenMagdy, sorry for the delay but I'm currently really busy and have a hard time to find few minutes to play with this - I might try early next week but cannot promise anything

don't worry, though, I really love the API and so we'll try to get this one in as soon as possible! and thank you for all the hard work! 🙇

rromanchuk commented 3 years ago

Although i'm going to keep my NSUbiquitousKeyValueStore usage with https://github.com/ArtSabintsev/Zephyr this PR gave me some clues. All i'm trying to achieve is getting an array of all keys defined in DefaultsKeys, instead of manually duplicating and hardcoding string key names.

I'd like to just be able to pass Defaults.keys that returns [String]. I want to be a lazy developer and not have to specify specific keys, as i can't think of a context where i wouldn't want these to sync. It also defends against developer error when a key is added or removed, i don't want to babysit AppDelegate's sync list.

Zephyr.addKeysToBeMonitored(keys: ["i-dont-want-to-manage-these, "keys"] ) Zephyr.sync(keys: ["i-dont-want-to-manage-these, "keys"])`