artsabintsev / zephyr Goto Github PK
View Code? Open in Web Editor NEWEffortlessly synchronize UserDefaults over iCloud.
Home Page: http://www.sabintsev.com/
License: MIT License
Effortlessly synchronize UserDefaults over iCloud.
Home Page: http://www.sabintsev.com/
License: MIT License
Hi,
it is happening to me that when saving a Date in UserDefaults
and then sync
I get:
Synchronized key '....' with value 'nil'
I am checking and the value is properly set, but after syncing is nil
:(
Any idea why this is happening?
Hi. I'm making a SwiftUI app and this is what I have:
UserDefaults.standard.register(defaults: [
"numbers": [1, 2, 3]
])
Zephyr.debugEnabled = true
Zephyr.sync(keys: "numbers")
sync() is called every time a new number is added, and it syncs across my devices without a problem.
However, if I delete the app and reinstall or install it on a new device, it would overwrite the default value to iCloud.
Debug information on fresh installation:
[Zephyr] Started synchronization TO iCloud.
[Zephyr] Unsubscribed 'numbers' from observation.
[Zephyr] Synchronized key 'numbers' with value '(
1,
2,
3
)' TO iCloud.
[Zephyr] Subscribed 'numbers' for observation.
[Zephyr] Finished synchronization TO iCloud.
Can you tell me what I am missing here? Thank you in advance.
Hey Arthur!
Just loaded this up in Xcode 8 beta 4 and I'm getting this error on line 75.
Use of unresolved identifier 'DispatchQueueAttributes'
private let zephyrQueue = DispatchQueue(label: "com.zephyr.queue", attributes: DispatchQueueAttributes.serial);
It looks like this initializer is only available in iOS 10+.
So if I load my prefs at the start .. a short while later they will be loaded from iCloud and with Zephyr, like magic, they're ready for me in userdefaults. But, I've already loaded my prefs. Sure, I could go check again, but it would be awesome if I received a notification for them or something. Actually, I think the most elegant way is with a callback, then I could just write:
Zephyr.sync() { /* (error:Error?) in */ }
I'm currently trying to get this set up to allow me to sync an array between devices.
In my AppDelegate I add
Zephyr.debugEnabled = true
Zephyr.addKeysToBeMonitored(keys: "defaultToSync")
Zephyr.sync(keys: "defaultToSync")
Which when launching the second device will pull in what value has been set.
I then change the value using:
UserDefaults().set(value, forKey: "defaultToSync")
Zephyr.sync(keys: "defaultToSync")
My second device never seems to pick up that things have changed, the device making the change does say it has synced TO iCloud.
My understanding is that the key being monitored should at some point see the change?
Any help is appreciated.
Hi, first I wanted to thank you for writing this library. It really makes the usage of NSUbiquitousKeyValueStore
effortless.
Now, I was looking into the Zephyr code and I have a couple of questions that aren't necessarily issues, but they might be.
In the syncToCloud(key:value:)
local storage observer is unregistered before the value is stored to the ubiquitous storage and synchronized. Is it possible that at that same time another thread updates the value in the local storage with the same key and that update doesn't get synced with the ubiquitous storage because the observer is unregistered at that point? Why is local storage observer unregistered during syncing to the iCloud anyway?
Also, it seems like different reasons for the occurrence of NSUbiquitousKeyValueStoreDidChangeExternallyNotification
aren't handled by Zephyr. Do you plan to add that anytime soon?
Thanks in advance for your response!
Device1, saved a bunch of data in iCloud.
Device2, just installed the app, saved some default data locally during initialization, and then called Sync, expecting to download whatever was in iCloud and override the default data saved a moment before.
What happened: Device1 now has the data of Device2 and I lost all data tha Device1 had previously uploaded to iCloud. I understand you are syncing with the Latest data, in this case Device2 pushed the latest data. But then how can I fetch what is in iCloud before saving defaults locally (specially when no internet) and not deleting everyhing in the cloud when calling sync.
is Sync() a synchronous process? can I know if it failed?
Any suggestion is appreciated.
Thank you :)
I'm getting a runtime issue while receiving changes from iCloud in a SwiftUI app which uses @AppStorage
to access UserDefaults.
[SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
It's pointing to this line of code, which I believe should run on the main thread to avoid this issue?
As far as I understand this change in iOS 13 UIApplication.willEnterForegroundNotification
is no longer posted when using the new scene delegates as described here:
Therefore listening to scene lifecycle notifications might be required.
What do you think?
I am just getting started with Zephyr, I want to use it sync my UserDefaults between iPad and iOS version of my app. I have set it up, and it doesn't seem to find the values across devices. Both devices are logged into the same iCloud account, and in debug mode I see that Zephyr is syncing - but each devices seems to find a unique instance. (this is in the simulator on both devices). Is this addressed via a UserDefaults SuiteName?
The syncing works great, but I'd like to give the user power of whether they want to sync. Any way to do that using Zephyr? Thx!
NSUbiquitousKeyValueStore is now also supported in watchOS 9. Currently Zephyr Swift package is not available on watchOS. Please add watchOS 9 as a target platform in the Package.swift.
Hi, and thanks for your control. I must be doing something wrong.
I am using v.3.0.1.
I am testing on 2 devices (iPhone 6 and an iPad). Both are running iOS11.2
In my didFinishLaunchingWithOptions
method, I'm calling
Zephyr.sync(keys: ["useAnimations"])
Zephyr.addKeysToBeMonitored(keys: "useAnimations")
The "useAnimations" tag is used throughout, and I copied and pasted it into the lines above.
Should I give the sync on each device some time to run?
What if the app is open on both devices at the same time?
I'm also using CloudKit. Is there any interaction that could cause a problem?
Many thanks
David
Hi!
Is it possible to remove a specific key from iCloud, or does the removeKeysFromBeingMonitoredWithKeys method remove also the keys?
Thank you very much,
Gianpiero
Initially, when I reported this issue, I thought it was due to a synchronization timing / data race issue, but I have since found otherwise.
It appears that the date stored in zephyrRemoteStoreDictionary[ZephyrSyncKey]
often supplies the incorrect time, thus causing data lost for the local store. Although I have yet to pinpoint the cause of the issue, this key fairly consistently (2 out of every 3 calls) reports a time that is ahead of the local time. Unfortunately, this means that when updating a default in the local store that is newer than the default stored remotely, the older remote value trumps the new local value.
I will continue looking into the issue and see if I can find out any additional information. Hopefully we can get this fixed!
Thank you for this project - saved me a good chunk of time, and it's beautifully written! 🍻
Good day,
I am still having the issue that when ever I try to update the key in UserDefaults it gets override by the value in iCloud.
my steps are:
the logs says:
[Zephyr] Started synchronization FROM iCloud
[Zephyr] Unsubscribed "myKey" from observation.
[Zephyr] Synchronized key "myKey" with value 'nil' FROM iCloud
[Zephyr] Subscribed "myKey" for observation.
[Zephyr] Finished synchronization FROM iCloud
and I always got override instead of updating the value in iCloud...
I am assuming I am doing the steps wrong, I would really appreciate some help here. Thank you very much in advance
UPDATE:
after some debugging I could see that when dataStoreWithLatestData()
looks for the latest Data, zephyrRemoteStoreDictionary[ZephyrSyncKey]
returns nil, so it is assuming that the valid value is the one in iCloud.
still don't see what am I doing wrong here
I started to implement iCloud sync into an existing app when I came across your library. As it would be implemented to add iCloud synch as a new feature, there is the scenario where a user has two (or more) devices with local settings each. The current logic would sync the local settings from the first device that starts the app to iCloud and then overwrite the second device's local settings without checking in if this is desired. For this case an option would be great that asks the user once which settings to use. Any thoughts on this? Thanks and thank you for this great piece of code.
First off: Thank you for this library. This is exactly what I was looking for.
There seems to be a sync issue with macOS Monterey (tested version: 12.0.1 (21A558))
It seems that Apple changed the behavior on how it posts the NSUbiquitousKeyValueStore.didChangeExternallyNotification
notification.
Formerly, notification.userInfo?[NSUbiquitousKeyValueStoreChangedKeysKey]
contained ALL changed keys (including ZephyrSyncKey
). In my tests, this is no longer the case. Instead, the notification gets posted multiple times, with different values. Here is what happens in my scenario:
We have Client A and Client B. Lets say A changes 2 keys. What happens is:
Zephyr:
NSUbiquitousKeyValueStore.didChangeExternallyNotification
notification for key 1, stores remote value to local user defaults.NSUbiquitousKeyValueStore.didChangeExternallyNotification
notification for key 2, throws it away, because local sync date is newerNSUbiquitousKeyValueStore.didChangeExternallyNotification
notification for key ZephyrSyncKey
Lines 456 to 459 in d86fe8c
In this scenario, the second value was thrown out and both apps are no longer in sync.
I can't tell if this is a (beta) bug or if this is the new, expected behavior. I can't think of a good way to fix this, as we would now need to store the sync date of each property - or somehow batch all those updates coming in.
Hello! It's a cool library, but I'm newbie and can’t understand, how to send data to the cloud, and how to get it from there? There is only a sync method in the GitHub documentation.
I'm want to sync NSUserDefaults from app group like:
NSUserDefaults(suiteName: "group.xxx")
But there's no way to do with Zephyr.
Hi @ArtSabintsev ! Hope you're well. Believe it or not, I'm finally getting around to trying Zephyr for the first time 😄 . I'm finding it really easy and straightforward to set up, just have one point of confusion:
From the README:
To make sure there's no overwriting going on in a fresh installation of your app on a new device that's connected to the same iCloud account, make sure that your UserDefaults are registered BEFORE calling any of the Zephyr methods. One way to easily achieve this is by using the UserDefaults Register API.
I don't really understand this and I was wondering if you could explain this a little more.
UserDefaults
, or to "add the contents of [a] dictionary to the registration domain"? I've only ever saved to or read from UserDefaults
.UserDefaults.standard.register(defaults: ["foo": "bar"])
be sufficient?
Sorry for my late response.
answering your question here: #45 (comment)
first in:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?)
I register defaults, where:
let emptyData = Data()
UserDefaults.standard.register(defaults: [subscription.identifier: emptyData])
Zephyr.addKeysToBeMonitored(keys: [subscription.identifier])
later after checking the status of the user subscription I store the subscription in UserDefaults to later sync:
let encoder = PropertyListEncoder()
let statusData = try! encoder.encode(status)
UserDefaults.standard.set(statusData, forKey: subscription.identifier)
Zephyr.sync(keys: [subscription.identifier])
And here is where the override issue happens. I managed to fix it in my phone by removing the key from iCloud with NSUbiquitousKeyValueStore.default.removeObject(forKey:)
But trying with a different phone happen again.
If you could give me some hint here I would really appreciated
On an initial install of an app with Zephyr the iCloud data is not immediately available. Once it is available is should be synced with the current UserDefaults
. This does not happen on an initial load however, as keysDidChangeOnCloud
will return early if localStoredDate
is nil (which it is as this is the initial load of the application).
The issue seems to be in keysDidChangeOnCloud:452
, have made a PR: #47.
The compiler complains that this notification key got renamed:
Line 73 in ced975f
Using Zephyr 3.4.1
hi, Arthur!
how do you think is it possible to implement this callback or delegate method being fired in the end of sync?
@ArtSabintsev
I am fairly familiar with using observers, but the function you have listed in the documentation does not seem to be working. I need to be able to immediately update the ui after I change a single string value in NSUserDefaults. I would prefer not sync all user defaults, but have been having a hell of time getting this to work consistently A detailed example would be spectacular if you may have one. I have gotten this to work, but I am sure there is a better way. I found that creating an observer for NSUbiquitousKeyValueStore.didChangeExternallyNotification seems to work as a workaround, but based on the data in the public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?), I would assume it would be much easier/better to use this, as I hate adding extra observers if I can avoid it, so I don't have to worry about cleanup later on. Im sure you can appreciate that as much as myself. In case you are wondering I am testing on two actual devices, not the simulator. Also, as a separate question, on app startup when I add Zephyr.addKeysToBeMonitored(keys: ["somekey"]), do you still need to use Zephyr.sync(keys: ["somekey"]), or can I just begin to write to that specific UserDefault like normal, and Zephyr will take care of syncing it? These are the type of questions a detailed example will help illustrate better. I know it may seem very straightforward to you, but with no examples it is really hard to interpret how you intended this to be used. Thank you and keep up the great work. I would love to keep using this framework as it seems dead simple once I have some more insight into how you intend it to be used.
Hi,
I'm implementing Zephyr in my app now and I'm curious on what you believe is the best implementation of the library. Currently, in AppDelegate
's didFinishLaunchingWithOptions()
, I run:
Zephyr.sync(keys: "presets", "selectedTheme")
Zephyr.addKeysToBeMonitored(keys: "presets", "selectedTheme")
However, some users seem to be experiencing a crash on launch and I've narrowed it down to these two lines of code. Am I using Zephyr wrong? If so, could you explain a better approach?
Thanks so much for the library, I've used it before the Swift 3 update and it's phenomenal.
I see iOS
and tvOS
as the supported platforms in the ReadMe and in the code, but is there a reason macOS
isn't listed as well?
During one of the launches of my app, I've got a random crash that appears to be a data race condition.
The error is:
Thread 3: "Cannot remove an observer <Zephyr.Zephyr 0x282c413c0> for the key path "myKey" from <NSUserDefaults 0x282c41400> because it is not registered as an observer."
From the looks of it, one thread attempted to unregister an observer and another tried to register one at the same time. The key for both of these calls was the same. I was unable to reproduce this issue.
Initialization logic happens inside of my app's SwiftUI.App init as follows:
let keyToSync = "myKey"
Zephyr.addKeysToBeMonitored(keys: keyToSync)
Zephyr.sync(keys: keyToSync)
* frame #0: 0x00000001b7816504 libsystem_kernel.dylib`mach_msg_trap + 8
frame #1: 0x00000001b7816b9c libsystem_kernel.dylib`mach_msg + 76
frame #2: 0x0000000103e66c68 libdispatch.dylib`_dispatch_mach_send_and_wait_for_reply + 520
frame #3: 0x0000000103e67018 libdispatch.dylib`dispatch_mach_send_with_result_and_wait_for_reply + 56
frame #4: 0x00000001f0c9eb9c libxpc.dylib`xpc_connection_send_message_with_reply_sync + 240
frame #5: 0x0000000180a04c20 CoreFoundation`__104-[CFPrefsSearchListSource synchronouslySendDaemonMessage:andAgentMessage:andDirectMessage:replyHandler:]_block_invoke_2 + 40
frame #6: 0x00000001809d16f8 CoreFoundation`-[_CFXPreferences withConnectionForRole:performBlock:] + 52
frame #7: 0x00000001809f441c CoreFoundation`__104-[CFPrefsSearchListSource synchronouslySendDaemonMessage:andAgentMessage:andDirectMessage:replyHandler:]_block_invoke + 140
frame #8: 0x0000000180a0ccb0 CoreFoundation`CFPREFERENCES_IS_WAITING_FOR_SYSTEM_CFPREFSD + 100
frame #9: 0x0000000180a2b4ec CoreFoundation`-[CFPrefsSearchListSource synchronouslySendDaemonMessage:andAgentMessage:andDirectMessage:replyHandler:] + 332
frame #10: 0x0000000180a520ac CoreFoundation`-[CFPrefsSearchListSource alreadylocked_setObservingContents:] + 512
frame #11: 0x0000000180a4ce6c CoreFoundation`-[CFPrefsSearchListSource addPreferencesObserver:] + 164
frame #12: 0x0000000180993308 CoreFoundation`__108-[_CFXPreferences(SearchListAdditions) withSearchListForIdentifier:container:cloudConfigurationURL:perform:]_block_invoke + 404
frame #13: 0x000000018096b328 CoreFoundation`normalizeQuintuplet + 356
frame #14: 0x00000001809643ac CoreFoundation`-[_CFXPreferences withSearchListForIdentifier:container:cloudConfigurationURL:perform:] + 152
frame #15: 0x0000000180a500e8 CoreFoundation`-[_CFXPreferences registerUserDefaultsInstance:configurationURL:] + 184
frame #16: 0x0000000180a451f8 CoreFoundation`_CFPrefsRegisterUserDefaultsInstanceWithCloudConfigurationURL + 108
frame #17: 0x0000000182191994 Foundation`_NSKeyValueReplaceObservationInfoForObject + 116
frame #18: 0x000000018218d368 Foundation`-[NSObject(NSKeyValueObserverRegistration) _addObserver:forProperty:options:context:] + 384
frame #19: 0x0000000182189600 Foundation`-[NSObject(NSKeyValueObserverRegistration) addObserver:forKeyPath:options:context:] + 144
frame #20: 0x0000000102f9f1c4 MyApp`Zephyr.registerObserver(key=“myKey”, self=0x0000000282c413c0) at Zephyr.swift:392:26
frame #21: 0x0000000102f9e22c MyApp`Zephyr.syncFromCloud(key=“myKey”, value=(payload_data_0 = 0x000000028177e180, payload_data_1 = 0x000000016d092f40, payload_data_2 = 0x000000016d092f30, metadata = AnyObject), self=0x0000000282c413c0) at Zephyr.swift:372:9
frame #22: 0x0000000102f9e6a0 MyApp`Zephyr.syncSpecificKeys(keys=1 value, dataStore=remote, self=0x0000000282c413c0) at Zephyr.swift:289:17
frame #23: 0x0000000102f9e718 MyApp`closure #2 in static Zephyr.sync(self=Zephyr.Zephyr, keys=1 value) at Zephyr.swift:130:24
frame #24: 0x0000000102f9b9ec MyApp`thunk for @callee_guaranteed () -> () at <compiler-generated>:0
frame #25: 0x0000000102f9ba44 MyApp`thunk for @escaping @callee_guaranteed () -> () at <compiler-generated>:0
frame #26: 0x0000000103e4a3b4 libdispatch.dylib`_dispatch_client_callout + 20
frame #27: 0x0000000103e5b8e4 libdispatch.dylib`_dispatch_lane_barrier_sync_invoke_and_complete + 176
frame #28: 0x0000000102f9c64c MyApp`static Zephyr.sync(keys=1 value, self=Zephyr.Zephyr) at Zephyr.swift:129:32
frame #29: 0x0000000102f9bc88 MyApp`static Zephyr.sync(keys=1 value, self=Zephyr.Zephyr) at Zephyr.swift:92:13
frame #30: 0x0000000102e4be48 MyApp`MyAppApp.configureICloudSync(self=MyApp.MyAppApp @ 0x000000016d093628) at MyAppApp.swift:53:16
frame #31: 0x0000000102e4bb00 MyApp`MyAppApp.init() at MyAppApp.swift:30:9
frame #32: 0x0000000102e4da0c MyApp`protocol witness for App.init() in conformance MyAppApp at <compiler-generated>:0
frame #33: 0x00000001884cdca4 SwiftUI`static SwiftUI.App.main() -> () + 112
frame #34: 0x0000000102e4d940 MyApp`static MyAppApp.$main(self=MyApp.MyAppApp) at MyAppApp.swift:24:1
frame #35: 0x0000000102e4da38 MyApp`main at MyAppApp.swift:0
frame #36: 0x0000000103d1da24 dyld`start + 520
Simplified as I didn't find a way to copy it from Xcode (reported as Recorded stack frame):
#5 0x0000000102f9fce0 in Zephyr.unregisterObserver(key:) at Zephyr.swift:411
#6 0x0000000102f9dfbc in Zephyr.syncFromCloud(key:value:) at Zephyr.swift:360
#7 0x0000000102f9e6a0 in Zephyr.syncSpecificKeys(keys:dataStore:) at Zephyr.swift:289
#8 0x0000000102fa1eb0 in Zephyr.keysDidChangeOnCloud(notification:) at Zephyr.swift:462
Hey Arthur,
Getting this error which I didn't get on 2.0.0 trying to update Zephyr via Carthage.
*** Checking out Zephyr at "2.1.0"
*** xcodebuild output can be found in /var/folders/kr/y2y78_m56yqckp9bb2wj0l0h0000gn/T/carthage-xcodebuild.OgXngT.log
*** Skipped building Zephyr due to the error:
Dependency "Zephyr" has no shared framework schemes
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.