Add iCloud syncing
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.
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.
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. 😄
Hey @StevenMagdy, the implementation (and the API!) looks really clean, thank you! Can you please:
- add checks on watchOS (since the
NSUbiquitousKeyValueStoreis not available on watchOS) - add some tests for syncing?
I'll try to play with the sync as soon as possible as well!
@sunshinejr Thanks for the reply
- add checks on watchOS (since the
NSUbiquitousKeyValueStoreis 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? 🤔
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.
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! 🙇
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"])`