Skip to content

Commit 3554f6d

Browse files
committed
Add iCloud syncing
1 parent 1930a81 commit 3554f6d

File tree

5 files changed

+174
-1
lines changed

5 files changed

+174
-1
lines changed

Sources/Defaults+Syncing.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// SwiftyUserDefaults
3+
//
4+
// Copyright (c) 2015-present Radosław Pietruszewski, Łukasz Mróz
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in all
14+
// copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
// SOFTWARE.
23+
//
24+
25+
#if !os(Linux)
26+
27+
public extension DefaultsAdapter {
28+
29+
@available(macOS 10.11, iOS 9.0, *)
30+
func syncKeys(_ keyPath: [(KeyStore) -> RawKeyRepresentable]) {
31+
let keys = Set(keyPath.map { $0(keyStore)._key })
32+
DefaultsSyncer.shared.syncedKeys = keys
33+
}
34+
}
35+
36+
#endif

Sources/Defaults.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import Foundation
3434

3535
public var Defaults = DefaultsAdapter<DefaultsKeys>(defaults: .standard, keyStore: .init())
3636

37+
// MARK: - UserDefaults
38+
3739
public extension UserDefaults {
3840

3941
/// Returns `true` if `key` exists
@@ -80,3 +82,30 @@ internal extension UserDefaults {
8082
}
8183
}
8284
}
85+
86+
// MARK: - NSUbiquitousKeyValueStore
87+
88+
extension NSUbiquitousKeyValueStore {
89+
90+
/// Returns `true` if `key` exists
91+
func hasKey<T>(_ key: DefaultsKey<T>) -> Bool {
92+
return object(forKey: key._key) != nil
93+
}
94+
95+
/// Removes value for `key`
96+
func remove<T>(_ key: DefaultsKey<T>) {
97+
removeObject(forKey: key._key)
98+
synchronize()
99+
}
100+
101+
/// Removes all keys and values from user defaults
102+
/// Use with caution!
103+
/// - Note: This method only removes keys on the receiver `NSUbiquitousKeyValueStore` object.
104+
/// System-defined keys will still be present afterwards.
105+
func removeAll() {
106+
for (key, _) in dictionaryRepresentation {
107+
removeObject(forKey: key)
108+
}
109+
synchronize()
110+
}
111+
}

Sources/DefaultsKey.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,15 @@
2424

2525
import Foundation
2626

27+
public protocol RawKeyRepresentable {
28+
var _key: String { get }
29+
}
30+
2731
// MARK: - Static keys
2832

2933
/// Specialize with value type
3034
/// and pass key name to the initializer to create a key.
31-
public struct DefaultsKey<ValueType: DefaultsSerializable> {
35+
public struct DefaultsKey<ValueType: DefaultsSerializable>: RawKeyRepresentable {
3236

3337
public let _key: String
3438
public let defaultValue: ValueType.T?

Sources/DefaultsSyncer.swift

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//
2+
// SwiftyUserDefaults
3+
//
4+
// Copyright (c) 2015-present Radosław Pietruszewski, Łukasz Mróz
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in all
14+
// copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
// SOFTWARE.
23+
//
24+
#if !os(Linux)
25+
26+
import Foundation
27+
28+
// The OS requirment just to ignore dealing with unregistering notifications
29+
@available(macOS 10.11, iOS 9.0, *)
30+
internal class DefaultsSyncer {
31+
32+
var syncedKeys = Set<String>() {
33+
didSet { syncFromICloud() }
34+
}
35+
36+
let defaults: UserDefaults
37+
38+
private init(defaults: UserDefaults) {
39+
self.defaults = defaults
40+
syncFromICloud()
41+
NotificationCenter.default.addSafeObserver(self,
42+
selector: #selector(iCloudDefaultsDidUpdate),
43+
name: NSUbiquitousKeyValueStore.didChangeExternallyNotification)
44+
NotificationCenter.default.addSafeObserver(self,
45+
selector: #selector(localDefaultsDidUpdate),
46+
name: UserDefaults.didChangeNotification)
47+
}
48+
49+
@objc
50+
private func iCloudDefaultsDidUpdate() {
51+
// Housekeeping
52+
NotificationCenter.default.removeObserver(self, name: UserDefaults.didChangeNotification, object: nil)
53+
54+
syncFromICloud()
55+
56+
NotificationCenter.default.addSafeObserver(self,
57+
selector: #selector(localDefaultsDidUpdate),
58+
name: UserDefaults.didChangeNotification)
59+
}
60+
61+
private func syncFromICloud() {
62+
let allICloudKeys = Set(NSUbiquitousKeyValueStore.default.dictionaryRepresentation.keys)
63+
let updatedSyncedKeys = allICloudKeys.filter { syncedKeys.contains($0) }
64+
updatedSyncedKeys.forEach { key in
65+
let iCloudValue = NSUbiquitousKeyValueStore.default.object(forKey: key)
66+
defaults.set(iCloudValue, forKey: key)
67+
}
68+
}
69+
70+
@objc
71+
private func localDefaultsDidUpdate() {
72+
syncedKeys.forEach { key in
73+
let localValue = defaults.object(forKey: key)
74+
NSUbiquitousKeyValueStore.default.set(localValue, forKey: key)
75+
}
76+
// request upload to ICloud
77+
NSUbiquitousKeyValueStore.default.synchronize()
78+
}
79+
80+
}
81+
82+
@available(macOS 10.11, iOS 9.0, *)
83+
internal extension DefaultsSyncer {
84+
static let shared = DefaultsSyncer(defaults: Defaults.defaults)
85+
}
86+
87+
private extension NotificationCenter {
88+
func addSafeObserver(_ observer: Any, selector aSelector: Selector, name aName: NSNotification.Name, object anObject: Any? = nil) {
89+
removeObserver(observer, name: aName, object: anObject)
90+
addObserver(observer, selector: aSelector, name: aName, object: anObject)
91+
}
92+
}
93+
94+
#endif

SwiftyUserDefaults.xcodeproj/project.pbxproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
9FA427F1D84703CE8EC6CD38 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE155E06F856DFEF78FD63F7 /* Defaults.swift */; };
2929
A3CAE6734DDE8FD952CE2CF2 /* Defaults+Subscripts.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF92C9F82ABF7807D4D4DDDD /* Defaults+Subscripts.swift */; };
3030
ACE14E08E7B3EECD55B932B3 /* Defaults+Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8980553F1615D4ACB4AA43 /* Defaults+Dictionary.swift */; };
31+
B01ABE9223E4798200E5E003 /* DefaultsSyncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B01ABE9123E4798200E5E003 /* DefaultsSyncer.swift */; };
32+
B056341D23E3C5DD006D08EE /* Defaults+Syncing.swift in Sources */ = {isa = PBXBuildFile; fileRef = B056341C23E3C5DD006D08EE /* Defaults+Syncing.swift */; };
3133
B31C4E974609611EF7360E51 /* Defaults+Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8790CE5B53F91DAD51975EDC /* Defaults+Double.swift */; };
3234
C1EF5E679553F49B4C163561 /* SwiftyUserDefaults.h in Headers */ = {isa = PBXBuildFile; fileRef = B134969522555B348C06FD6C /* SwiftyUserDefaults.h */; settings = {ATTRIBUTES = (Public, ); }; };
3335
CC4CF6A63F6AA1B932D6757E /* DefaultsBridges.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20EBA39F1E25782FDC1CFC36 /* DefaultsBridges.swift */; };
@@ -69,6 +71,8 @@
6971
9701991125BC1345631E831E /* DefaultsObserver.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DefaultsObserver.swift; path = Sources/DefaultsObserver.swift; sourceTree = "<group>"; };
7072
A574FF49CBCEBCCEAE9BD42E /* Defaults+String.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Defaults+String.swift"; path = "Tests/SwiftyUserDefaultsTests/Built-ins/Defaults+String.swift"; sourceTree = "<group>"; };
7173
A5A5BEEC51B2A01157F9965F /* Defaults+BestFroggiesEnum.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Defaults+BestFroggiesEnum.swift"; path = "Tests/SwiftyUserDefaultsTests/External types/Defaults+BestFroggiesEnum.swift"; sourceTree = "<group>"; };
74+
B01ABE9123E4798200E5E003 /* DefaultsSyncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DefaultsSyncer.swift; path = Sources/DefaultsSyncer.swift; sourceTree = "<group>"; };
75+
B056341C23E3C5DD006D08EE /* Defaults+Syncing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Defaults+Syncing.swift"; path = "Sources/Defaults+Syncing.swift"; sourceTree = "<group>"; };
7276
B134969522555B348C06FD6C /* SwiftyUserDefaults.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SwiftyUserDefaults.h; path = Sources/SwiftyUserDefaults.h; sourceTree = "<group>"; };
7377
BF92C9F82ABF7807D4D4DDDD /* Defaults+Subscripts.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Defaults+Subscripts.swift"; path = "Sources/Defaults+Subscripts.swift"; sourceTree = "<group>"; };
7478
CB55DC75BF69CEA7DB03750E /* BuiltIns.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BuiltIns.swift; path = Sources/BuiltIns.swift; sourceTree = "<group>"; };
@@ -131,6 +135,8 @@
131135
85096B65E07A3BC7F856F0D1 /* OptionalType.swift */,
132136
858E15CF231FEC2F00DC1418 /* PropertyWrappers.swift */,
133137
B134969522555B348C06FD6C /* SwiftyUserDefaults.h */,
138+
B056341C23E3C5DD006D08EE /* Defaults+Syncing.swift */,
139+
B01ABE9123E4798200E5E003 /* DefaultsSyncer.swift */,
134140
);
135141
name = Sources;
136142
sourceTree = "<group>";
@@ -165,7 +171,9 @@
165171
1A0E1BA11455E9E44F192EAA /* Sources */,
166172
C14BA19AA073B0F4B6ED9A3D /* Tests */,
167173
);
174+
indentWidth = 4;
168175
sourceTree = "<group>";
176+
tabWidth = 4;
169177
};
170178
5BF9A4C7B374362259AAD1B0 /* Built-ins */ = {
171179
isa = PBXGroup;
@@ -357,8 +365,10 @@
357365
buildActionMask = 2147483647;
358366
files = (
359367
858E15D0231FEC2F00DC1418 /* PropertyWrappers.swift in Sources */,
368+
B01ABE9223E4798200E5E003 /* DefaultsSyncer.swift in Sources */,
360369
D102C1011A4314AFF773DCD5 /* Defaults+StringToBool.swift in Sources */,
361370
037280C1C6C68BDB8E3759E6 /* DefaultsKey.swift in Sources */,
371+
B056341D23E3C5DD006D08EE /* Defaults+Syncing.swift in Sources */,
362372
CC4CF6A63F6AA1B932D6757E /* DefaultsBridges.swift in Sources */,
363373
ED39909122B0204E0046F502 /* DefaultsKeys.swift in Sources */,
364374
A3CAE6734DDE8FD952CE2CF2 /* Defaults+Subscripts.swift in Sources */,

0 commit comments

Comments
 (0)