diff --git a/Sources/Defaults/Defaults+iCloud.swift b/Sources/Defaults/Defaults+iCloud.swift index 6efe552..0540f81 100644 --- a/Sources/Defaults/Defaults+iCloud.swift +++ b/Sources/Defaults/Defaults+iCloud.swift @@ -39,7 +39,7 @@ extension Defaults { ## Dynamically Toggle Syncing - You can also toggle the syncing behavior dynamically using the ``Defaults/iCloud/add(_:)-5gffb`` and ``Defaults/iCloud/remove(_:)-1b8w5`` methods. + You can also toggle the syncing behavior dynamically using the ``Defaults/iCloud/add(_:)`` and ``Defaults/iCloud/remove(_:)-1b8w5`` methods. ```swift import Defaults @@ -82,15 +82,10 @@ extension Defaults { /** Add the keys to be automatically synced. */ - public static func add(_ keys: Defaults.Keys...) { - synchronizer.add(keys) - } - - /** - Add the keys to be automatically synced. - */ - public static func add(_ keys: [Defaults.Keys]) { - synchronizer.add(keys) + // TODO: Support array of Defaults.Key after Swift 6 pack iteration is supported. + // https://github.com/sindresorhus/Defaults/pull/185#discussion_r1704464183 + public static func add(_ keys: repeat Defaults.Key) { + repeat synchronizer.add(each keys) } /** @@ -269,11 +264,23 @@ final class iCloudSynchronizer { /** Add new key and start to observe its changes. */ - func add(_ keys: [Defaults.Keys]) { - self.keys.formUnion(keys) - syncWithoutWaiting(keys) - for key in keys { - localKeysMonitor.add(key: key) + func add(_ key: Defaults.Key) { + let (isInserted, _) = self.keys.insert(key) + guard isInserted else { + return + } + + localKeysMonitor.add(key: key) + + // If the local value is the default value, only sync from remote, since all devices should already have the default value. + if key._isDefaultValue { + guard case .remote = latestDataSource(forKey: key) else { + return + } + + syncWithoutWaiting([key], .remote) + } else { + syncWithoutWaiting([key]) } } diff --git a/Sources/Defaults/Defaults.swift b/Sources/Defaults/Defaults.swift index 6c37700..79aefa3 100644 --- a/Sources/Defaults/Defaults.swift +++ b/Sources/Defaults/Defaults.swift @@ -187,6 +187,31 @@ extension Defaults.Key { ) where Value == T? { self.init(name, default: nil, suite: suite, iCloud: iCloud) } + + /** + Check whether the stored value is the default value. + + - Note: This is only for internal use because it would not work for non-equatable values. + */ + var _isDefaultValue: Bool { + let defaultValue = defaultValue + let value = suite[self] + guard + let defaultValue = defaultValue as? any Equatable, + let value = value as? any Equatable + else { + return false + } + + return defaultValue.isEqual(value) + } +} + +extension Defaults.Key where Value: Equatable { + /** + Check whether the stored value is the default value. + */ + public var isDefaultValue: Bool { self._isDefaultValue } } extension Defaults { diff --git a/Sources/Defaults/Utilities.swift b/Sources/Defaults/Utilities.swift index 07a871f..ab92d99 100644 --- a/Sources/Defaults/Utilities.swift +++ b/Sources/Defaults/Utilities.swift @@ -165,6 +165,19 @@ extension Collection { } } +extension Equatable { + func isEqual(_ rhs: any Equatable) -> Bool { + guard + let rhs = rhs as? Self, + rhs == self + else { + return false + } + + return true + } +} + extension Defaults { @usableFromInline static func isValidKeyPath(name: String) -> Bool { diff --git a/Tests/DefaultsTests/Defaults+iCloudTests.swift b/Tests/DefaultsTests/Defaults+iCloudTests.swift index 4e3824c..2f1d451 100644 --- a/Tests/DefaultsTests/Defaults+iCloudTests.swift +++ b/Tests/DefaultsTests/Defaults+iCloudTests.swift @@ -88,14 +88,12 @@ final class DefaultsICloudTests: XCTestCase { } func testICloudInitialize() async { - print(Defaults.iCloud.keys) let name = Defaults.Key("testICloudInitialize_name", default: "0", iCloud: true) let quality = Defaults.Key("testICloudInitialize_quality", default: 0.0, iCloud: true) - print(Defaults.iCloud.keys) await Defaults.iCloud.waitForSyncCompletion() - XCTAssertEqual(mockStorage.data(forKey: name.name), "0") - XCTAssertEqual(mockStorage.data(forKey: quality.name), 0.0) + XCTAssertNil(mockStorage.data(forKey: name.name)) + XCTAssertNil(mockStorage.data(forKey: quality.name)) let name_expected = ["1", "2", "3", "4", "5", "6", "7"] let quality_expected = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0] @@ -251,8 +249,9 @@ final class DefaultsICloudTests: XCTestCase { func testAddFromDetached() async { let name = Defaults.Key("testInitAddFromDetached_name", default: "0") + let quantity = Defaults.Key("testInitAddFromDetached_quantity", default: false) let task = Task.detached { - Defaults.iCloud.add(name) + Defaults.iCloud.add(name, quantity) Defaults.iCloud.syncWithoutWaiting() await Defaults.iCloud.waitForSyncCompletion() } @@ -268,7 +267,7 @@ final class DefaultsICloudTests: XCTestCase { let name = Defaults.Key("testICloudInitializeFromDetached_name", default: "0", iCloud: true) await Defaults.iCloud.waitForSyncCompletion() - XCTAssertEqual(mockStorage.data(forKey: name.name), "0") + XCTAssertNil(mockStorage.data(forKey: name.name)) } await task.value } diff --git a/Tests/DefaultsTests/DefaultsTests.swift b/Tests/DefaultsTests/DefaultsTests.swift index b783917..091b47b 100644 --- a/Tests/DefaultsTests/DefaultsTests.swift +++ b/Tests/DefaultsTests/DefaultsTests.swift @@ -177,6 +177,13 @@ final class DefaultsTests: XCTestCase { Defaults.removeAll(suite: customSuite) } + func testIsDefaultValue() { + let key = Defaults.Key("isDefaultValue", default: false) + XCTAssert(key.isDefaultValue) + Defaults[key].toggle() + XCTAssert(!key.isDefaultValue) + } + func testObserveKeyCombine() { let key = Defaults.Key("observeKey", default: false) let expect = expectation(description: "Observation closure being called")