GithubHelp home page GithubHelp logo

artsabintsev / zephyr Goto Github PK

View Code? Open in Web Editor NEW
917.0 20.0 60.0 11.44 MB

Effortlessly synchronize UserDefaults over iCloud.

Home Page: http://www.sabintsev.com/

License: MIT License

Ruby 3.51% Swift 96.49%
swift nsuserdefaults icloud sync nsubiquitouskeyvaluestore icloud-sync cocoapods carthage swift-package-manager userdefaults

zephyr's People

Contributors

aedificator-nl avatar albertwujj avatar artsabintsev avatar davehakim avatar dependabot[bot] avatar funkenstrahlen avatar krzyzanowskim avatar marcheimendinger avatar rhx avatar shaundon avatar sigito avatar steipete avatar tunous avatar twostraws avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

zephyr's Issues

sync with value nil

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?

App overwriting default values to iCloud on fresh installation

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.

Use of unresolved identifier 'DispatchQueueAttributes'

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+.

[idea] allow callback on update from iCloud

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 */ }

Background sync not triggered

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.

A couple of qustions

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!

Sync is over-writting previously saved Data in iCloud instead of fetching it.

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 :)

Publishing changes from background threads is not allowed (SwiftUI)

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?

using Zephyr with a suiteName

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?

Trouble Syncing

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

Remove data from iCloud

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

zephyrRemoteStoreDictionary[ZephyrSyncKey] reports incorrect time

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! 🍻

override new value on UserDefaults

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:

  • When I launch the app, userDefaults registers "myKey" == nil
  • Zephyr.addKeysToBeMonitored
  • during onboarding I change the value of UserDefaults "myKey" to true
  • right after I call Zephyr.sync(keys:"myKey")

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

Let user chose which way to synch the first time?

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.

Issue when changing multiple keys in macOS Monterey

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:

  • Receives NSUbiquitousKeyValueStore.didChangeExternallyNotification notification for key 1, stores remote value to local user defaults.
  • Zeyphr sets local sync date
  • Receives NSUbiquitousKeyValueStore.didChangeExternallyNotification notification for key 2, throws it away, because local sync date is newer
  • Receives NSUbiquitousKeyValueStore.didChangeExternallyNotification notification for key ZephyrSyncKey

Zephyr/Sources/Zephyr.swift

Lines 456 to 459 in d86fe8c

if let localStoredDate = zephyrLocalStoreDictionary[ZephyrSyncKey] as? Date,
!(remoteStoredDate.timeIntervalSince1970 > localStoredDate.timeIntervalSince1970) {
return
}

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.

Need help!

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.

Explanation of Register API as it relates to 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.

  • What does it mean to "register" UserDefaults, or to "add the contents of [a] dictionary to the registration domain"? I've only ever saved to or read from UserDefaults.
  • What are the potential negative side effects if I don't use the Register API before calling Zephyr?
  • How should I call it when the app launches?
    • For example, would UserDefaults.standard.register(defaults: ["foo": "bar"]) be sufficient?
      • What does this do, exactly?

follow up override issue

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

Issue with initial load from iCloud

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.

Observer does not seem to be working

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.

Best Implementation?

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.

Potential data race crash

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)

Stack trace from thread 1
  * 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
Stack trace from thread 3

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

Carthage Error: Dependency "Zephyr" has no shared framework schemes

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

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.