diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a80cc11..4977b3e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,9 +5,20 @@ on: jobs: test: runs-on: macos-latest + strategy: + matrix: + scheme: [Defaults-macOS, Defaults-iOS, Defaults-tvOS] + include: + - scheme: Defaults-macOS + destination: macOS + - scheme: Defaults-iOS + destination: iOS Simulator,name=iPhone 8 + - scheme: Defaults-tvOS + destination: tvOS Simulator,name=Apple TV steps: - uses: actions/checkout@v2 - - run: swift test + - name: Run tests + run: xcodebuild clean test -project Defaults.xcodeproj -scheme ${{ matrix.scheme }} -destination "platform=${{ matrix.destination }}" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO lint: runs-on: ubuntu-latest steps: diff --git a/Defaults.xcodeproj/project.pbxproj b/Defaults.xcodeproj/project.pbxproj index d641672..35d2b74 100644 --- a/Defaults.xcodeproj/project.pbxproj +++ b/Defaults.xcodeproj/project.pbxproj @@ -8,6 +8,79 @@ /* Begin PBXBuildFile section */ 52D6D9871BEFF229002C0205 /* Defaults.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D97C1BEFF229002C0205 /* Defaults.framework */; }; + 71056FF425DE6DEF00524EDA /* Migration+Protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71056FF125DE6DEF00524EDA /* Migration+Protocol.swift */; }; + 71056FF525DE6DEF00524EDA /* Migration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71056FF225DE6DEF00524EDA /* Migration+Extensions.swift */; }; + 71056FF625DE6DEF00524EDA /* Migration+UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71056FF325DE6DEF00524EDA /* Migration+UserDefaults.swift */; }; + 7108EAC125942BF60013A623 /* DefaultsSwiftUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7108EAC025942BF60013A623 /* DefaultsSwiftUITests.swift */; }; + 7108EAC225942BF60013A623 /* DefaultsSwiftUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7108EAC025942BF60013A623 /* DefaultsSwiftUITests.swift */; }; + 7108EAC325942BF60013A623 /* DefaultsSwiftUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7108EAC025942BF60013A623 /* DefaultsSwiftUITests.swift */; }; + 7108EAF425949DBF0013A623 /* DefaultsDictionaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7108EAF325949DBF0013A623 /* DefaultsDictionaryTests.swift */; }; + 7108EAF525949DBF0013A623 /* DefaultsDictionaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7108EAF325949DBF0013A623 /* DefaultsDictionaryTests.swift */; }; + 7108EAF625949DBF0013A623 /* DefaultsDictionaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7108EAF325949DBF0013A623 /* DefaultsDictionaryTests.swift */; }; + 71362AC025A5493300FAA91B /* DefaultsCollectionCustomElementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71362ABF25A5493300FAA91B /* DefaultsCollectionCustomElementTests.swift */; }; + 71362AC125A5493300FAA91B /* DefaultsCollectionCustomElementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71362ABF25A5493300FAA91B /* DefaultsCollectionCustomElementTests.swift */; }; + 71362AC225A5493300FAA91B /* DefaultsCollectionCustomElementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71362ABF25A5493300FAA91B /* DefaultsCollectionCustomElementTests.swift */; }; + 71362ACB25A567AD00FAA91B /* DefaultsSetAlgebraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71362ACA25A567AD00FAA91B /* DefaultsSetAlgebraTests.swift */; }; + 71362ACC25A567AD00FAA91B /* DefaultsSetAlgebraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71362ACA25A567AD00FAA91B /* DefaultsSetAlgebraTests.swift */; }; + 71362ACD25A567AD00FAA91B /* DefaultsSetAlgebraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71362ACA25A567AD00FAA91B /* DefaultsSetAlgebraTests.swift */; }; + 7150D6F425C2968700201966 /* DefaultsMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7150D6F325C2968700201966 /* DefaultsMigrationTests.swift */; }; + 7150D6F525C2968700201966 /* DefaultsMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7150D6F325C2968700201966 /* DefaultsMigrationTests.swift */; }; + 7150D6F625C2968700201966 /* DefaultsMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7150D6F325C2968700201966 /* DefaultsMigrationTests.swift */; }; + 7152A04D25A5BF6200CC9BA5 /* DefaultsSetAlgebraCustomElementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7152A04C25A5BF6200CC9BA5 /* DefaultsSetAlgebraCustomElementTests.swift */; }; + 7152A04E25A5BF6200CC9BA5 /* DefaultsSetAlgebraCustomElementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7152A04C25A5BF6200CC9BA5 /* DefaultsSetAlgebraCustomElementTests.swift */; }; + 7152A04F25A5BF6200CC9BA5 /* DefaultsSetAlgebraCustomElementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7152A04C25A5BF6200CC9BA5 /* DefaultsSetAlgebraCustomElementTests.swift */; }; + 71573E9925A445CE00F18D4E /* DefaultsCollectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71573E8925A445A100F18D4E /* DefaultsCollectionTests.swift */; }; + 71573EA125A445D400F18D4E /* DefaultsCollectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71573E8925A445A100F18D4E /* DefaultsCollectionTests.swift */; }; + 71573EA925A445DB00F18D4E /* DefaultsCollectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71573E8925A445A100F18D4E /* DefaultsCollectionTests.swift */; }; + 7168638325E886DB00F55131 /* Migration+Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7168638225E886DB00F55131 /* Migration+Defaults.swift */; }; + 718B783325917CCA004FF90D /* Defaults+Protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 718B783225917CCA004FF90D /* Defaults+Protocol.swift */; }; + 718B783425917CCA004FF90D /* Defaults+Protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 718B783225917CCA004FF90D /* Defaults+Protocol.swift */; }; + 718B783525917CCA004FF90D /* Defaults+Protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 718B783225917CCA004FF90D /* Defaults+Protocol.swift */; }; + 718B783625917CCA004FF90D /* Defaults+Protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 718B783225917CCA004FF90D /* Defaults+Protocol.swift */; }; + 718B783F25917D09004FF90D /* Defaults+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 718B783E25917D09004FF90D /* Defaults+Extensions.swift */; }; + 718B784025917D09004FF90D /* Defaults+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 718B783E25917D09004FF90D /* Defaults+Extensions.swift */; }; + 718B784125917D09004FF90D /* Defaults+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 718B783E25917D09004FF90D /* Defaults+Extensions.swift */; }; + 718B784225917D09004FF90D /* Defaults+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 718B783E25917D09004FF90D /* Defaults+Extensions.swift */; }; + 7191AD872591977700AD472F /* DefaultsArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7191AD862591977700AD472F /* DefaultsArrayTests.swift */; }; + 7191AD882591977700AD472F /* DefaultsArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7191AD862591977700AD472F /* DefaultsArrayTests.swift */; }; + 7191AD892591977700AD472F /* DefaultsArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7191AD862591977700AD472F /* DefaultsArrayTests.swift */; }; + 719F5E20258AFB2F004540F6 /* Defaults+Bridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 719F5E1F258AFB2F004540F6 /* Defaults+Bridge.swift */; }; + 719F5E21258AFB2F004540F6 /* Defaults+Bridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 719F5E1F258AFB2F004540F6 /* Defaults+Bridge.swift */; }; + 719F5E22258AFB2F004540F6 /* Defaults+Bridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 719F5E1F258AFB2F004540F6 /* Defaults+Bridge.swift */; }; + 719F5E23258AFB2F004540F6 /* Defaults+Bridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 719F5E1F258AFB2F004540F6 /* Defaults+Bridge.swift */; }; + 71A7132D260497EE004095EE /* Migration+Protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71056FF125DE6DEF00524EDA /* Migration+Protocol.swift */; }; + 71A7132E260497EE004095EE /* Migration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71056FF225DE6DEF00524EDA /* Migration+Extensions.swift */; }; + 71A7132F260497EE004095EE /* Migration+UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71056FF325DE6DEF00524EDA /* Migration+UserDefaults.swift */; }; + 71A71330260497EE004095EE /* Migration+Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7168638225E886DB00F55131 /* Migration+Defaults.swift */; }; + 71A713482604A084004095EE /* Migration+Protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71056FF125DE6DEF00524EDA /* Migration+Protocol.swift */; }; + 71A713492604A084004095EE /* Migration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71056FF225DE6DEF00524EDA /* Migration+Extensions.swift */; }; + 71A7134A2604A084004095EE /* Migration+UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71056FF325DE6DEF00524EDA /* Migration+UserDefaults.swift */; }; + 71A7134B2604A084004095EE /* Migration+Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7168638225E886DB00F55131 /* Migration+Defaults.swift */; }; + 71A7135B2604A13B004095EE /* Migration+Protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71056FF125DE6DEF00524EDA /* Migration+Protocol.swift */; }; + 71A7135C2604A13B004095EE /* Migration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71056FF225DE6DEF00524EDA /* Migration+Extensions.swift */; }; + 71A7135D2604A13B004095EE /* Migration+UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71056FF325DE6DEF00524EDA /* Migration+UserDefaults.swift */; }; + 71A7135E2604A13B004095EE /* Migration+Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7168638225E886DB00F55131 /* Migration+Defaults.swift */; }; + 71B96F1E259986F100079F69 /* DefaultsCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71B96F1D259986F100079F69 /* DefaultsCodableTests.swift */; }; + 71B96F1F259986F100079F69 /* DefaultsCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71B96F1D259986F100079F69 /* DefaultsCodableTests.swift */; }; + 71B96F20259986F100079F69 /* DefaultsCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71B96F1D259986F100079F69 /* DefaultsCodableTests.swift */; }; + 71C55FF6259C13190053CCB3 /* DefaultsNSColorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71C55FF4259C13190053CCB3 /* DefaultsNSColorTests.swift */; }; + 71C56007259C25080053CCB3 /* DefaultsUIColorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71C56006259C25080053CCB3 /* DefaultsUIColorTests.swift */; }; + 71C56009259C25080053CCB3 /* DefaultsUIColorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71C56006259C25080053CCB3 /* DefaultsUIColorTests.swift */; }; + 71C5602E259C2A950053CCB3 /* DefaultsSetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71C5602D259C2A950053CCB3 /* DefaultsSetTests.swift */; }; + 71C5602F259C2A950053CCB3 /* DefaultsSetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71C5602D259C2A950053CCB3 /* DefaultsSetTests.swift */; }; + 71C56030259C2A950053CCB3 /* DefaultsSetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71C5602D259C2A950053CCB3 /* DefaultsSetTests.swift */; }; + 71F002F925959000001A1864 /* DefaultsNSSecureCodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F002F825959000001A1864 /* DefaultsNSSecureCodingTests.swift */; }; + 71F002FA25959000001A1864 /* DefaultsNSSecureCodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F002F825959000001A1864 /* DefaultsNSSecureCodingTests.swift */; }; + 71F002FB25959000001A1864 /* DefaultsNSSecureCodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F002F825959000001A1864 /* DefaultsNSSecureCodingTests.swift */; }; + 71F003042595B460001A1864 /* DefaultsCustomBridgeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F003032595B460001A1864 /* DefaultsCustomBridgeTests.swift */; }; + 71F003052595B460001A1864 /* DefaultsCustomBridgeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F003032595B460001A1864 /* DefaultsCustomBridgeTests.swift */; }; + 71F003062595B460001A1864 /* DefaultsCustomBridgeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F003032595B460001A1864 /* DefaultsCustomBridgeTests.swift */; }; + 71F0030F2595C7B8001A1864 /* DefaultsEnumTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F0030E2595C7B8001A1864 /* DefaultsEnumTests.swift */; }; + 71F003102595C7B8001A1864 /* DefaultsEnumTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F0030E2595C7B8001A1864 /* DefaultsEnumTests.swift */; }; + 71F003112595C7B8001A1864 /* DefaultsEnumTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F0030E2595C7B8001A1864 /* DefaultsEnumTests.swift */; }; + 71F0031A2595E15B001A1864 /* DefaultsCodableEnumTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F003192595E15B001A1864 /* DefaultsCodableEnumTests.swift */; }; + 71F0031B2595E15B001A1864 /* DefaultsCodableEnumTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F003192595E15B001A1864 /* DefaultsCodableEnumTests.swift */; }; + 71F0031C2595E15B001A1864 /* DefaultsCodableEnumTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F003192595E15B001A1864 /* DefaultsCodableEnumTests.swift */; }; 8933C7851EB5B820000D00A4 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8933C7841EB5B820000D00A4 /* Defaults.swift */; }; 8933C7861EB5B820000D00A4 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8933C7841EB5B820000D00A4 /* Defaults.swift */; }; 8933C7871EB5B820000D00A4 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8933C7841EB5B820000D00A4 /* Defaults.swift */; }; @@ -74,6 +147,29 @@ 52D6D9F01BEFFFBE002C0205 /* Defaults.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Defaults.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 52D6DA0F1BF000BD002C0205 /* Defaults.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Defaults.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6614F6E222FC6E1C00B0C9CE /* readme.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; lineEnding = 0; path = readme.md; sourceTree = ""; usesTabs = 1; }; + 71056FF125DE6DEF00524EDA /* Migration+Protocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Migration+Protocol.swift"; sourceTree = ""; }; + 71056FF225DE6DEF00524EDA /* Migration+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Migration+Extensions.swift"; sourceTree = ""; }; + 71056FF325DE6DEF00524EDA /* Migration+UserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Migration+UserDefaults.swift"; sourceTree = ""; }; + 7108EAC025942BF60013A623 /* DefaultsSwiftUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsSwiftUITests.swift; sourceTree = ""; }; + 7108EAF325949DBF0013A623 /* DefaultsDictionaryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsDictionaryTests.swift; sourceTree = ""; }; + 71362ABF25A5493300FAA91B /* DefaultsCollectionCustomElementTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsCollectionCustomElementTests.swift; sourceTree = ""; }; + 71362ACA25A567AD00FAA91B /* DefaultsSetAlgebraTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsSetAlgebraTests.swift; sourceTree = ""; }; + 7150D6F325C2968700201966 /* DefaultsMigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsMigrationTests.swift; sourceTree = ""; }; + 7152A04C25A5BF6200CC9BA5 /* DefaultsSetAlgebraCustomElementTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsSetAlgebraCustomElementTests.swift; sourceTree = ""; }; + 71573E8925A445A100F18D4E /* DefaultsCollectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsCollectionTests.swift; sourceTree = ""; }; + 7168638225E886DB00F55131 /* Migration+Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Migration+Defaults.swift"; sourceTree = ""; }; + 718B783225917CCA004FF90D /* Defaults+Protocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Defaults+Protocol.swift"; sourceTree = ""; }; + 718B783E25917D09004FF90D /* Defaults+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Defaults+Extensions.swift"; sourceTree = ""; }; + 7191AD862591977700AD472F /* DefaultsArrayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsArrayTests.swift; sourceTree = ""; }; + 719F5E1F258AFB2F004540F6 /* Defaults+Bridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Defaults+Bridge.swift"; sourceTree = ""; }; + 71B96F1D259986F100079F69 /* DefaultsCodableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsCodableTests.swift; sourceTree = ""; }; + 71C55FF4259C13190053CCB3 /* DefaultsNSColorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsNSColorTests.swift; sourceTree = ""; }; + 71C56006259C25080053CCB3 /* DefaultsUIColorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsUIColorTests.swift; sourceTree = ""; }; + 71C5602D259C2A950053CCB3 /* DefaultsSetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsSetTests.swift; sourceTree = ""; }; + 71F002F825959000001A1864 /* DefaultsNSSecureCodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsNSSecureCodingTests.swift; sourceTree = ""; }; + 71F003032595B460001A1864 /* DefaultsCustomBridgeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsCustomBridgeTests.swift; sourceTree = ""; }; + 71F0030E2595C7B8001A1864 /* DefaultsEnumTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsEnumTests.swift; sourceTree = ""; }; + 71F003192595E15B001A1864 /* DefaultsCodableEnumTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsCodableEnumTests.swift; sourceTree = ""; }; 8933C7841EB5B820000D00A4 /* Defaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Defaults.swift; sourceTree = ""; usesTabs = 1; }; 8933C7891EB5B82A000D00A4 /* DefaultsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = DefaultsTests.swift; sourceTree = ""; usesTabs = 1; }; AD2FAA261CD0B6D800659CF4 /* Defaults.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Defaults.plist; sourceTree = ""; }; @@ -179,6 +275,25 @@ path = Configs; sourceTree = ""; }; + 71056FF025DE6DEF00524EDA /* Migration */ = { + isa = PBXGroup; + children = ( + 7156A8F226205E0C00A1A66E /* v5 */, + 7168638225E886DB00F55131 /* Migration+Defaults.swift */, + ); + path = Migration; + sourceTree = ""; + }; + 7156A8F226205E0C00A1A66E /* v5 */ = { + isa = PBXGroup; + children = ( + 71056FF125DE6DEF00524EDA /* Migration+Protocol.swift */, + 71056FF225DE6DEF00524EDA /* Migration+Extensions.swift */, + 71056FF325DE6DEF00524EDA /* Migration+UserDefaults.swift */, + ); + path = v5; + sourceTree = ""; + }; 8933C7811EB5B7E0000D00A4 /* Sources */ = { isa = PBXGroup; children = ( @@ -191,6 +306,22 @@ isa = PBXGroup; children = ( 8933C7891EB5B82A000D00A4 /* DefaultsTests.swift */, + 7191AD862591977700AD472F /* DefaultsArrayTests.swift */, + 7108EAC025942BF60013A623 /* DefaultsSwiftUITests.swift */, + 7108EAF325949DBF0013A623 /* DefaultsDictionaryTests.swift */, + 71F002F825959000001A1864 /* DefaultsNSSecureCodingTests.swift */, + 71F003032595B460001A1864 /* DefaultsCustomBridgeTests.swift */, + 71F0030E2595C7B8001A1864 /* DefaultsEnumTests.swift */, + 71F003192595E15B001A1864 /* DefaultsCodableEnumTests.swift */, + 71B96F1D259986F100079F69 /* DefaultsCodableTests.swift */, + 71C55FF4259C13190053CCB3 /* DefaultsNSColorTests.swift */, + 71C56006259C25080053CCB3 /* DefaultsUIColorTests.swift */, + 71C5602D259C2A950053CCB3 /* DefaultsSetTests.swift */, + 71573E8925A445A100F18D4E /* DefaultsCollectionTests.swift */, + 71362ABF25A5493300FAA91B /* DefaultsCollectionCustomElementTests.swift */, + 71362ACA25A567AD00FAA91B /* DefaultsSetAlgebraTests.swift */, + 7152A04C25A5BF6200CC9BA5 /* DefaultsSetAlgebraCustomElementTests.swift */, + 7150D6F325C2968700201966 /* DefaultsMigrationTests.swift */, ); name = Tests; path = Tests/DefaultsTests; @@ -215,6 +346,7 @@ E30E93D822E9425E00530C8F /* Defaults */ = { isa = PBXGroup; children = ( + 71056FF025DE6DEF00524EDA /* Migration */, 8933C7841EB5B820000D00A4 /* Defaults.swift */, E339B3B72449F10D00E7A40A /* UserDefaults.swift */, E339B3B22449ED2000E7A40A /* Reset.swift */, @@ -222,6 +354,9 @@ E286D0C623B8D51100570D1E /* Observation+Combine.swift */, E38C9F26244ADA2F00A6737A /* SwiftUI.swift */, E3EB3E32216505920033B089 /* Utilities.swift */, + 718B783225917CCA004FF90D /* Defaults+Protocol.swift */, + 718B783E25917D09004FF90D /* Defaults+Extensions.swift */, + 719F5E1F258AFB2F004540F6 /* Defaults+Bridge.swift */, ); path = Defaults; sourceTree = ""; @@ -264,6 +399,7 @@ isa = PBXNativeTarget; buildConfigurationList = 52D6D9901BEFF229002C0205 /* Build configuration list for PBXNativeTarget "Defaults-iOS" */; buildPhases = ( + 71A71338260497FF004095EE /* SwiftLint */, 52D6D9771BEFF229002C0205 /* Sources */, 52D6D9781BEFF229002C0205 /* Frameworks */, 52D6D9791BEFF229002C0205 /* Headers */, @@ -300,6 +436,7 @@ isa = PBXNativeTarget; buildConfigurationList = 52D6D9E71BEFFF6E002C0205 /* Build configuration list for PBXNativeTarget "Defaults-watchOS" */; buildPhases = ( + 71A713532604A090004095EE /* SwiftLint */, 52D6D9DD1BEFFF6E002C0205 /* Sources */, 52D6D9DE1BEFFF6E002C0205 /* Frameworks */, 52D6D9DF1BEFFF6E002C0205 /* Headers */, @@ -318,6 +455,7 @@ isa = PBXNativeTarget; buildConfigurationList = 52D6DA011BEFFFBE002C0205 /* Build configuration list for PBXNativeTarget "Defaults-tvOS" */; buildPhases = ( + 71A713472604A05A004095EE /* SwiftLint */, 52D6D9EB1BEFFFBE002C0205 /* Sources */, 52D6D9EC1BEFFFBE002C0205 /* Frameworks */, 52D6D9ED1BEFFFBE002C0205 /* Headers */, @@ -365,6 +503,8 @@ DD7502811C68FCFC006590AF /* PBXTargetDependency */, ); name = "Defaults-macOS Tests"; + packageProductDependencies = ( + ); productName = "Defaults-OS Tests"; productReference = DD75027A1C68FCFC006590AF /* Defaults-macOS Tests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; @@ -436,6 +576,8 @@ Base, ); mainGroup = 52D6D9721BEFF229002C0205; + packageReferences = ( + ); productRefGroup = 52D6D97D1BEFF229002C0205 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -504,6 +646,60 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 71A71338260497FF004095EE /* SwiftLint */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = SwiftLint; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "swiftlint\n"; + }; + 71A713472604A05A004095EE /* SwiftLint */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = SwiftLint; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "swiftlint\n"; + }; + 71A713532604A090004095EE /* SwiftLint */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = SwiftLint; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "swiftlint\n"; + }; E3FD0A4B25BDA35F0011D293 /* SwiftLint */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -529,13 +725,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 71A7132D260497EE004095EE /* Migration+Protocol.swift in Sources */, + 71A7132E260497EE004095EE /* Migration+Extensions.swift in Sources */, + 71A7132F260497EE004095EE /* Migration+UserDefaults.swift in Sources */, + 71A71330260497EE004095EE /* Migration+Defaults.swift in Sources */, E286D0C823B8D54C00570D1E /* Observation+Combine.swift in Sources */, E38C9F28244ADA2F00A6737A /* SwiftUI.swift in Sources */, 8933C7851EB5B820000D00A4 /* Defaults.swift in Sources */, E339B3B92449F10D00E7A40A /* UserDefaults.swift in Sources */, + 719F5E21258AFB2F004540F6 /* Defaults+Bridge.swift in Sources */, E3EB3E35216507AE0033B089 /* Observation.swift in Sources */, E3EB3E33216505920033B089 /* Utilities.swift in Sources */, + 718B784025917D09004FF90D /* Defaults+Extensions.swift in Sources */, E339B3B42449ED2000E7A40A /* Reset.swift in Sources */, + 718B783425917CCA004FF90D /* Defaults+Protocol.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -543,7 +746,22 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7152A04D25A5BF6200CC9BA5 /* DefaultsSetAlgebraCustomElementTests.swift in Sources */, + 71573E9925A445CE00F18D4E /* DefaultsCollectionTests.swift in Sources */, + 71362ACB25A567AD00FAA91B /* DefaultsSetAlgebraTests.swift in Sources */, + 71F002F925959000001A1864 /* DefaultsNSSecureCodingTests.swift in Sources */, + 7108EAC125942BF60013A623 /* DefaultsSwiftUITests.swift in Sources */, + 71F0031A2595E15B001A1864 /* DefaultsCodableEnumTests.swift in Sources */, + 71C56007259C25080053CCB3 /* DefaultsUIColorTests.swift in Sources */, + 71C5602E259C2A950053CCB3 /* DefaultsSetTests.swift in Sources */, + 7108EAF425949DBF0013A623 /* DefaultsDictionaryTests.swift in Sources */, + 71F003042595B460001A1864 /* DefaultsCustomBridgeTests.swift in Sources */, 8933C7901EB5B82D000D00A4 /* DefaultsTests.swift in Sources */, + 71B96F1E259986F100079F69 /* DefaultsCodableTests.swift in Sources */, + 7150D6F425C2968700201966 /* DefaultsMigrationTests.swift in Sources */, + 71F0030F2595C7B8001A1864 /* DefaultsEnumTests.swift in Sources */, + 7191AD872591977700AD472F /* DefaultsArrayTests.swift in Sources */, + 71362AC025A5493300FAA91B /* DefaultsCollectionCustomElementTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -551,13 +769,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 71A7135B2604A13B004095EE /* Migration+Protocol.swift in Sources */, + 71A7135C2604A13B004095EE /* Migration+Extensions.swift in Sources */, + 71A7135D2604A13B004095EE /* Migration+UserDefaults.swift in Sources */, + 71A7135E2604A13B004095EE /* Migration+Defaults.swift in Sources */, E286D0CA23B8D54E00570D1E /* Observation+Combine.swift in Sources */, E38C9F2A244ADA2F00A6737A /* SwiftUI.swift in Sources */, E3EB3E3A216507C40033B089 /* Utilities.swift in Sources */, E339B3BB2449F10D00E7A40A /* UserDefaults.swift in Sources */, + 719F5E23258AFB2F004540F6 /* Defaults+Bridge.swift in Sources */, E3EB3E37216507B50033B089 /* Observation.swift in Sources */, 8933C7871EB5B820000D00A4 /* Defaults.swift in Sources */, + 718B784225917D09004FF90D /* Defaults+Extensions.swift in Sources */, E339B3B62449ED2000E7A40A /* Reset.swift in Sources */, + 718B783625917CCA004FF90D /* Defaults+Protocol.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -565,13 +790,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 71A713482604A084004095EE /* Migration+Protocol.swift in Sources */, + 71A713492604A084004095EE /* Migration+Extensions.swift in Sources */, + 71A7134A2604A084004095EE /* Migration+UserDefaults.swift in Sources */, + 71A7134B2604A084004095EE /* Migration+Defaults.swift in Sources */, E286D0C923B8D54D00570D1E /* Observation+Combine.swift in Sources */, E38C9F29244ADA2F00A6737A /* SwiftUI.swift in Sources */, E3EB3E3B216507C40033B089 /* Utilities.swift in Sources */, E339B3BA2449F10D00E7A40A /* UserDefaults.swift in Sources */, + 719F5E22258AFB2F004540F6 /* Defaults+Bridge.swift in Sources */, E3EB3E38216507B60033B089 /* Observation.swift in Sources */, 8933C7881EB5B820000D00A4 /* Defaults.swift in Sources */, + 718B784125917D09004FF90D /* Defaults+Extensions.swift in Sources */, E339B3B52449ED2000E7A40A /* Reset.swift in Sources */, + 718B783525917CCA004FF90D /* Defaults+Protocol.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -581,11 +813,18 @@ files = ( E286D0C723B8D51100570D1E /* Observation+Combine.swift in Sources */, E38C9F27244ADA2F00A6737A /* SwiftUI.swift in Sources */, + 7168638325E886DB00F55131 /* Migration+Defaults.swift in Sources */, E3EB3E39216507C30033B089 /* Utilities.swift in Sources */, E339B3B82449F10D00E7A40A /* UserDefaults.swift in Sources */, + 719F5E20258AFB2F004540F6 /* Defaults+Bridge.swift in Sources */, E3EB3E36216507B50033B089 /* Observation.swift in Sources */, + 71056FF425DE6DEF00524EDA /* Migration+Protocol.swift in Sources */, + 71056FF525DE6DEF00524EDA /* Migration+Extensions.swift in Sources */, 8933C7861EB5B820000D00A4 /* Defaults.swift in Sources */, + 718B783F25917D09004FF90D /* Defaults+Extensions.swift in Sources */, E339B3B32449ED2000E7A40A /* Reset.swift in Sources */, + 71056FF625DE6DEF00524EDA /* Migration+UserDefaults.swift in Sources */, + 718B783325917CCA004FF90D /* Defaults+Protocol.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -593,7 +832,22 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7152A04E25A5BF6200CC9BA5 /* DefaultsSetAlgebraCustomElementTests.swift in Sources */, + 71573EA125A445D400F18D4E /* DefaultsCollectionTests.swift in Sources */, + 71362ACC25A567AD00FAA91B /* DefaultsSetAlgebraTests.swift in Sources */, + 71C55FF6259C13190053CCB3 /* DefaultsNSColorTests.swift in Sources */, + 71F002FA25959000001A1864 /* DefaultsNSSecureCodingTests.swift in Sources */, + 7108EAC225942BF60013A623 /* DefaultsSwiftUITests.swift in Sources */, + 71F0031B2595E15B001A1864 /* DefaultsCodableEnumTests.swift in Sources */, + 71C5602F259C2A950053CCB3 /* DefaultsSetTests.swift in Sources */, + 7108EAF525949DBF0013A623 /* DefaultsDictionaryTests.swift in Sources */, + 71F003052595B460001A1864 /* DefaultsCustomBridgeTests.swift in Sources */, 8933C78F1EB5B82C000D00A4 /* DefaultsTests.swift in Sources */, + 71B96F1F259986F100079F69 /* DefaultsCodableTests.swift in Sources */, + 7150D6F525C2968700201966 /* DefaultsMigrationTests.swift in Sources */, + 71F003102595C7B8001A1864 /* DefaultsEnumTests.swift in Sources */, + 7191AD882591977700AD472F /* DefaultsArrayTests.swift in Sources */, + 71362AC125A5493300FAA91B /* DefaultsCollectionCustomElementTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -601,7 +855,22 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7152A04F25A5BF6200CC9BA5 /* DefaultsSetAlgebraCustomElementTests.swift in Sources */, + 71573EA925A445DB00F18D4E /* DefaultsCollectionTests.swift in Sources */, + 71362ACD25A567AD00FAA91B /* DefaultsSetAlgebraTests.swift in Sources */, + 71F002FB25959000001A1864 /* DefaultsNSSecureCodingTests.swift in Sources */, + 7108EAC325942BF60013A623 /* DefaultsSwiftUITests.swift in Sources */, + 71F0031C2595E15B001A1864 /* DefaultsCodableEnumTests.swift in Sources */, + 71C56009259C25080053CCB3 /* DefaultsUIColorTests.swift in Sources */, + 71C56030259C2A950053CCB3 /* DefaultsSetTests.swift in Sources */, + 7108EAF625949DBF0013A623 /* DefaultsDictionaryTests.swift in Sources */, + 71F003062595B460001A1864 /* DefaultsCustomBridgeTests.swift in Sources */, 8933C78E1EB5B82C000D00A4 /* DefaultsTests.swift in Sources */, + 71B96F20259986F100079F69 /* DefaultsCodableTests.swift in Sources */, + 7150D6F625C2968700201966 /* DefaultsMigrationTests.swift in Sources */, + 71F003112595C7B8001A1864 /* DefaultsEnumTests.swift in Sources */, + 7191AD892591977700AD472F /* DefaultsArrayTests.swift in Sources */, + 71362AC225A5493300FAA91B /* DefaultsCollectionCustomElementTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Defaults.xcodeproj/xcshareddata/xcschemes/Defaults-iOS.xcscheme b/Defaults.xcodeproj/xcshareddata/xcschemes/Defaults-iOS.xcscheme index dfc0e45..34a2199 100644 --- a/Defaults.xcodeproj/xcshareddata/xcschemes/Defaults-iOS.xcscheme +++ b/Defaults.xcodeproj/xcshareddata/xcschemes/Defaults-iOS.xcscheme @@ -26,7 +26,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES"> Serializable? { + guard let value = value else { + return nil + } + + do { + // Some codable values like URL and enum are encoded as a top-level + // string which JSON can't handle, so we need to wrap it in an array + // We need this: https://forums.swift.org/t/allowing-top-level-fragments-in-jsondecoder/11750 + let data = try JSONEncoder().encode([value]) + return String(String(data: data, encoding: .utf8)!.dropFirst().dropLast()) + } catch { + print(error) + return nil + } + } + + public func deserialize(_ object: Serializable?) -> Value? { + guard let jsonString = object else { + return nil + } + + return [Value].init(jsonString: "[\(jsonString)]")?.first + } +} + +/** +Any `Value` which protocol conforms to `Codable` and `Defaults.Serializable` will use `CodableBridge` +to do the serialization and deserialization. +*/ +extension Defaults { + public struct TopLevelCodableBridge: CodableBridge {} +} + +/** +`RawRepresentableCodableBridge` is indeed because if `enum SomeEnum: String, Codable, Defaults.Serializable` +the compiler will confuse between `RawRepresentableBridge` and `TopLevelCodableBridge`. +*/ +extension Defaults { + public struct RawRepresentableCodableBridge: CodableBridge {} +} + +extension Defaults { + public struct URLBridge: CodableBridge { + public typealias Value = URL + } +} + +extension Defaults { + public struct RawRepresentableBridge: Defaults.Bridge { + public typealias Value = Value + public typealias Serializable = Value.RawValue + + public func serialize(_ value: Value?) -> Serializable? { + value?.rawValue + } + + public func deserialize(_ object: Serializable?) -> Value? { + guard let rawValue = object else { + return nil + } + + return Value(rawValue: rawValue) + } + } +} + +extension Defaults { + public struct NSSecureCodingBridge: Defaults.Bridge { + public typealias Value = Value + public typealias Serializable = Data + + public func serialize(_ value: Value?) -> Serializable? { + guard let object = value else { + return nil + } + + // Version below macOS 10.13 and iOS 11.0 does not support `archivedData(withRootObject:requiringSecureCoding:)`. + // We need to set `requiresSecureCoding` by ourself. + if #available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) { + return try? NSKeyedArchiver.archivedData(withRootObject: object, requiringSecureCoding: true) + } else { + let keyedArchiver = NSKeyedArchiver() + keyedArchiver.requiresSecureCoding = true + keyedArchiver.encode(object, forKey: NSKeyedArchiveRootObjectKey) + return keyedArchiver.encodedData + } + } + + public func deserialize(_ object: Serializable?) -> Value? { + guard let data = object else { + return nil + } + + do { + return try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? Value + } catch { + print(error) + return nil + } + } + } +} + +extension Defaults { + public struct OptionalBridge: Defaults.Bridge { + public typealias Value = Wrapped.Value + public typealias Serializable = Wrapped.Serializable + + public func serialize(_ value: Value?) -> Serializable? { + Wrapped.bridge.serialize(value) + } + + public func deserialize(_ object: Serializable?) -> Value? { + Wrapped.bridge.deserialize(object) + } + } +} + +extension Defaults { + public struct ArrayBridge: Defaults.Bridge { + public typealias Value = [Element] + public typealias Serializable = [Element.Serializable] + + public func serialize(_ value: Value?) -> Serializable? { + guard let array = value as? [Element.Value] else { + return nil + } + + return array.map { Element.bridge.serialize($0) }.compact() + } + + public func deserialize(_ object: Serializable?) -> Value? { + guard let array = object else { + return nil + } + + return array.map { Element.bridge.deserialize($0) }.compact() as? Value + } + } +} + +extension Defaults { + public struct DictionaryBridge: Defaults.Bridge { + public typealias Value = [Key: Element.Value] + public typealias Serializable = [String: Element.Serializable] + + public func serialize(_ value: Value?) -> Serializable? { + guard let dictionary = value else { + return nil + } + + // `Key` which stored in `UserDefaults` have to be `String` + return dictionary.reduce(into: Serializable()) { memo, tuple in + memo[String(tuple.key)] = Element.bridge.serialize(tuple.value) + } + } + + public func deserialize(_ object: Serializable?) -> Value? { + guard let dictionary = object else { + return nil + } + + return dictionary.reduce(into: Value()) { memo, tuple in + // Use `LosslessStringConvertible` to create `Key` instance + guard let key = Key(tuple.key) else { + return + } + + memo[key] = Element.bridge.deserialize(tuple.value) + } + } + } +} + +/** +We need both `SetBridge` and `SetAlgebraBridge`. + +Because `Set` conforms to `Sequence` but `SetAlgebra` not. + +Set conforms to `Sequence`, so we can convert it into an array with `Array.init(S)` and store it in the `UserDefaults`. + +But `SetAlgebra` does not, so it is hard to convert it into an array. + +Thats why we need `Defaults.SetAlgebraSerializable` protocol to convert it into an array. +*/ +extension Defaults { + public struct SetBridge: Defaults.Bridge { + public typealias Value = Set + public typealias Serializable = Any + + public func serialize(_ value: Value?) -> Serializable? { + guard let set = value else { + return nil + } + + if Element.isNativelySupportedType { + return Array(set) + } + + return set.map { Element.bridge.serialize($0 as? Element.Value) }.compact() + } + + public func deserialize(_ object: Serializable?) -> Value? { + if Element.isNativelySupportedType { + guard let array = object as? [Element] else { + return nil + } + + return Set(array) + } + + guard + let array = object as? [Element.Serializable], + let elements = array.map({ Element.bridge.deserialize($0) }).compact() as? [Element] + else { + return nil + } + + return Set(elements) + } + } +} + +extension Defaults { + public struct SetAlgebraBridge: Defaults.Bridge where Value.Element: Defaults.Serializable { + public typealias Value = Value + public typealias Element = Value.Element + public typealias Serializable = Any + + public func serialize(_ value: Value?) -> Serializable? { + guard let setAlgebra = value else { + return nil + } + + if Element.isNativelySupportedType { + return setAlgebra.toArray() + } + + return setAlgebra.toArray().map { Element.bridge.serialize($0 as? Element.Value) }.compact() + } + + public func deserialize(_ object: Serializable?) -> Value? { + if Element.isNativelySupportedType { + guard let array = object as? [Element] else { + return nil + } + + return Value(array) + } + + guard + let array = object as? [Element.Serializable], + let elements = array.map({ Element.bridge.deserialize($0) }).compact() as? [Element] + else { + return nil + } + + return Value(elements) + } + } +} + +extension Defaults { + public struct CollectionBridge: Defaults.Bridge where Value.Element: Defaults.Serializable { + public typealias Value = Value + public typealias Element = Value.Element + public typealias Serializable = Any + + public func serialize(_ value: Value?) -> Serializable? { + guard let collection = value else { + return nil + } + + if Element.isNativelySupportedType { + return Array(collection) + } + + return collection.map { Element.bridge.serialize($0 as? Element.Value) }.compact() + } + + public func deserialize(_ object: Serializable?) -> Value? { + if Element.isNativelySupportedType { + guard let array = object as? [Element] else { + return nil + } + + return Value(array) + } + + guard + let array = object as? [Element.Serializable], + let elements = array.map({ Element.bridge.deserialize($0) }).compact() as? [Element] + else { + return nil + } + + return Value(elements) + } + } +} diff --git a/Sources/Defaults/Defaults+Extensions.swift b/Sources/Defaults/Defaults+Extensions.swift new file mode 100644 index 0000000..40c9f30 --- /dev/null +++ b/Sources/Defaults/Defaults+Extensions.swift @@ -0,0 +1,135 @@ +import Foundation +import CoreGraphics +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +extension Defaults.Serializable { + public static var isNativelySupportedType: Bool { false } +} + +extension Data: Defaults.Serializable { + public static let isNativelySupportedType = true +} + +extension Date: Defaults.Serializable { + public static let isNativelySupportedType = true +} + +extension Bool: Defaults.Serializable { + public static let isNativelySupportedType = true +} + +extension Int: Defaults.Serializable { + public static let isNativelySupportedType = true +} + +extension UInt: Defaults.Serializable { + public static let isNativelySupportedType = true +} + +extension Double: Defaults.Serializable { + public static let isNativelySupportedType = true +} + +extension Float: Defaults.Serializable { + public static let isNativelySupportedType = true +} + +extension String: Defaults.Serializable { + public static let isNativelySupportedType = true +} + +extension CGFloat: Defaults.Serializable { + public static let isNativelySupportedType = true +} + +extension Int8: Defaults.Serializable { + public static let isNativelySupportedType = true +} + +extension UInt8: Defaults.Serializable { + public static let isNativelySupportedType = true +} + +extension Int16: Defaults.Serializable { + public static let isNativelySupportedType = true +} + +extension UInt16: Defaults.Serializable { + public static let isNativelySupportedType = true +} + +extension Int32: Defaults.Serializable { + public static let isNativelySupportedType = true +} + +extension UInt32: Defaults.Serializable { + public static let isNativelySupportedType = true +} + +extension Int64: Defaults.Serializable { + public static let isNativelySupportedType = true +} + +extension UInt64: Defaults.Serializable { + public static let isNativelySupportedType = true +} + +extension URL: Defaults.Serializable { + public static let bridge = Defaults.URLBridge() +} + +extension Defaults.Serializable where Self: Codable { + public static var bridge: Defaults.TopLevelCodableBridge { Defaults.TopLevelCodableBridge() } +} + +extension Defaults.Serializable where Self: RawRepresentable { + public static var bridge: Defaults.RawRepresentableBridge { Defaults.RawRepresentableBridge() } +} + +extension Defaults.Serializable where Self: RawRepresentable & Codable { + public static var bridge: Defaults.RawRepresentableCodableBridge { Defaults.RawRepresentableCodableBridge() } +} + +extension Defaults.Serializable where Self: NSSecureCoding { + public static var bridge: Defaults.NSSecureCodingBridge { Defaults.NSSecureCodingBridge() } +} + +extension Optional: Defaults.Serializable where Wrapped: Defaults.Serializable { + public static var isNativelySupportedType: Bool { Wrapped.isNativelySupportedType } + public static var bridge: Defaults.OptionalBridge { Defaults.OptionalBridge() } +} + +extension Defaults.CollectionSerializable where Element: Defaults.Serializable { + public static var bridge: Defaults.CollectionBridge { Defaults.CollectionBridge() } +} + +extension Defaults.SetAlgebraSerializable where Element: Defaults.Serializable & Hashable { + public static var bridge: Defaults.SetAlgebraBridge { Defaults.SetAlgebraBridge() } +} + +extension Set: Defaults.Serializable where Element: Defaults.Serializable { + public static var bridge: Defaults.SetBridge { Defaults.SetBridge() } +} + + +extension Array: Defaults.Serializable where Element: Defaults.Serializable { + public static var isNativelySupportedType: Bool { Element.isNativelySupportedType } + public static var bridge: Defaults.ArrayBridge { Defaults.ArrayBridge() } +} + +extension Dictionary: Defaults.Serializable where Key: LosslessStringConvertible & Hashable, Value: Defaults.Serializable { + public static var isNativelySupportedType: Bool { Value.isNativelySupportedType } + public static var bridge: Defaults.DictionaryBridge { Defaults.DictionaryBridge() } +} + +#if os(macOS) +/// `NSColor` conforms to `NSSecureCoding`, so it goes to `NSSecureCodingBridge` +extension NSColor: Defaults.Serializable {} +#else +/// `UIColor` conforms to `NSSecureCoding`, so it goes to `NSSecureCodingBridge` +extension UIColor: Defaults.Serializable {} +#endif diff --git a/Sources/Defaults/Defaults+Protocol.swift b/Sources/Defaults/Defaults+Protocol.swift new file mode 100644 index 0000000..9159697 --- /dev/null +++ b/Sources/Defaults/Defaults+Protocol.swift @@ -0,0 +1,93 @@ +import Foundation + +/** +All type that able to work with `Defaults` should conform this protocol. + +It should have a static variable bridge which protocol should conform to `Defaults.Bridge`. + +``` +struct User { + username: String + password: String +} + +extension User: Defaults.Serializable { + static let bridge = UserBridge() +} +``` +*/ +public protocol DefaultsSerializable { + typealias Value = Bridge.Value + typealias Serializable = Bridge.Serializable + associatedtype Bridge: DefaultsBridge + + /// Static bridge for the `Value` which cannot store natively + static var bridge: Bridge { get } + + /// A flag to determine whether `Value` can be store natively or not + static var isNativelySupportedType: Bool { get } +} + +/** +A Bridge can do the serialization and de-serialization. + +Have two associate types `Value` and `Serializable`. + +- `Value`: the type user want to use it. +- `Serializable`: the type stored in `UserDefaults`. +- `serialize`: will be executed before storing to the `UserDefaults` . +- `deserialize`: will be executed after retrieving its value from the `UserDefaults`. + +``` +struct User { + username: String + password: String +} + +struct UserBridge: Defaults.Bridge { + typealias Value = User + typealias Serializable = [String: String] + + func serialize(_ value: Value?) -> Serializable? { + guard let value = value else { + return nil + } + + return ["username": value.username, "password": value.password] + } + + func deserialize(_ object: Serializable?) -> Value? { + guard + let object = object, + let username = object["username"], + let password = object["password"] + else { + return nil + } + + return User(username: username, password: password) + } +} +``` +*/ +public protocol DefaultsBridge { + associatedtype Value + associatedtype Serializable + + func serialize(_ value: Value?) -> Serializable? + + func deserialize(_ object: Serializable?) -> Value? +} + +public protocol DefaultsCollectionSerializable: Collection, Defaults.Serializable { + /// `Collection` does not have initializer, but we need initializer to convert an array into the `Value` + init(_ elements: [Element]) +} + +public protocol DefaultsSetAlgebraSerializable: SetAlgebra, Defaults.Serializable { + /// Since `SetAlgebra` protocol does not conform to `Sequence`, we cannot convert a `SetAlgebra` to an `Array` directly. + func toArray() -> [Element] +} + +/// Convenience protocol for `Codable` +public protocol DefaultsCodableBridge: DefaultsBridge where Serializable == String, Value: Codable {} diff --git a/Sources/Defaults/Defaults.swift b/Sources/Defaults/Defaults.swift index 227c9bb..c88d28f 100644 --- a/Sources/Defaults/Defaults.swift +++ b/Sources/Defaults/Defaults.swift @@ -16,16 +16,15 @@ extension DefaultsBaseKey { public enum Defaults { public typealias BaseKey = DefaultsBaseKey public typealias AnyKey = Keys + public typealias Serializable = DefaultsSerializable + public typealias CollectionSerializable = DefaultsCollectionSerializable + public typealias SetAlgebraSerializable = DefaultsSetAlgebraSerializable + public typealias Bridge = DefaultsBridge + typealias CodableBridge = DefaultsCodableBridge public class Keys: BaseKey { public typealias Key = Defaults.Key - @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) - public typealias NSSecureCodingKey = Defaults.NSSecureCodingKey - - @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) - public typealias NSSecureCodingOptionalKey = Defaults.NSSecureCodingOptionalKey - public let name: String public let suite: UserDefaults @@ -35,7 +34,7 @@ public enum Defaults { } } - public final class Key: AnyKey { + public final class Key: AnyKey { public let defaultValue: Value /// Create a defaults key. @@ -49,67 +48,16 @@ public enum Defaults { return } - // Sets the default value in the actual UserDefaults, so it can be used in other contexts, like binding. - if UserDefaults.isNativelySupportedType(Value.self) { - suite.register(defaults: [key: defaultValue]) - } else if let value = suite._encode(defaultValue) { - suite.register(defaults: [key: value]) - } - } - } - - @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) - public final class NSSecureCodingKey: AnyKey { - public let defaultValue: Value - - /// Create a defaults key. - /// The `default` parameter can be left out if the `Value` type is an optional. - public init(_ key: String, default defaultValue: Value, suite: UserDefaults = .standard) { - self.defaultValue = defaultValue - - super.init(name: key, suite: suite) - - if (defaultValue as? _DefaultsOptionalType)?.isNil == true { + guard let serialized = Value.toSerializable(defaultValue) else { return } // Sets the default value in the actual UserDefaults, so it can be used in other contexts, like binding. - if UserDefaults.isNativelySupportedType(Value.self) { - suite.register(defaults: [key: defaultValue]) - } else if let value = try? NSKeyedArchiver.archivedData(withRootObject: defaultValue, requiringSecureCoding: true) { - suite.register(defaults: [key: value]) - } + suite.register(defaults: [self.name: serialized]) } } - @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) - public final class NSSecureCodingOptionalKey: AnyKey { - /// Create an optional defaults key. - public init(_ key: String, suite: UserDefaults = .standard) { - super.init(name: key, suite: suite) - } - } - - /// Access a defaults value using a `Defaults.Key`. - public static subscript(key: Key) -> Value { - get { key.suite[key] } - set { - key.suite[key] = newValue - } - } - - /// Access a defaults value using a `Defaults.NSSecureCodingKey`. - @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) - public static subscript(key: NSSecureCodingKey) -> Value { - get { key.suite[key] } - set { - key.suite[key] = newValue - } - } - - /// Access a defaults value using a `Defaults.NSSecureCodingOptionalKey`. - @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) - public static subscript(key: NSSecureCodingOptionalKey) -> Value? { + public static subscript(key: Key) -> Value { get { key.suite[key] } set { key.suite[key] = newValue @@ -129,14 +77,7 @@ extension Defaults { } extension Defaults.Key { - public convenience init(_ key: String, suite: UserDefaults = .standard) where Value == T? { - self.init(key, default: nil, suite: suite) - } -} - -@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) -extension Defaults.NSSecureCodingKey where Value: _DefaultsOptionalType { - public convenience init(_ key: String, suite: UserDefaults = .standard) { + public convenience init(_ key: String, suite: UserDefaults = .standard) where Value == T? { self.init(key, default: nil, suite: suite) } } diff --git a/Sources/Defaults/Migration/Migration+Defaults.swift b/Sources/Defaults/Migration/Migration+Defaults.swift new file mode 100644 index 0000000..1d463b8 --- /dev/null +++ b/Sources/Defaults/Migration/Migration+Defaults.swift @@ -0,0 +1,46 @@ +import Foundation + +extension Defaults { + public enum Version: Int { + case v5 = 5 + } + + /** + Migration the given key's value from json string to `Value`. + + ``` + extension Defaults.Keys { + static let array = Key?>("array") + } + + Defaults.migrate(.array, to: .v5) + ``` + */ + public static func migrate(_ keys: Key..., to version: Version) { + migrate(keys, to: version) + } + + public static func migrate(_ keys: Key..., to version: Version) { + migrate(keys, to: version) + } + + public static func migrate(_ keys: [Key], to version: Version) { + switch version { + case .v5: + for key in keys { + let suite = key.suite + suite.migrateCodableToNative(forKey: key.name, of: Value.self) + } + } + } + + public static func migrate(_ keys: [Key], to version: Version) { + switch version { + case .v5: + for key in keys { + let suite = key.suite + suite.migrateCodableToNative(forKey: key.name, of: Value.self) + } + } + } +} diff --git a/Sources/Defaults/Migration/v5/Migration+Extensions.swift b/Sources/Defaults/Migration/v5/Migration+Extensions.swift new file mode 100644 index 0000000..0e68560 --- /dev/null +++ b/Sources/Defaults/Migration/v5/Migration+Extensions.swift @@ -0,0 +1,251 @@ +import Foundation +import CoreGraphics + +extension Defaults { + public typealias NativeType = DefaultsNativeType + public typealias CodableType = DefaultsCodableType +} + +extension Data: Defaults.NativeType { + public typealias CodableForm = Self +} +extension Data: Defaults.CodableType { + public typealias NativeForm = Self + + public func toNative() -> Self { + self + } +} + +extension Date: Defaults.NativeType { + public typealias CodableForm = Self +} +extension Date: Defaults.CodableType { + public typealias NativeForm = Self + + public func toNative() -> Self { + self + } +} + +extension Bool: Defaults.NativeType { + public typealias CodableForm = Self +} +extension Bool: Defaults.CodableType { + public typealias NativeForm = Self + + public func toNative() -> Self { + self + } +} + +extension Int: Defaults.NativeType { + public typealias CodableForm = Self +} +extension Int: Defaults.CodableType { + public typealias NativeForm = Self + + public func toNative() -> Self { + self + } +} + +extension UInt: Defaults.NativeType { + public typealias CodableForm = Self +} +extension UInt: Defaults.CodableType { + public typealias NativeForm = Self + + public func toNative() -> Self { + self + } +} + +extension Double: Defaults.NativeType { + public typealias CodableForm = Self +} +extension Double: Defaults.CodableType { + public typealias NativeForm = Self + + public func toNative() -> Self { + self + } +} + +extension Float: Defaults.NativeType { + public typealias CodableForm = Self +} +extension Float: Defaults.CodableType { + public typealias NativeForm = Self + + public func toNative() -> Self { + self + } +} + +extension String: Defaults.NativeType { + public typealias CodableForm = Self +} +extension String: Defaults.CodableType { + public typealias NativeForm = Self + + public func toNative() -> Self { + self + } +} + +extension CGFloat: Defaults.NativeType { + public typealias CodableForm = Self +} +extension CGFloat: Defaults.CodableType { + public typealias NativeForm = Self + + public func toNative() -> Self { + self + } +} + +extension Int8: Defaults.NativeType { + public typealias CodableForm = Self +} +extension Int8: Defaults.CodableType { + public typealias NativeForm = Self + + public func toNative() -> Self { + self + } +} + +extension UInt8: Defaults.NativeType { + public typealias CodableForm = Self +} +extension UInt8: Defaults.CodableType { + public typealias NativeForm = Self + + public func toNative() -> Self { + self + } +} + +extension Int16: Defaults.NativeType { + public typealias CodableForm = Self +} +extension Int16: Defaults.CodableType { + public typealias NativeForm = Self + + public func toNative() -> Self { + self + } +} + +extension UInt16: Defaults.NativeType { + public typealias CodableForm = Self +} +extension UInt16: Defaults.CodableType { + public typealias NativeForm = Self + + public func toNative() -> Self { + self + } +} + +extension Int32: Defaults.NativeType { + public typealias CodableForm = Self +} +extension Int32: Defaults.CodableType { + public typealias NativeForm = Self + + public func toNative() -> Self { + self + } +} + +extension UInt32: Defaults.NativeType { + public typealias CodableForm = Self +} +extension UInt32: Defaults.CodableType { + public typealias NativeForm = Self + + public func toNative() -> Self { + self + } +} + +extension Int64: Defaults.NativeType { + public typealias CodableForm = Self +} +extension Int64: Defaults.CodableType { + public typealias NativeForm = Self + + public func toNative() -> Self { + self + } +} + +extension UInt64: Defaults.NativeType { + public typealias CodableForm = Self +} +extension UInt64: Defaults.CodableType { + public typealias NativeForm = Self + + public func toNative() -> Self { + self + } +} + +extension URL: Defaults.NativeType { + public typealias CodableForm = Self +} +extension URL: Defaults.CodableType { + public typealias NativeForm = Self + + public func toNative() -> Self { + self + } +} + +extension Optional: Defaults.NativeType where Wrapped: Defaults.NativeType { + public typealias CodableForm = Wrapped.CodableForm +} + +extension Defaults.CollectionSerializable where Self: Defaults.NativeType, Element: Defaults.NativeType { + public typealias CodableForm = [Element.CodableForm] +} + +extension Defaults.SetAlgebraSerializable where Self: Defaults.NativeType, Element: Defaults.NativeType { + public typealias CodableForm = [Element.CodableForm] +} + +extension Defaults.CodableType where Self: RawRepresentable, NativeForm: RawRepresentable, Self.RawValue == NativeForm.RawValue { + public func toNative() -> NativeForm { + NativeForm(rawValue: self.rawValue)! + } +} + +extension Set: Defaults.NativeType where Element: Defaults.NativeType { + public typealias CodableForm = [Element.CodableForm] +} + +extension Array: Defaults.NativeType where Element: Defaults.NativeType { + public typealias CodableForm = [Element.CodableForm] +} +extension Array: Defaults.CodableType where Element: Defaults.CodableType { + public typealias NativeForm = [Element.NativeForm] + + public func toNative() -> NativeForm { + map { $0.toNative() } + } +} + +extension Dictionary: Defaults.NativeType where Key: LosslessStringConvertible & Hashable, Value: Defaults.NativeType { + public typealias CodableForm = [String: Value.CodableForm] +} +extension Dictionary: Defaults.CodableType where Key == String, Value: Defaults.CodableType { + public typealias NativeForm = [String: Value.NativeForm] + + public func toNative() -> NativeForm { + reduce(into: NativeForm()) { memo, tuple in + memo[tuple.key] = tuple.value.toNative() + } + } +} diff --git a/Sources/Defaults/Migration/v5/Migration+Protocol.swift b/Sources/Defaults/Migration/v5/Migration+Protocol.swift new file mode 100644 index 0000000..0f9c891 --- /dev/null +++ b/Sources/Defaults/Migration/v5/Migration+Protocol.swift @@ -0,0 +1,62 @@ +import Foundation + +/** +Only for migration. + +Represents the type after migration and its protocol should conform to `Defaults.Serializable`. + +It should have an associated type name `CodableForm` which protocol conform to `Codable`. +So we can convert the json string into `NativeType` like this. +``` +guard + let jsonString = string, + let jsonData = jsonString.data(using: .utf8), + let codable = try? JSONDecoder().decode(NativeType.CodableForm.self, from: jsonData) +else { + return nil +} + +return codable.toNative() +``` +*/ +public protocol DefaultsNativeType: Defaults.Serializable { + associatedtype CodableForm: Defaults.CodableType +} + +/** +Only for migration. + +Represents the type before migration an its protocol should conform to `Codable`. + +The main purposed of `CodableType` is trying to infer the `Codable` type to do `JSONDecoder().decode` +It should have an associated type name `NativeForm` which is the type we want it to store in `UserDefaults`. +And it also have a `toNative()` function to convert itself into `NativeForm`. + +``` +struct User { + username: String + password: String +} + +struct CodableUser: Codable { + username: String + password: String +} + +extension User: Defaults.NativeType { + typealias CodableForm = CodableUser +} + +extension CodableUser: Defaults.CodableType { + typealias NativeForm = User + + func toNative() -> NativeForm { + User(username: self.username, password: self.password) + } +} +``` +*/ +public protocol DefaultsCodableType: Codable { + associatedtype NativeForm: Defaults.NativeType + func toNative() -> NativeForm +} diff --git a/Sources/Defaults/Migration/v5/Migration+UserDefaults.swift b/Sources/Defaults/Migration/v5/Migration+UserDefaults.swift new file mode 100644 index 0000000..fc2050b --- /dev/null +++ b/Sources/Defaults/Migration/v5/Migration+UserDefaults.swift @@ -0,0 +1,41 @@ +import Foundation + +extension UserDefaults { + func migrateCodableToNative(forKey key: String, of type: Value.Type) { + guard + let jsonString = string(forKey: key), + let jsonData = jsonString.data(using: .utf8), + let codable = try? JSONDecoder().decode(Value.self, from: jsonData) + else { + return + } + + _set(key, to: codable) + } + + /** + Get json string in `UserDefaults` and decode it into the `NativeForm`. + + How it works? + For example: + Step1. If `Value` is `[String]`, `Value.CodableForm` will covert into `[String].CodableForm`. + `JSONDecoder().decode([String].CodableForm.self, from: jsonData)` + + Step2. `Array`conform to `NativeType`, its `CodableForm` is `[Element.CodableForm]` and `Element` is `String`. + `JSONDecoder().decode([String.CodableForm].self, from: jsonData)` + + Step3. `String`'s `CodableForm` is `self`, because `String` is `Codable`. + `JSONDecoder().decode([String].self, from: jsonData)` + */ + func migrateCodableToNative(forKey key: String, of type: Value.Type) { + guard + let jsonString = string(forKey: key), + let jsonData = jsonString.data(using: .utf8), + let codable = try? JSONDecoder().decode(Value.CodableForm.self, from: jsonData) + else { + return + } + + _set(key, to: codable.toNative()) + } +} diff --git a/Sources/Defaults/Observation+Combine.swift b/Sources/Defaults/Observation+Combine.swift index ca485d2..f2acbc1 100644 --- a/Sources/Defaults/Observation+Combine.swift +++ b/Sources/Defaults/Observation+Combine.swift @@ -88,7 +88,7 @@ extension Defaults { ``` */ @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) - public static func publisher( + public static func publisher( _ key: Key, options: ObservationOptions = [.initial] ) -> AnyPublisher, Never> { @@ -98,34 +98,6 @@ extension Defaults { return AnyPublisher(publisher) } - /** - Returns a type-erased `Publisher` that publishes changes related to the given key. - */ - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) - public static func publisher( - _ key: NSSecureCodingKey, - options: ObservationOptions = [.initial] - ) -> AnyPublisher, Never> { - let publisher = DefaultsPublisher(suite: key.suite, key: key.name, options: options) - .map { NSSecureCodingKeyChange(change: $0, defaultValue: key.defaultValue) } - - return AnyPublisher(publisher) - } - - /** - Returns a type-erased `Publisher` that publishes changes related to the given optional key. - */ - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) - public static func publisher( - _ key: NSSecureCodingOptionalKey, - options: ObservationOptions = [.initial] - ) -> AnyPublisher, Never> { - let publisher = DefaultsPublisher(suite: key.suite, key: key.name, options: options) - .map { NSSecureCodingOptionalKeyChange(change: $0) } - - return AnyPublisher(publisher) - } - /** Publisher for multiple `Key` observation, but without specific information about changes. */ diff --git a/Sources/Defaults/Observation.swift b/Sources/Defaults/Observation.swift index 345e785..88e9a17 100644 --- a/Sources/Defaults/Observation.swift +++ b/Sources/Defaults/Observation.swift @@ -37,7 +37,7 @@ extension Defaults { public typealias ObservationOptions = Set - private static func deserialize(_ value: Any?, to type: Value.Type) -> Value? { + private static func deserialize(_ value: Any?, to type: Value.Type) -> Value? { guard let value = value, !(value is NSNull) @@ -45,34 +45,7 @@ extension Defaults { return nil } - // This handles the case where the value was a plist value using `isNativelySupportedType` - if let value = value as? Value { - return value - } - - // Using the array trick as done below in `UserDefaults#_set()` - return [Value].init(jsonString: "\([value])")?.first - } - - @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) - private static func deserialize(_ value: Any?, to type: Value.Type) -> Value? { - guard - let value = value, - !(value is NSNull) - else { - return nil - } - - // This handles the case where the value was a plist value using `isNativelySupportedType` - if let value = value as? Value { - return value - } - - guard let dataValue = value as? Data else { - return nil - } - - return try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(dataValue) as? Value + return Value.toValue(value) } struct BaseChange { @@ -91,7 +64,7 @@ extension Defaults { } } - public struct KeyChange { + public struct KeyChange { public let kind: NSKeyValueChange public let indexes: IndexSet? public let isPrior: Bool @@ -107,40 +80,6 @@ extension Defaults { } } - @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) - public struct NSSecureCodingKeyChange { - public let kind: NSKeyValueChange - public let indexes: IndexSet? - public let isPrior: Bool - public let newValue: Value - public let oldValue: Value - - init(change: BaseChange, defaultValue: Value) { - self.kind = change.kind - self.indexes = change.indexes - self.isPrior = change.isPrior - self.oldValue = deserialize(change.oldValue, to: Value.self) ?? defaultValue - self.newValue = deserialize(change.newValue, to: Value.self) ?? defaultValue - } - } - - @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) - public struct NSSecureCodingOptionalKeyChange { - public let kind: NSKeyValueChange - public let indexes: IndexSet? - public let isPrior: Bool - public let newValue: Value? - public let oldValue: Value? - - init(change: BaseChange) { - self.kind = change.kind - self.indexes = change.indexes - self.isPrior = change.isPrior - self.oldValue = deserialize(change.oldValue, to: Value.self) - self.newValue = deserialize(change.newValue, to: Value.self) - } - } - private static var preventPropagationThreadDictionaryKey: String { "\(type(of: Observation.self))_threadUpdatingValuesFlag" } @@ -242,7 +181,6 @@ extension Defaults { guard !updatingValuesFlag else { return } - callback(BaseChange(change: change)) } } @@ -352,7 +290,7 @@ extension Defaults { } ``` */ - public static func observe( + public static func observe( _ key: Key, options: ObservationOptions = [.initial], handler: @escaping (KeyChange) -> Void @@ -366,41 +304,6 @@ extension Defaults { return observation } - /** - Observe a defaults key. - */ - @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) - public static func observe( - _ key: NSSecureCodingKey, - options: ObservationOptions = [.initial], - handler: @escaping (NSSecureCodingKeyChange) -> Void - ) -> Observation { - let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in - handler( - NSSecureCodingKeyChange(change: change, defaultValue: key.defaultValue) - ) - } - observation.start(options: options) - return observation - } - - /** - Observe an optional defaults key. - */ - @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) - public static func observe( - _ key: NSSecureCodingOptionalKey, - options: ObservationOptions = [.initial], - handler: @escaping (NSSecureCodingOptionalKeyChange) -> Void - ) -> Observation { - let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in - handler( - NSSecureCodingOptionalKeyChange(change: change) - ) - } - observation.start(options: options) - return observation - } /** Observe multiple keys of any type, but without any information about the changes. @@ -447,10 +350,4 @@ extension Defaults.ObservationOptions { } } -@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) -extension Defaults.NSSecureCodingKeyChange: Equatable where Value: Equatable { } - -@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) -extension Defaults.NSSecureCodingOptionalKeyChange: Equatable where Value: Equatable { } - extension Defaults.KeyChange: Equatable where Value: Equatable { } diff --git a/Sources/Defaults/SwiftUI.swift b/Sources/Defaults/SwiftUI.swift index 743d2dc..6cc7405 100644 --- a/Sources/Defaults/SwiftUI.swift +++ b/Sources/Defaults/SwiftUI.swift @@ -4,7 +4,7 @@ import Combine @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) extension Defaults { - final class Observable: ObservableObject { + final class Observable: ObservableObject { let objectWillChange = ObservableObjectPublisher() private var observation: DefaultsObservation? private let key: Defaults.Key @@ -40,7 +40,7 @@ extension Defaults { @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) @propertyWrapper -public struct Default: DynamicProperty { +public struct Default: DynamicProperty { public typealias Publisher = AnyPublisher, Never> private let key: Defaults.Key diff --git a/Sources/Defaults/UserDefaults.swift b/Sources/Defaults/UserDefaults.swift index b033317..4a5db25 100644 --- a/Sources/Defaults/UserDefaults.swift +++ b/Sources/Defaults/UserDefaults.swift @@ -1,136 +1,29 @@ import Foundation extension UserDefaults { - private func _get(_ key: String) -> Value? { - if UserDefaults.isNativelySupportedType(Value.self) { - return object(forKey: key) as? Value - } - - guard - let text = string(forKey: key), - let data = "[\(text)]".data(using: .utf8) - else { + func _get(_ key: String) -> Value? { + guard let anyObject = object(forKey: key) else { return nil } - do { - return (try JSONDecoder().decode([Value].self, from: data)).first - } catch { - print(error) - } - - return nil + return Value.toValue(anyObject) } - @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) - private func _get(_ key: String) -> Value? { - if UserDefaults.isNativelySupportedType(Value.self) { - return object(forKey: key) as? Value - } - - guard - let data = data(forKey: key) - else { - return nil - } - - do { - return try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? Value - } catch { - print(error) - } - - return nil - } - - func _encode(_ value: Value) -> String? { - do { - // Some codable values like URL and enum are encoded as a top-level - // string which JSON can't handle, so we need to wrap it in an array - // We need this: https://forums.swift.org/t/allowing-top-level-fragments-in-jsondecoder/11750 - let data = try JSONEncoder().encode([value]) - return String(String(data: data, encoding: .utf8)!.dropFirst().dropLast()) - } catch { - print(error) - return nil - } - } - - private func _set(_ key: String, to value: Value) { + func _set(_ key: String, to value: Value) { if (value as? _DefaultsOptionalType)?.isNil == true { removeObject(forKey: key) return } - if UserDefaults.isNativelySupportedType(Value.self) { - set(value, forKey: key) - return - } - - set(_encode(value), forKey: key) + set(Value.toSerializable(value), forKey: key) } - @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) - private func _set(_ key: String, to value: Value) { - // TODO: Handle nil here too. - if UserDefaults.isNativelySupportedType(Value.self) { - set(value, forKey: key) - return - } - - set(try? NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: true), forKey: key) - } - - public subscript(key: Defaults.Key) -> Value { + public subscript(key: Defaults.Key) -> Value { get { _get(key.name) ?? key.defaultValue } set { _set(key.name, to: newValue) } } - - @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) - public subscript(key: Defaults.NSSecureCodingKey) -> Value { - get { _get(key.name) ?? key.defaultValue } - set { - _set(key.name, to: newValue) - } - } - - @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) - public subscript(key: Defaults.NSSecureCodingOptionalKey) -> Value? { - get { _get(key.name) } - set { - guard let value = newValue else { - set(nil, forKey: key.name) - return - } - - _set(key.name, to: value) - } - } - - static func isNativelySupportedType(_ type: T.Type) -> Bool { - switch type { - case - is Bool.Type, - is Bool?.Type, // swiftlint:disable:this discouraged_optional_boolean - is String.Type, - is String?.Type, - is Int.Type, - is Int?.Type, - is Double.Type, - is Double?.Type, - is Float.Type, - is Float?.Type, - is Date.Type, - is Date?.Type, - is Data.Type, - is Data?.Type: - return true - default: - return false - } - } } extension UserDefaults { diff --git a/Sources/Defaults/Utilities.swift b/Sources/Defaults/Utilities.swift index 0746b7a..c1a684c 100644 --- a/Sources/Defaults/Utilities.swift +++ b/Sources/Defaults/Utilities.swift @@ -146,3 +146,55 @@ extension DispatchQueue { } } } + +extension Sequence { + /// Returns an array containing the non-nil elements. + func compact() -> [T] where Element == T? { + // TODO: Make this `compactMap(\.self)` when https://bugs.swift.org/browse/SR-12897 is fixed. + compactMap { $0 } + } +} + +extension Defaults.Serializable { + /** + Cast `Serializable` value to `Self`. + Convert the native support type from `UserDefaults` into `Self`. + + ``` + guard let anyObject = object(forKey: key) else { + return nil + } + + return Value.toValue(anyObject) + ``` + */ + static func toValue(_ anyObject: Any) -> Self? { + // Return directly if `anyObject` can cast to Value, means `Value` is Native supported type. + if Self.isNativelySupportedType, let anyObject = anyObject as? Self { + return anyObject + } else if let value = Self.bridge.deserialize(anyObject as? Serializable) { + return value as? Self + } + + return nil + } + + /** + Cast `Self` to `Serializable`. + Convert `Self` into `UserDefaults` native support type. + + ``` + set(Value.toSerialize(value), forKey: key) + ``` + */ + static func toSerializable(_ value: Self) -> Any? { + // Return directly if `Self` is native supported type, since it does not need serialization. + if Self.isNativelySupportedType { + return value + } else if let serialized = Self.bridge.serialize(value as? Self.Value) { + return serialized + } + + return nil + } +} diff --git a/Tests/DefaultsTests/DefaultsArrayTests.swift b/Tests/DefaultsTests/DefaultsArrayTests.swift new file mode 100644 index 0000000..6761d3e --- /dev/null +++ b/Tests/DefaultsTests/DefaultsArrayTests.swift @@ -0,0 +1,178 @@ +import Foundation +import Defaults +import XCTest + +private let fixtureArray = ["Hank", "Chen"] + +extension Defaults.Keys { + fileprivate static let array = Key<[String]>("array", default: fixtureArray) +} + +final class DefaultsArrayTests: XCTestCase { + override func setUp() { + super.setUp() + Defaults.removeAll() + } + + override func tearDown() { + super.tearDown() + Defaults.removeAll() + } + + func testKey() { + let key = Defaults.Key<[String]>("independentArrayStringKey", default: fixtureArray) + XCTAssertEqual(Defaults[key][0], fixtureArray[0]) + let newValue = "John" + Defaults[key][0] = newValue + XCTAssertEqual(Defaults[key][0], newValue) + } + + func testOptionalKey() { + let key = Defaults.Key<[String]?>("independentArrayOptionalStringKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = fixtureArray + XCTAssertEqual(Defaults[key]?[0], fixtureArray[0]) + Defaults[key] = nil + XCTAssertNil(Defaults[key]) + let newValue = ["John", "Chen"] + Defaults[key] = newValue + XCTAssertEqual(Defaults[key]?[0], newValue[0]) + } + + func testNestedKey() { + let defaultValue = ["Hank", "Chen"] + let key = Defaults.Key<[[String]]>("independentArrayNestedKey", default: [defaultValue]) + XCTAssertEqual(Defaults[key][0][0], "Hank") + let newValue = ["Sindre", "Sorhus"] + Defaults[key][0] = newValue + Defaults[key].append(defaultValue) + XCTAssertEqual(Defaults[key][0][0], newValue[0]) + XCTAssertEqual(Defaults[key][0][1], newValue[1]) + XCTAssertEqual(Defaults[key][1][0], defaultValue[0]) + XCTAssertEqual(Defaults[key][1][1], defaultValue[1]) + } + + func testDictionaryKey() { + let defaultValue = ["0": "HankChen"] + let key = Defaults.Key<[[String: String]]>("independentArrayDictionaryKey", default: [defaultValue]) + XCTAssertEqual(Defaults[key][0]["0"], defaultValue["0"]) + let newValue = ["0": "SindreSorhus"] + Defaults[key][0] = newValue + Defaults[key].append(defaultValue) + XCTAssertEqual(Defaults[key][0]["0"], newValue["0"]) + XCTAssertEqual(Defaults[key][1]["0"], defaultValue["0"]) + } + + func testNestedDictionaryKey() { + let defaultValue = ["0": [["0": 0]]] + let key = Defaults.Key<[[String: [[String: Int]]]]>("independentArrayNestedDictionaryKey", default: [defaultValue]) + XCTAssertEqual(Defaults[key][0]["0"]![0]["0"], 0) + let newValue = 1 + Defaults[key][0]["0"]![0]["0"] = newValue + Defaults[key].append(defaultValue) + XCTAssertEqual(Defaults[key][1]["0"]![0]["0"], 0) + XCTAssertEqual(Defaults[key][0]["0"]![0]["0"], newValue) + } + + func testType() { + XCTAssertEqual(Defaults[.array][0], fixtureArray[0]) + let newName = "Hank121314" + Defaults[.array][0] = newName + XCTAssertEqual(Defaults[.array][0], newName) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveKeyCombine() { + let key = Defaults.Key<[String]>("observeArrayKeyCombine", default: fixtureArray) + let newName = "Chen" + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let cancellable = publisher.sink { tuples in + for (index, expected) in [(fixtureArray[0], newName), (newName, fixtureArray[0])].enumerated() { + XCTAssertEqual(expected.0, tuples[index].0[0]) + XCTAssertEqual(expected.1, tuples[index].1[0]) + } + + expect.fulfill() + } + + Defaults[key][0] = newName + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveOptionalKeyCombine() { + let key = Defaults.Key<[String]?>("observeArrayOptionalKeyCombine") + let newName = ["Chen"] + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(3) + + // swiftlint:disable discouraged_optional_collection + let expectedValues: [([String]?, [String]?)] = [(nil, fixtureArray), (fixtureArray, newName), (newName, nil)] + + let cancellable = publisher.sink { actualValues in + for (expected, actual) in zip(expectedValues, actualValues) { + XCTAssertEqual(expected.0, actual.0) + XCTAssertEqual(expected.1, actual.1) + } + + expect.fulfill() + } + + Defaults[key] = fixtureArray + Defaults[key] = newName + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + func testObserveKey() { + let key = Defaults.Key<[String]>("observeArrayKey", default: fixtureArray) + let newName = "John" + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue, fixtureArray) + XCTAssertEqual(change.newValue, [fixtureArray[0], newName]) + observation.invalidate() + expect.fulfill() + } + + Defaults[key][1] = newName + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveOptionalKey() { + let key = Defaults.Key<[String]?>("observeArrayOptionalKey") + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertNil(change.oldValue) + XCTAssertEqual(change.newValue!, fixtureArray) + observation.invalidate() + expect.fulfill() + } + + Defaults[key] = fixtureArray + observation.invalidate() + + waitForExpectations(timeout: 10) + } +} diff --git a/Tests/DefaultsTests/DefaultsCodableEnumTests.swift b/Tests/DefaultsTests/DefaultsCodableEnumTests.swift new file mode 100644 index 0000000..18cf3f1 --- /dev/null +++ b/Tests/DefaultsTests/DefaultsCodableEnumTests.swift @@ -0,0 +1,305 @@ +import Foundation +import Defaults +import XCTest + +private enum FixtureCodableEnum: String, Defaults.Serializable & Codable & Hashable { + case tenMinutes = "10 Minutes" + case halfHour = "30 Minutes" + case oneHour = "1 Hour" +} + +extension Defaults.Keys { + fileprivate static let codableEnum = Key("codable_enum", default: .oneHour) + fileprivate static let codableEnumArray = Key<[FixtureCodableEnum]>("codable_enum", default: [.oneHour]) + fileprivate static let codableEnumDictionary = Key<[String: FixtureCodableEnum]>("codable_enum", default: ["0": .oneHour]) +} + +final class DefaultsCodableEnumTests: XCTestCase { + override func setUp() { + super.setUp() + Defaults.removeAll() + } + + override func tearDown() { + super.tearDown() + Defaults.removeAll() + } + + func testKey() { + let key = Defaults.Key("independentCodableEnumKey", default: .tenMinutes) + XCTAssertEqual(Defaults[key], .tenMinutes) + Defaults[key] = .halfHour + XCTAssertEqual(Defaults[key], .halfHour) + } + + func testOptionalKey() { + let key = Defaults.Key("independentCodableEnumOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = .tenMinutes + XCTAssertEqual(Defaults[key], .tenMinutes) + } + + func testArrayKey() { + let key = Defaults.Key<[FixtureCodableEnum]>("independentCodableEnumArrayKey", default: [.tenMinutes]) + XCTAssertEqual(Defaults[key][0], .tenMinutes) + Defaults[key][0] = .halfHour + XCTAssertEqual(Defaults[key][0], .halfHour) + } + + func testArrayOptionalKey() { + let key = Defaults.Key<[FixtureCodableEnum]?>("independentCodableEnumArrayOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = [.halfHour] + } + + func testNestedArrayKey() { + let key = Defaults.Key<[[FixtureCodableEnum]]>("independentCodableEnumNestedArrayKey", default: [[.tenMinutes]]) + XCTAssertEqual(Defaults[key][0][0], .tenMinutes) + Defaults[key].append([.halfHour]) + Defaults[key][0].append(.oneHour) + XCTAssertEqual(Defaults[key][1][0], .halfHour) + XCTAssertEqual(Defaults[key][0][1], .oneHour) + } + + func testArrayDictionaryKey() { + let key = Defaults.Key<[[String: FixtureCodableEnum]]>("independentCodableEnumArrayDictionaryKey", default: [["0": .tenMinutes]]) + XCTAssertEqual(Defaults[key][0]["0"], .tenMinutes) + Defaults[key][0]["1"] = .halfHour + Defaults[key].append(["0": .oneHour]) + XCTAssertEqual(Defaults[key][0]["1"], .halfHour) + XCTAssertEqual(Defaults[key][1]["0"], .oneHour) + } + + func testDictionaryKey() { + let key = Defaults.Key<[String: FixtureCodableEnum]>("independentCodableEnumDictionaryKey", default: ["0": .tenMinutes]) + XCTAssertEqual(Defaults[key]["0"], .tenMinutes) + Defaults[key]["1"] = .halfHour + Defaults[key]["0"] = .oneHour + XCTAssertEqual(Defaults[key]["0"], .oneHour) + XCTAssertEqual(Defaults[key]["1"], .halfHour) + } + + func testDictionaryOptionalKey() { + let key = Defaults.Key<[String: FixtureCodableEnum]?>("independentCodableEnumDictionaryOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = ["0": .tenMinutes] + Defaults[key]?["1"] = .halfHour + XCTAssertEqual(Defaults[key]?["0"], .tenMinutes) + XCTAssertEqual(Defaults[key]?["1"], .halfHour) + } + + func testDictionaryArrayKey() { + let key = Defaults.Key<[String: [FixtureCodableEnum]]>("independentCodableEnumDictionaryArrayKey", default: ["0": [.tenMinutes]]) + XCTAssertEqual(Defaults[key]["0"]?[0], .tenMinutes) + Defaults[key]["0"]?.append(.halfHour) + Defaults[key]["1"] = [.oneHour] + XCTAssertEqual(Defaults[key]["0"]?[0], .tenMinutes) + XCTAssertEqual(Defaults[key]["0"]?[1], .halfHour) + XCTAssertEqual(Defaults[key]["1"]?[0], .oneHour) + } + + func testType() { + XCTAssertEqual(Defaults[.codableEnum], .oneHour) + Defaults[.codableEnum] = .tenMinutes + XCTAssertEqual(Defaults[.codableEnum], .tenMinutes) + } + + func testArrayType() { + XCTAssertEqual(Defaults[.codableEnumArray][0], .oneHour) + Defaults[.codableEnumArray].append(.halfHour) + XCTAssertEqual(Defaults[.codableEnumArray][0], .oneHour) + XCTAssertEqual(Defaults[.codableEnumArray][1], .halfHour) + } + + func testDictionaryType() { + XCTAssertEqual(Defaults[.codableEnumDictionary]["0"], .oneHour) + Defaults[.codableEnumDictionary]["1"] = .halfHour + XCTAssertEqual(Defaults[.codableEnumDictionary]["0"], .oneHour) + XCTAssertEqual(Defaults[.codableEnumDictionary]["1"], .halfHour) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveKeyCombine() { + let key = Defaults.Key("observeCodableEnumKeyCombine", default: .tenMinutes) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let expectedValue: [(FixtureCodableEnum, FixtureCodableEnum)] = [(.tenMinutes, .oneHour), (.oneHour, .tenMinutes)] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0, tuples[index].0) + XCTAssertEqual(expected.1, tuples[index].1) + } + + expect.fulfill() + } + + Defaults[key] = .oneHour + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveOptionalKeyCombine() { + let key = Defaults.Key("observeCodableEnumOptionalKeyCombine") + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(3) + + let expectedValue: [(FixtureCodableEnum?, FixtureCodableEnum?)] = [(nil, .tenMinutes), (.tenMinutes, .halfHour), (.halfHour, nil)] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0, tuples[index].0) + XCTAssertEqual(expected.1, tuples[index].1) + } + + expect.fulfill() + } + + Defaults[key] = .tenMinutes + Defaults[key] = .halfHour + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveArrayKeyCombine() { + let key = Defaults.Key<[FixtureCodableEnum]>("observeCodableEnumArrayKeyCombine", default: [.tenMinutes]) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let expectedValue: [(FixtureCodableEnum?, FixtureCodableEnum?)] = [(.tenMinutes, .halfHour), (.halfHour, .tenMinutes)] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0, tuples[index].0[0]) + XCTAssertEqual(expected.1, tuples[index].1[0]) + } + + expect.fulfill() + } + + Defaults[key][0] = .halfHour + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveDictionaryKeyCombine() { + let key = Defaults.Key<[String: FixtureCodableEnum]>("observeCodableEnumDictionaryKeyCombine", default: ["0": .tenMinutes]) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let expectedValue: [(FixtureCodableEnum?, FixtureCodableEnum?)] = [(.tenMinutes, .halfHour), (.halfHour, .tenMinutes)] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0, tuples[index].0["0"]) + XCTAssertEqual(expected.1, tuples[index].1["0"]) + } + + expect.fulfill() + } + + Defaults[key]["0"] = .halfHour + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + func testObserveKey() { + let key = Defaults.Key("observeCodableEnumKey", default: .tenMinutes) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue, .tenMinutes) + XCTAssertEqual(change.newValue, .halfHour) + observation.invalidate() + expect.fulfill() + } + + Defaults[key] = .halfHour + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveOptionalKey() { + let key = Defaults.Key("observeCodableEnumOptionalKey") + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertNil(change.oldValue) + XCTAssertEqual(change.newValue, .halfHour) + observation.invalidate() + expect.fulfill() + } + + Defaults[key] = .halfHour + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveArrayKey() { + let key = Defaults.Key<[FixtureCodableEnum]>("observeCodableEnumArrayKey", default: [.tenMinutes]) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue[0], .tenMinutes) + XCTAssertEqual(change.newValue[1], .halfHour) + observation.invalidate() + expect.fulfill() + } + + Defaults[key].append(.halfHour) + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveDictionaryKey() { + let key = Defaults.Key<[String: FixtureCodableEnum]>("observeCodableEnumDictionaryKey", default: ["0": .tenMinutes]) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue["0"], .tenMinutes) + XCTAssertEqual(change.newValue["1"], .halfHour) + observation.invalidate() + expect.fulfill() + } + + Defaults[key]["1"] = .halfHour + observation.invalidate() + + waitForExpectations(timeout: 10) + } +} diff --git a/Tests/DefaultsTests/DefaultsCodableTests.swift b/Tests/DefaultsTests/DefaultsCodableTests.swift new file mode 100644 index 0000000..cd98bb6 --- /dev/null +++ b/Tests/DefaultsTests/DefaultsCodableTests.swift @@ -0,0 +1,300 @@ +import Foundation +import XCTest +import Defaults + +private struct Unicorn: Codable, Defaults.Serializable { + var isUnicorn: Bool +} + +private let fixtureCodable = Unicorn(isUnicorn: true) + +extension Defaults.Keys { + fileprivate static let codable = Key("codable", default: fixtureCodable) + fileprivate static let codableArray = Key<[Unicorn]>("codable", default: [fixtureCodable]) + fileprivate static let codableDictionary = Key<[String: Unicorn]>("codable", default: ["0": fixtureCodable]) +} + +final class DefaultsCodableTests: XCTestCase { + override func setUp() { + super.setUp() + Defaults.removeAll() + } + + override func tearDown() { + super.tearDown() + Defaults.removeAll() + } + + func testKey() { + let key = Defaults.Key("independentCodableKey", default: fixtureCodable) + XCTAssertTrue(Defaults[key].isUnicorn) + Defaults[key].isUnicorn = false + XCTAssertFalse(Defaults[key].isUnicorn) + } + + func testOptionalKey() { + let key = Defaults.Key("independentCodableOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = Unicorn(isUnicorn: true) + XCTAssertTrue(Defaults[key]?.isUnicorn ?? false) + } + + func testArrayKey() { + let key = Defaults.Key<[Unicorn]>("independentCodableArrayKey", default: [fixtureCodable]) + XCTAssertTrue(Defaults[key][0].isUnicorn) + Defaults[key].append(Unicorn(isUnicorn: false)) + XCTAssertTrue(Defaults[key][0].isUnicorn) + XCTAssertFalse(Defaults[key][1].isUnicorn) + } + + func testArrayOptionalKey() { + let key = Defaults.Key<[Unicorn]?>("independentCodableArrayOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = [fixtureCodable] + Defaults[key]?.append(Unicorn(isUnicorn: false)) + XCTAssertTrue(Defaults[key]?[0].isUnicorn ?? false) + XCTAssertFalse(Defaults[key]?[1].isUnicorn ?? false) + } + + func testNestedArrayKey() { + let key = Defaults.Key<[[Unicorn]]>("independentCodableNestedArrayKey", default: [[fixtureCodable]]) + XCTAssertTrue(Defaults[key][0][0].isUnicorn) + Defaults[key].append([fixtureCodable]) + Defaults[key][0].append(Unicorn(isUnicorn: false)) + XCTAssertTrue(Defaults[key][0][0].isUnicorn) + XCTAssertTrue(Defaults[key][1][0].isUnicorn) + XCTAssertFalse(Defaults[key][0][1].isUnicorn) + } + + func testArrayDictionaryKey() { + let key = Defaults.Key<[[String: Unicorn]]>("independentCodableArrayDictionaryKey", default: [["0": fixtureCodable]]) + XCTAssertTrue(Defaults[key][0]["0"]?.isUnicorn ?? false) + Defaults[key].append(["0": fixtureCodable]) + Defaults[key][0]["1"] = Unicorn(isUnicorn: false) + XCTAssertTrue(Defaults[key][0]["0"]?.isUnicorn ?? false) + XCTAssertTrue(Defaults[key][1]["0"]?.isUnicorn ?? false) + XCTAssertFalse(Defaults[key][0]["1"]?.isUnicorn ?? true) + } + + func testDictionaryKey() { + let key = Defaults.Key<[String: Unicorn]>("independentCodableDictionaryKey", default: ["0": fixtureCodable]) + XCTAssertTrue(Defaults[key]["0"]?.isUnicorn ?? false) + Defaults[key]["1"] = Unicorn(isUnicorn: false) + XCTAssertTrue(Defaults[key]["0"]?.isUnicorn ?? false) + XCTAssertFalse(Defaults[key]["1"]?.isUnicorn ?? true) + } + + func testDictionaryOptionalKey() { + let key = Defaults.Key<[String: Unicorn]?>("independentCodableDictionaryOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = ["0": fixtureCodable] + Defaults[key]?["1"] = Unicorn(isUnicorn: false) + XCTAssertTrue(Defaults[key]?["0"]?.isUnicorn ?? false) + XCTAssertFalse(Defaults[key]?["1"]?.isUnicorn ?? true) + } + + func testDictionaryArrayKey() { + let key = Defaults.Key<[String: [Unicorn]]>("independentCodableDictionaryArrayKey", default: ["0": [fixtureCodable]]) + XCTAssertTrue(Defaults[key]["0"]?[0].isUnicorn ?? false) + Defaults[key]["1"] = [fixtureCodable] + Defaults[key]["0"]?.append(Unicorn(isUnicorn: false)) + XCTAssertTrue(Defaults[key]["1"]?[0].isUnicorn ?? false) + XCTAssertFalse(Defaults[key]["0"]?[1].isUnicorn ?? true) + } + + func testType() { + XCTAssertTrue(Defaults[.codable].isUnicorn) + Defaults[.codable] = Unicorn(isUnicorn: false) + XCTAssertFalse(Defaults[.codable].isUnicorn) + } + + func testArrayType() { + XCTAssertTrue(Defaults[.codableArray][0].isUnicorn) + Defaults[.codableArray][0] = Unicorn(isUnicorn: false) + XCTAssertFalse(Defaults[.codableArray][0].isUnicorn) + } + + func testDictionaryType() { + XCTAssertTrue(Defaults[.codableDictionary]["0"]?.isUnicorn ?? false) + Defaults[.codableDictionary]["0"] = Unicorn(isUnicorn: false) + XCTAssertFalse(Defaults[.codableDictionary]["0"]?.isUnicorn ?? true) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveKeyCombine() { + let key = Defaults.Key("observeCodableKeyCombine", default: fixtureCodable) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue.isUnicorn, $0.newValue.isUnicorn) } + .collect(2) + + let cancellable = publisher.sink { tuples in + for (index, expected) in [(true, false), (false, true)].enumerated() { + XCTAssertEqual(expected.0, tuples[index].0) + XCTAssertEqual(expected.1, tuples[index].1) + } + + expect.fulfill() + } + + Defaults[key] = Unicorn(isUnicorn: false) + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveOptionalKeyCombine() { + let key = Defaults.Key("observeCodableOptionalKeyCombine") + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue?.isUnicorn, $0.newValue?.isUnicorn) } + .collect(2) + + let expectedValue: [(Bool?, Bool?)] = [(nil, true), (true, nil)] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0, tuples[index].0) + XCTAssertEqual(expected.1, tuples[index].1) + } + + expect.fulfill() + } + + Defaults[key] = fixtureCodable + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveArrayKeyCombine() { + let key = Defaults.Key<[Unicorn]>("observeCodableArrayKeyCombine", default: [fixtureCodable]) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let cancellable = publisher.sink { tuples in + for (index, expected) in [(true, false), (false, true)].enumerated() { + XCTAssertEqual(expected.0, tuples[index].0[0].isUnicorn) + XCTAssertEqual(expected.1, tuples[index].1[0].isUnicorn) + } + + expect.fulfill() + } + + Defaults[key][0] = Unicorn(isUnicorn: false) + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveDictionaryKeyCombine() { + let key = Defaults.Key<[String: Unicorn]>("observeCodableDictionaryKeyCombine", default: ["0": fixtureCodable]) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let cancellable = publisher.sink { tuples in + for (index, expected) in [(true, false), (false, true)].enumerated() { + XCTAssertEqual(expected.0, tuples[index].0["0"]?.isUnicorn) + XCTAssertEqual(expected.1, tuples[index].1["0"]?.isUnicorn) + } + + expect.fulfill() + } + + Defaults[key]["0"] = Unicorn(isUnicorn: false) + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + func testObserveKey() { + let key = Defaults.Key("observeCodableKey", default: fixtureCodable) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertTrue(change.oldValue.isUnicorn) + XCTAssertFalse(change.newValue.isUnicorn) + observation.invalidate() + expect.fulfill() + } + + Defaults[key] = Unicorn(isUnicorn: false) + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveOptionalKey() { + let key = Defaults.Key("observeCodableOptionalKey") + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertNil(change.oldValue) + XCTAssertTrue(change.newValue?.isUnicorn ?? false) + observation.invalidate() + expect.fulfill() + } + + Defaults[key] = fixtureCodable + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveArrayKey() { + let key = Defaults.Key<[Unicorn]>("observeCodableArrayKey", default: [fixtureCodable]) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertTrue(change.oldValue[0].isUnicorn) + XCTAssertFalse(change.newValue[0].isUnicorn) + observation.invalidate() + expect.fulfill() + } + + Defaults[key][0] = Unicorn(isUnicorn: false) + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveDictionaryKey() { + let key = Defaults.Key<[String: Unicorn]>("observeCodableDictionaryKey", default: ["0": fixtureCodable]) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertTrue(change.oldValue["0"]?.isUnicorn ?? false) + XCTAssertFalse(change.newValue["0"]?.isUnicorn ?? true) + observation.invalidate() + expect.fulfill() + } + + Defaults[key]["0"] = Unicorn(isUnicorn: false) + observation.invalidate() + + waitForExpectations(timeout: 10) + } +} diff --git a/Tests/DefaultsTests/DefaultsCollectionCustomElementTests.swift b/Tests/DefaultsTests/DefaultsCollectionCustomElementTests.swift new file mode 100644 index 0000000..c40dbd2 --- /dev/null +++ b/Tests/DefaultsTests/DefaultsCollectionCustomElementTests.swift @@ -0,0 +1,351 @@ +import Foundation +import XCTest +import Defaults + +private struct Item: Equatable { + let name: String + let count: UInt +} + +extension Item: Defaults.Serializable { + static let bridge = ItemBridge() +} + +private struct ItemBridge: Defaults.Bridge { + typealias Value = Item + typealias Serializable = [String: String] + func serialize(_ value: Value?) -> Serializable? { + guard let value = value else { + return nil + } + + return ["name": value.name, "count": String(value.count)] + } + + func deserialize(_ object: Serializable?) -> Value? { + guard + let object = object, + let name = object["name"], + let count = UInt(object["count"] ?? "0") + else { + return nil + } + + return Value(name: name, count: count) + } +} + +private let fixtureCustomCollection = Item(name: "Apple", count: 10) +private let fixtureCustomCollection1 = Item(name: "Banana", count: 20) +private let fixtureCustomCollection2 = Item(name: "Grape", count: 30) + +extension Defaults.Keys { + fileprivate static let collectionCustomElement = Key>("collectionCustomElement", default: .init(items: [fixtureCustomCollection])) + fileprivate static let collectionCustomElementArray = Key<[Bag]>("collectionCustomElementArray", default: [.init(items: [fixtureCustomCollection])]) + fileprivate static let collectionCustomElementDictionary = Key<[String: Bag]>("collectionCustomElementDictionary", default: ["0": .init(items: [fixtureCustomCollection])]) +} + +final class DefaultsCollectionCustomElementTests: XCTestCase { + override func setUp() { + super.setUp() + Defaults.removeAll() + } + + override func tearDown() { + super.tearDown() + Defaults.removeAll() + } + + func testKey() { + let key = Defaults.Key>("independentCollectionCustomElementKey", default: .init(items: [fixtureCustomCollection])) + Defaults[key].insert(element: fixtureCustomCollection1, at: 1) + Defaults[key].insert(element: fixtureCustomCollection2, at: 2) + XCTAssertEqual(Defaults[key][0], fixtureCustomCollection) + XCTAssertEqual(Defaults[key][1], fixtureCustomCollection1) + XCTAssertEqual(Defaults[key][2], fixtureCustomCollection2) + } + + func testOptionalKey() { + let key = Defaults.Key?>("independentCollectionCustomElementOptionalKey") + Defaults[key] = .init(items: [fixtureCustomCollection]) + Defaults[key]?.insert(element: fixtureCustomCollection1, at: 1) + Defaults[key]?.insert(element: fixtureCustomCollection2, at: 2) + XCTAssertEqual(Defaults[key]?[0], fixtureCustomCollection) + XCTAssertEqual(Defaults[key]?[1], fixtureCustomCollection1) + XCTAssertEqual(Defaults[key]?[2], fixtureCustomCollection2) + } + + func testArrayKey() { + let key = Defaults.Key<[Bag]>("independentCollectionCustomElementArrayKey", default: [.init(items: [fixtureCustomCollection])]) + Defaults[key][0].insert(element: fixtureCustomCollection1, at: 1) + Defaults[key].append(.init(items: [fixtureCustomCollection2])) + XCTAssertEqual(Defaults[key][0][0], fixtureCustomCollection) + XCTAssertEqual(Defaults[key][0][1], fixtureCustomCollection1) + XCTAssertEqual(Defaults[key][1][0], fixtureCustomCollection2) + } + + func testArrayOptionalKey() { + let key = Defaults.Key<[Bag]?>("independentCollectionCustomElementArrayOptionalKey") + Defaults[key] = [.init(items: [fixtureCustomCollection])] + Defaults[key]?[0].insert(element: fixtureCustomCollection1, at: 1) + Defaults[key]?.append(Bag(items: [fixtureCustomCollection2])) + XCTAssertEqual(Defaults[key]?[0][0], fixtureCustomCollection) + XCTAssertEqual(Defaults[key]?[0][1], fixtureCustomCollection1) + XCTAssertEqual(Defaults[key]?[1][0], fixtureCustomCollection2) + } + + func testNestedArrayKey() { + let key = Defaults.Key<[[Bag]]>("independentCollectionCustomElementNestedArrayKey", default: [[.init(items: [fixtureCustomCollection])]]) + Defaults[key][0][0].insert(element: fixtureCustomCollection, at: 1) + Defaults[key][0].append(.init(items: [fixtureCustomCollection1])) + Defaults[key].append([.init(items: [fixtureCustomCollection2])]) + XCTAssertEqual(Defaults[key][0][0][0], fixtureCustomCollection) + XCTAssertEqual(Defaults[key][0][0][1], fixtureCustomCollection) + XCTAssertEqual(Defaults[key][0][1][0], fixtureCustomCollection1) + XCTAssertEqual(Defaults[key][1][0][0], fixtureCustomCollection2) + } + + func testArrayDictionaryKey() { + let key = Defaults.Key<[[String: Bag]]>("independentCollectionCustomElementArrayDictionaryKey", default: [["0": .init(items: [fixtureCustomCollection])]]) + Defaults[key][0]["0"]?.insert(element: fixtureCustomCollection, at: 1) + Defaults[key][0]["1"] = .init(items: [fixtureCustomCollection1]) + Defaults[key].append(["0": .init(items: [fixtureCustomCollection2])]) + XCTAssertEqual(Defaults[key][0]["0"]?[0], fixtureCustomCollection) + XCTAssertEqual(Defaults[key][0]["0"]?[1], fixtureCustomCollection) + XCTAssertEqual(Defaults[key][0]["1"]?[0], fixtureCustomCollection1) + XCTAssertEqual(Defaults[key][1]["0"]?[0], fixtureCustomCollection2) + } + + func testDictionaryKey() { + let key = Defaults.Key<[String: Bag]>("independentCollectionCustomElementDictionaryKey", default: ["0": .init(items: [fixtureCustomCollection])]) + Defaults[key]["0"]?.insert(element: fixtureCustomCollection1, at: 1) + Defaults[key]["1"] = .init(items: [fixtureCustomCollection2]) + XCTAssertEqual(Defaults[key]["0"]?[0], fixtureCustomCollection) + XCTAssertEqual(Defaults[key]["0"]?[1], fixtureCustomCollection1) + XCTAssertEqual(Defaults[key]["1"]?[0], fixtureCustomCollection2) + } + + func testDictionaryOptionalKey() { + let key = Defaults.Key<[String: Bag]?>("independentCollectionCustomElementDictionaryOptionalKey") + Defaults[key] = ["0": .init(items: [fixtureCustomCollection])] + Defaults[key]?["0"]?.insert(element: fixtureCustomCollection1, at: 1) + Defaults[key]?["1"] = .init(items: [fixtureCustomCollection2]) + XCTAssertEqual(Defaults[key]?["0"]?[0], fixtureCustomCollection) + XCTAssertEqual(Defaults[key]?["0"]?[1], fixtureCustomCollection1) + XCTAssertEqual(Defaults[key]?["1"]?[0], fixtureCustomCollection2) + } + + func testDictionaryArrayKey() { + let key = Defaults.Key<[String: [Bag]]>("independentCollectionCustomElementDictionaryArrayKey", default: ["0": [.init(items: [fixtureCustomCollection])]]) + Defaults[key]["0"]?[0].insert(element: fixtureCustomCollection, at: 1) + Defaults[key]["0"]?.append(.init(items: [fixtureCustomCollection1])) + Defaults[key]["1"] = [.init(items: [fixtureCustomCollection2])] + XCTAssertEqual(Defaults[key]["0"]?[0][0], fixtureCustomCollection) + XCTAssertEqual(Defaults[key]["0"]?[0][1], fixtureCustomCollection) + XCTAssertEqual(Defaults[key]["0"]?[1][0], fixtureCustomCollection1) + XCTAssertEqual(Defaults[key]["1"]?[0][0], fixtureCustomCollection2) + } + + func testType() { + Defaults[.collectionCustomElement].insert(element: fixtureCustomCollection1, at: 1) + Defaults[.collectionCustomElement].insert(element: fixtureCustomCollection2, at: 2) + XCTAssertEqual(Defaults[.collectionCustomElement][0], fixtureCustomCollection) + XCTAssertEqual(Defaults[.collectionCustomElement][1], fixtureCustomCollection1) + XCTAssertEqual(Defaults[.collectionCustomElement][2], fixtureCustomCollection2) + } + + func testArrayType() { + Defaults[.collectionCustomElementArray][0].insert(element: fixtureCustomCollection1, at: 1) + Defaults[.collectionCustomElementArray].append(.init(items: [fixtureCustomCollection2])) + XCTAssertEqual(Defaults[.collectionCustomElementArray][0][0], fixtureCustomCollection) + XCTAssertEqual(Defaults[.collectionCustomElementArray][0][1], fixtureCustomCollection1) + XCTAssertEqual(Defaults[.collectionCustomElementArray][1][0], fixtureCustomCollection2) + } + + func testDictionaryType() { + Defaults[.collectionCustomElementDictionary]["0"]?.insert(element: fixtureCustomCollection1, at: 1) + Defaults[.collectionCustomElementDictionary]["1"] = .init(items: [fixtureCustomCollection2]) + XCTAssertEqual(Defaults[.collectionCustomElementDictionary]["0"]?[0], fixtureCustomCollection) + XCTAssertEqual(Defaults[.collectionCustomElementDictionary]["0"]?[1], fixtureCustomCollection1) + XCTAssertEqual(Defaults[.collectionCustomElementDictionary]["1"]?[0], fixtureCustomCollection2) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveKeyCombine() { + let key = Defaults.Key>("observeCollectionCustomElementKeyCombine", default: .init(items: [fixtureCustomCollection])) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let cancellable = publisher.sink { tuples in + for (index, expected) in [(fixtureCustomCollection, fixtureCustomCollection1), (fixtureCustomCollection1, fixtureCustomCollection)].enumerated() { + XCTAssertEqual(expected.0, tuples[index].0[0]) + XCTAssertEqual(expected.1, tuples[index].1[0]) + } + + expect.fulfill() + } + + Defaults[key].insert(element: fixtureCustomCollection1, at: 0) + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveOptionalKeyCombine() { + let key = Defaults.Key?>("observeCollectionCustomElementOptionalKeyCombine") + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(3) + + let expectedValue: [(Item?, Item?)] = [(nil, fixtureCustomCollection), (fixtureCustomCollection, fixtureCustomCollection1), (fixtureCustomCollection1, nil)] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0, tuples[index].0?[0]) + XCTAssertEqual(expected.1, tuples[index].1?[0]) + } + + expect.fulfill() + } + + Defaults[key] = .init(items: [fixtureCustomCollection]) + Defaults[key]?.insert(element: fixtureCustomCollection1, at: 0) + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveArrayKeyCombine() { + let key = Defaults.Key<[Bag]>("observeCollectionCustomElementArrayKeyCombine", default: [.init(items: [fixtureCustomCollection])]) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let cancellable = publisher.sink { tuples in + for (index, expected) in [(fixtureCustomCollection, fixtureCustomCollection1), (fixtureCustomCollection1, fixtureCustomCollection)].enumerated() { + XCTAssertEqual(expected.0, tuples[index].0[0][0]) + XCTAssertEqual(expected.1, tuples[index].1[0][0]) + } + + expect.fulfill() + } + + Defaults[key][0].insert(element: fixtureCustomCollection1, at: 0) + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveDictionaryKeyCombine() { + let key = Defaults.Key<[String: Bag]>("observeCollectionCustomElementDictionaryKeyCombine", default: ["0": .init(items: [fixtureCustomCollection])]) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let cancellable = publisher.sink { tuples in + for (index, expected) in [(fixtureCustomCollection, fixtureCustomCollection1), (fixtureCustomCollection1, fixtureCustomCollection)].enumerated() { + XCTAssertEqual(expected.0, tuples[index].0["0"]?[0]) + XCTAssertEqual(expected.1, tuples[index].1["0"]?[0]) + } + + expect.fulfill() + } + + Defaults[key]["0"]?.insert(element: fixtureCustomCollection1, at: 0) + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + func testObserveKey() { + let key = Defaults.Key>("observeCollectionCustomElementKey", default: .init(items: [fixtureCustomCollection])) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue[0], fixtureCustomCollection) + XCTAssertEqual(change.newValue[0], fixtureCustomCollection1) + observation.invalidate() + expect.fulfill() + } + + Defaults[key].insert(element: fixtureCustomCollection1, at: 0) + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveOptionalKey() { + let key = Defaults.Key?>("observeCollectionCustomElementOptionalKey") + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertNil(change.oldValue) + XCTAssertEqual(change.newValue?[0], fixtureCustomCollection) + observation.invalidate() + expect.fulfill() + } + + Defaults[key] = .init(items: [fixtureCustomCollection]) + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveArrayKey() { + let key = Defaults.Key<[Bag]>("observeCollectionCustomElementArrayKey", default: [.init(items: [fixtureCustomCollection])]) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue[0][0], fixtureCustomCollection) + XCTAssertEqual(change.newValue[0][0], fixtureCustomCollection1) + observation.invalidate() + expect.fulfill() + } + + Defaults[key][0].insert(element: fixtureCustomCollection1, at: 0) + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveDictionaryKey() { + let key = Defaults.Key<[String: Bag]>("observeCollectionCustomElementArrayKey", default: ["0": .init(items: [fixtureCustomCollection])]) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue["0"]?[0], fixtureCustomCollection) + XCTAssertEqual(change.newValue["0"]?[0], fixtureCustomCollection1) + observation.invalidate() + expect.fulfill() + } + + Defaults[key]["0"]?.insert(element: fixtureCustomCollection1, at: 0) + observation.invalidate() + + waitForExpectations(timeout: 10) + } +} diff --git a/Tests/DefaultsTests/DefaultsCollectionTests.swift b/Tests/DefaultsTests/DefaultsCollectionTests.swift new file mode 100644 index 0000000..12501bc --- /dev/null +++ b/Tests/DefaultsTests/DefaultsCollectionTests.swift @@ -0,0 +1,344 @@ +import Foundation +import XCTest +import Defaults + +struct Bag: Collection { + var items: [Element] + + init(items: [Element]) { + self.items = items + } + + var startIndex: Int { + items.startIndex + } + + var endIndex: Int { + items.endIndex + } + + mutating func insert(element: Element, at: Int) { + items.insert(element, at: at) + } + + func index(after index: Int) -> Int { + items.index(after: index) + } + + subscript(position: Int) -> Element { + items[position] + } +} + +extension Bag: Defaults.CollectionSerializable { + init(_ elements: [Element]) { + self.items = elements + } +} + + +private let fixtureCollection = ["Juice", "Apple", "Banana"] + +extension Defaults.Keys { + fileprivate static let collection = Key>("collection", default: Bag(items: fixtureCollection)) + fileprivate static let collectionArray = Key<[Bag]>("collectionArray", default: [Bag(items: fixtureCollection)]) + fileprivate static let collectionDictionary = Key<[String: Bag]>("collectionDictionary", default: ["0": Bag(items: fixtureCollection)]) +} + +final class DefaultsCollectionTests: XCTestCase { + override func setUp() { + super.setUp() + Defaults.removeAll() + } + + override func tearDown() { + super.tearDown() + Defaults.removeAll() + } + + func testKey() { + let key = Defaults.Key>("independentCollectionKey", default: Bag(items: fixtureCollection)) + Defaults[key].insert(element: "123", at: 0) + XCTAssertEqual(Defaults[key][0], "123") + } + + func testOptionalKey() { + let key = Defaults.Key?>("independentCollectionOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = Bag(items: []) + Defaults[key]?.insert(element: fixtureCollection[0], at: 0) + XCTAssertEqual(Defaults[key]?[0], fixtureCollection[0]) + Defaults[key]?.insert(element: fixtureCollection[1], at: 1) + XCTAssertEqual(Defaults[key]?[1], fixtureCollection[1]) + } + + func testArrayKey() { + let key = Defaults.Key<[Bag]>("independentCollectionArrayKey", default: [Bag(items: [fixtureCollection[0]])]) + Defaults[key].append(Bag(items: [fixtureCollection[1]])) + XCTAssertEqual(Defaults[key][1][0], fixtureCollection[1]) + Defaults[key][0].insert(element: fixtureCollection[2], at: 1) + XCTAssertEqual(Defaults[key][0][1], fixtureCollection[2]) + } + + func testArrayOptionalKey() { + let key = Defaults.Key<[Bag]?>("independentCollectionArrayOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = [Bag(items: [fixtureCollection[0]])] + Defaults[key]?.append(Bag(items: [fixtureCollection[1]])) + XCTAssertEqual(Defaults[key]?[1][0], fixtureCollection[1]) + Defaults[key]?[0].insert(element: fixtureCollection[2], at: 1) + XCTAssertEqual(Defaults[key]?[0][1], fixtureCollection[2]) + } + + func testNestedArrayKey() { + let key = Defaults.Key<[[Bag]]>("independentCollectionNestedArrayKey", default: [[Bag(items: [fixtureCollection[0]])]]) + Defaults[key][0].append(Bag(items: [fixtureCollection[1]])) + Defaults[key].append([Bag(items: [fixtureCollection[2]])]) + XCTAssertEqual(Defaults[key][0][0][0], fixtureCollection[0]) + XCTAssertEqual(Defaults[key][0][1][0], fixtureCollection[1]) + XCTAssertEqual(Defaults[key][1][0][0], fixtureCollection[2]) + } + + func testArrayDictionaryKey() { + let key = Defaults.Key<[[String: Bag]]>("independentCollectionArrayDictionaryKey", default: [["0": Bag(items: [fixtureCollection[0]])]]) + Defaults[key][0]["1"] = Bag(items: [fixtureCollection[1]]) + Defaults[key].append(["0": Bag(items: [fixtureCollection[2]])]) + XCTAssertEqual(Defaults[key][0]["0"]?[0], fixtureCollection[0]) + XCTAssertEqual(Defaults[key][0]["1"]?[0], fixtureCollection[1]) + XCTAssertEqual(Defaults[key][1]["0"]?[0], fixtureCollection[2]) + } + + func testDictionaryKey() { + let key = Defaults.Key<[String: Bag]>("independentCollectionDictionaryKey", default: ["0": Bag(items: [fixtureCollection[0]])]) + Defaults[key]["0"]?.insert(element: fixtureCollection[1], at: 1) + Defaults[key]["1"] = Bag(items: [fixtureCollection[2]]) + XCTAssertEqual(Defaults[key]["0"]?[0], fixtureCollection[0]) + XCTAssertEqual(Defaults[key]["0"]?[1], fixtureCollection[1]) + XCTAssertEqual(Defaults[key]["1"]?[0], fixtureCollection[2]) + } + + func testDictionaryOptionalKey() { + let key = Defaults.Key<[String: Bag]?>("independentCollectionDictionaryOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = ["0": Bag(items: [fixtureCollection[0]])] + Defaults[key]?["0"]?.insert(element: fixtureCollection[1], at: 1) + Defaults[key]?["1"] = Bag(items: [fixtureCollection[2]]) + XCTAssertEqual(Defaults[key]?["0"]?[0], fixtureCollection[0]) + XCTAssertEqual(Defaults[key]?["0"]?[1], fixtureCollection[1]) + XCTAssertEqual(Defaults[key]?["1"]?[0], fixtureCollection[2]) + } + + func testDictionaryArrayKey() { + let key = Defaults.Key<[String: [Bag]]>("independentCollectionDictionaryArrayKey", default: ["0": [Bag(items: [fixtureCollection[0]])]]) + Defaults[key]["0"]?[0].insert(element: fixtureCollection[1], at: 1) + Defaults[key]["1"] = [Bag(items: [fixtureCollection[2]])] + XCTAssertEqual(Defaults[key]["0"]?[0][0], fixtureCollection[0]) + XCTAssertEqual(Defaults[key]["0"]?[0][1], fixtureCollection[1]) + XCTAssertEqual(Defaults[key]["1"]?[0][0], fixtureCollection[2]) + } + + func testType() { + Defaults[.collection].insert(element: "123", at: 0) + XCTAssertEqual(Defaults[.collection][0], "123") + } + + func testArrayType() { + Defaults[.collectionArray].append(Bag(items: [fixtureCollection[0]])) + Defaults[.collectionArray][0].insert(element: "123", at: 0) + XCTAssertEqual(Defaults[.collectionArray][0][0], "123") + XCTAssertEqual(Defaults[.collectionArray][1][0], fixtureCollection[0]) + } + + func testDictionaryType() { + Defaults[.collectionDictionary]["1"] = Bag(items: [fixtureCollection[0]]) + Defaults[.collectionDictionary]["0"]?.insert(element: "123", at: 0) + XCTAssertEqual(Defaults[.collectionDictionary]["0"]?[0], "123") + XCTAssertEqual(Defaults[.collectionDictionary]["1"]?[0], fixtureCollection[0]) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveKeyCombine() { + let key = Defaults.Key>("observeCollectionKeyCombine", default: .init(items: fixtureCollection)) + let item = "Grape" + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let cancellable = publisher.sink { tuples in + for (index, expected) in [(fixtureCollection[0], item), (item, fixtureCollection[0])].enumerated() { + XCTAssertEqual(expected.0, tuples[index].0[0]) + XCTAssertEqual(expected.1, tuples[index].1[0]) + } + + expect.fulfill() + } + + Defaults[key].insert(element: item, at: 0) + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveOptionalKeyCombine() { + let key = Defaults.Key?>("observeCollectionOptionalKeyCombine") + let item = "Grape" + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(3) + + let expectedValue: [(String?, String?)] = [(nil, fixtureCollection[0]), (fixtureCollection[0], item), (item, nil)] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0, tuples[index].0?[0]) + XCTAssertEqual(expected.1, tuples[index].1?[0]) + } + + expect.fulfill() + } + + Defaults[key] = Bag(items: fixtureCollection) + Defaults[key]?.insert(element: item, at: 0) + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveArrayKeyCombine() { + let key = Defaults.Key<[Bag]>("observeCollectionArrayKeyCombine", default: [.init(items: fixtureCollection)]) + let item = "Grape" + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let cancellable = publisher.sink { tuples in + for (index, expected) in [(fixtureCollection[0], item), (item, fixtureCollection[0])].enumerated() { + XCTAssertEqual(expected.0, tuples[index].0[0][0]) + XCTAssertEqual(expected.1, tuples[index].1[0][0]) + } + + expect.fulfill() + } + + Defaults[key][0].insert(element: item, at: 0) + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveDictionaryKeyCombine() { + let key = Defaults.Key<[String: Bag]>("observeCollectionArrayKeyCombine", default: ["0": .init(items: fixtureCollection)]) + let item = "Grape" + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let cancellable = publisher.sink { tuples in + for (index, expected) in [(fixtureCollection[0], item), (item, fixtureCollection[0])].enumerated() { + XCTAssertEqual(expected.0, tuples[index].0["0"]?[0]) + XCTAssertEqual(expected.1, tuples[index].1["0"]?[0]) + } + + expect.fulfill() + } + + Defaults[key]["0"]?.insert(element: item, at: 0) + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + func testObserveKey() { + let key = Defaults.Key>("observeCollectionKey", default: .init(items: fixtureCollection)) + let item = "Grape" + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue[0], fixtureCollection[0]) + XCTAssertEqual(change.newValue[0], item) + observation.invalidate() + expect.fulfill() + } + + Defaults[key].insert(element: item, at: 0) + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveOptionalKey() { + let key = Defaults.Key?>("observeCollectionOptionalKey") + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertNil(change.oldValue) + XCTAssertEqual(change.newValue?[0], fixtureCollection[0]) + observation.invalidate() + expect.fulfill() + } + + Defaults[key] = .init(items: fixtureCollection) + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveArrayKey() { + let key = Defaults.Key<[Bag]>("observeCollectionArrayKey", default: [.init(items: fixtureCollection)]) + let item = "Grape" + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue[0][0], fixtureCollection[0]) + XCTAssertEqual(change.newValue[0][0], item) + observation.invalidate() + expect.fulfill() + } + + Defaults[key][0].insert(element: item, at: 0) + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveDictionaryKey() { + let key = Defaults.Key<[String: Bag]>("observeCollectionDictionaryKey", default: ["0": .init(items: fixtureCollection)]) + let item = "Grape" + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue["0"]?[0], fixtureCollection[0]) + XCTAssertEqual(change.newValue["0"]?[0], item) + observation.invalidate() + expect.fulfill() + } + + Defaults[key]["0"]?.insert(element: item, at: 0) + observation.invalidate() + + waitForExpectations(timeout: 10) + } +} diff --git a/Tests/DefaultsTests/DefaultsCustomBridgeTests.swift b/Tests/DefaultsTests/DefaultsCustomBridgeTests.swift new file mode 100644 index 0000000..86efcaf --- /dev/null +++ b/Tests/DefaultsTests/DefaultsCustomBridgeTests.swift @@ -0,0 +1,357 @@ +import Foundation +import Defaults +import XCTest + +public struct User: Hashable, Equatable { + var username: String + var password: String +} + +extension User: Defaults.Serializable { + public static let bridge = DefaultsUserBridge() +} + +public final class DefaultsUserBridge: Defaults.Bridge { + public typealias Value = User + public typealias Serializable = [String: String] + + public func serialize(_ value: Value?) -> Serializable? { + guard let value = value else { + return nil + } + + return ["username": value.username, "password": value.password] + } + + public func deserialize(_ object: Serializable?) -> Value? { + guard + let object = object, + let username = object["username"], + let password = object["password"] + else { + return nil + } + + return User(username: username, password: password) + } +} + +private let fixtureCustomBridge = User(username: "hank121314", password: "123456") + +extension Defaults.Keys { + fileprivate static let customBridge = Key("customBridge", default: fixtureCustomBridge) + fileprivate static let customBridgeArray = Key<[User]>("array_customBridge", default: [fixtureCustomBridge]) + fileprivate static let customBridgeDictionary = Key<[String: User]>("dictionary_customBridge", default: ["0": fixtureCustomBridge]) +} + + +final class DefaultsCustomBridge: XCTestCase { + override func setUp() { + super.setUp() + Defaults.removeAll() + } + + override func tearDown() { + super.tearDown() + Defaults.removeAll() + } + + func testKey() { + let key = Defaults.Key("independentCustomBridgeKey", default: fixtureCustomBridge) + XCTAssertEqual(Defaults[key], fixtureCustomBridge) + let newUser = User(username: "sindresorhus", password: "123456789") + Defaults[key] = newUser + XCTAssertEqual(Defaults[key], newUser) + } + + func testOptionalKey() { + let key = Defaults.Key("independentCustomBridgeOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = fixtureCustomBridge + XCTAssertEqual(Defaults[key], fixtureCustomBridge) + } + + func testArrayKey() { + let user = User(username: "hank121314", password: "123456") + let key = Defaults.Key<[User]>("independentCustomBridgeArrayKey", default: [user]) + XCTAssertEqual(Defaults[key][0], user) + let newUser = User(username: "sindresorhus", password: "123456789") + Defaults[key][0] = newUser + XCTAssertEqual(Defaults[key][0], newUser) + } + + func testArrayOptionalKey() { + let key = Defaults.Key<[User]?>("independentCustomBridgeArrayOptionalKey") + XCTAssertNil(Defaults[key]) + let newUser = User(username: "hank121314", password: "123456") + Defaults[key] = [newUser] + XCTAssertEqual(Defaults[key]?[0], newUser) + Defaults[key] = nil + XCTAssertNil(Defaults[key]) + } + + func testNestedArrayKey() { + let key = Defaults.Key<[[User]]>("independentCustomBridgeNestedArrayKey", default: [[fixtureCustomBridge], [fixtureCustomBridge]]) + XCTAssertEqual(Defaults[key][0][0].username, fixtureCustomBridge.username) + let newUsername = "John" + let newPassword = "7891011" + Defaults[key][0][0] = User(username: newUsername, password: newPassword) + XCTAssertEqual(Defaults[key][0][0].username, newUsername) + XCTAssertEqual(Defaults[key][0][0].password, newPassword) + XCTAssertEqual(Defaults[key][1][0].username, fixtureCustomBridge.username) + XCTAssertEqual(Defaults[key][1][0].password, fixtureCustomBridge.password) + } + + func testArrayDictionaryKey() { + let key = Defaults.Key<[[String: User]]>("independentCustomBridgeArrayDictionaryKey", default: [["0": fixtureCustomBridge], ["0": fixtureCustomBridge]]) + XCTAssertEqual(Defaults[key][0]["0"]?.username, fixtureCustomBridge.username) + let newUser = User(username: "sindresorhus", password: "123456789") + Defaults[key][0]["0"] = newUser + XCTAssertEqual(Defaults[key][0]["0"], newUser) + XCTAssertEqual(Defaults[key][1]["0"], fixtureCustomBridge) + } + + func testSetKey() { + let key = Defaults.Key>("independentCustomBridgeSetKey", default: [fixtureCustomBridge]) + XCTAssertEqual(Defaults[key].first, fixtureCustomBridge) + Defaults[key].insert(fixtureCustomBridge) + XCTAssertEqual(Defaults[key].count, 1) + let newUser = User(username: "sindresorhus", password: "123456789") + Defaults[key].insert(newUser) + XCTAssertTrue(Defaults[key].contains(newUser)) + } + + func testDictionaryKey() { + let key = Defaults.Key<[String: User]>("independentCustomBridgeDictionaryKey", default: ["0": fixtureCustomBridge]) + XCTAssertEqual(Defaults[key]["0"], fixtureCustomBridge) + let newUser = User(username: "sindresorhus", password: "123456789") + Defaults[key]["0"] = newUser + XCTAssertEqual(Defaults[key]["0"], newUser) + } + + func testDictionaryOptionalKey() { + let key = Defaults.Key<[String: User]?>("independentCustomBridgeDictionaryOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = ["0": fixtureCustomBridge] + XCTAssertEqual(Defaults[key]?["0"], fixtureCustomBridge) + } + + func testDictionaryArrayKey() { + let key = Defaults.Key<[String: [User]]>("independentCustomBridgeDictionaryArrayKey", default: ["0": [fixtureCustomBridge]]) + XCTAssertEqual(Defaults[key]["0"]?[0], fixtureCustomBridge) + let newUser = User(username: "sindresorhus", password: "123456789") + Defaults[key]["0"]?[0] = newUser + Defaults[key]["0"]?.append(fixtureCustomBridge) + XCTAssertEqual(Defaults[key]["0"]?[0], newUser) + XCTAssertEqual(Defaults[key]["0"]?[0], newUser) + XCTAssertEqual(Defaults[key]["0"]?[1], fixtureCustomBridge) + XCTAssertEqual(Defaults[key]["0"]?[1], fixtureCustomBridge) + } + + func testType() { + XCTAssertEqual(Defaults[.customBridge], fixtureCustomBridge) + let newUser = User(username: "sindresorhus", password: "123456789") + Defaults[.customBridge] = newUser + XCTAssertEqual(Defaults[.customBridge], newUser) + } + + func testArrayType() { + XCTAssertEqual(Defaults[.customBridgeArray][0], fixtureCustomBridge) + let newUser = User(username: "sindresorhus", password: "123456789") + Defaults[.customBridgeArray][0] = newUser + XCTAssertEqual(Defaults[.customBridgeArray][0], newUser) + } + + func testDictionaryType() { + XCTAssertEqual(Defaults[.customBridgeDictionary]["0"], fixtureCustomBridge) + let newUser = User(username: "sindresorhus", password: "123456789") + Defaults[.customBridgeDictionary]["0"] = newUser + XCTAssertEqual(Defaults[.customBridgeDictionary]["0"], newUser) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveKeyCombine() { + let key = Defaults.Key("observeCustomBridgeKeyCombine", default: fixtureCustomBridge) + let newUser = User(username: "sindresorhus", password: "123456789") + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let cancellable = publisher.sink { tuples in + for (index, expected) in [(fixtureCustomBridge, newUser), (newUser, fixtureCustomBridge)].enumerated() { + XCTAssertEqual(expected.0, tuples[index].0) + XCTAssertEqual(expected.1, tuples[index].1) + } + + expect.fulfill() + } + + Defaults[key] = newUser + Defaults[key] = fixtureCustomBridge + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveOptionalKeyCombine() { + let key = Defaults.Key("observeCustomBridgeOptionalKeyCombine") + let newUser = User(username: "sindresorhus", password: "123456789") + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(3) + + let expectedValue: [(User?, User?)] = [(nil, fixtureCustomBridge), (fixtureCustomBridge, newUser), (newUser, nil)] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0, tuples[index].0) + XCTAssertEqual(expected.1, tuples[index].1) + } + + expect.fulfill() + } + + Defaults[key] = fixtureCustomBridge + Defaults[key] = newUser + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveArrayKeyCombine() { + let key = Defaults.Key<[User]>("observeCustomBridgeArrayKeyCombine", default: [fixtureCustomBridge]) + let newUser = User(username: "sindresorhus", password: "123456789") + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let cancellable = publisher.sink { tuples in + for (index, expected) in [([fixtureCustomBridge], [newUser]), ([newUser], [newUser, fixtureCustomBridge])].enumerated() { + XCTAssertEqual(expected.0, tuples[index].0) + XCTAssertEqual(expected.1, tuples[index].1) + } + + expect.fulfill() + } + + Defaults[key][0] = newUser + Defaults[key].append(fixtureCustomBridge) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveDictionaryCombine() { + let key = Defaults.Key<[String: User]>("observeCustomBridgeDictionaryKeyCombine", default: ["0": fixtureCustomBridge]) + let newUser = User(username: "sindresorhus", password: "123456789") + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let cancellable = publisher.sink { tuples in + for (index, expected) in [(fixtureCustomBridge, newUser), (newUser, fixtureCustomBridge)].enumerated() { + XCTAssertEqual(expected.0, tuples[index].0["0"]) + XCTAssertEqual(expected.1, tuples[index].1["0"]) + } + + expect.fulfill() + } + + Defaults[key]["0"] = newUser + Defaults[key]["0"] = fixtureCustomBridge + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + func testObserveKey() { + let key = Defaults.Key("observeCustomBridgeKey", default: fixtureCustomBridge) + let newUser = User(username: "sindresorhus", password: "123456789") + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue, fixtureCustomBridge) + XCTAssertEqual(change.newValue, newUser) + observation.invalidate() + expect.fulfill() + } + + Defaults[key] = newUser + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveOptionalKey() { + let key = Defaults.Key("observeCustomBridgeOptionalKey") + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertNil(change.oldValue) + XCTAssertEqual(change.newValue, fixtureCustomBridge) + observation.invalidate() + expect.fulfill() + } + + Defaults[key] = fixtureCustomBridge + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveArrayKey() { + let key = Defaults.Key<[User]>("observeCustomBridgeArrayKey", default: [fixtureCustomBridge]) + let newUser = User(username: "sindresorhus", password: "123456789") + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue[0], fixtureCustomBridge) + XCTAssertEqual(change.newValue[0], newUser) + observation.invalidate() + expect.fulfill() + } + + Defaults[key][0] = newUser + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveDictionaryKey() { + let key = Defaults.Key<[String: User]>("observeCustomBridgeDictionaryKey", default: ["0": fixtureCustomBridge]) + let newUser = User(username: "sindresorhus", password: "123456789") + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue["0"], fixtureCustomBridge) + XCTAssertEqual(change.newValue["0"], newUser) + observation.invalidate() + expect.fulfill() + } + + Defaults[key]["0"] = newUser + observation.invalidate() + + waitForExpectations(timeout: 10) + } +} diff --git a/Tests/DefaultsTests/DefaultsDictionaryTests.swift b/Tests/DefaultsTests/DefaultsDictionaryTests.swift new file mode 100644 index 0000000..367d400 --- /dev/null +++ b/Tests/DefaultsTests/DefaultsDictionaryTests.swift @@ -0,0 +1,160 @@ +import Foundation +import Defaults +import XCTest + +private let fixtureDictionary = ["0": "Hank"] + +private let fixtureArray = ["Hank", "Chen"] + +extension Defaults.Keys { + fileprivate static let dictionary = Key<[String: String]>("dictionary", default: fixtureDictionary) +} + +final class DefaultsDictionaryTests: XCTestCase { + override func setUp() { + super.setUp() + Defaults.removeAll() + } + + override func tearDown() { + super.tearDown() + Defaults.removeAll() + } + + func testKey() { + let key = Defaults.Key<[String: String]>("independentDictionaryStringKey", default: fixtureDictionary) + XCTAssertEqual(Defaults[key]["0"], fixtureDictionary["0"]) + let newValue = "John" + Defaults[key]["0"] = newValue + XCTAssertEqual(Defaults[key]["0"], newValue) + } + + func testOptionalKey() { + let key = Defaults.Key<[String: String]?>("independentDictionaryOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = fixtureDictionary + XCTAssertEqual(Defaults[key]?["0"], fixtureDictionary["0"]) + Defaults[key] = nil + XCTAssertNil(Defaults[key]) + let newValue = ["0": "Chen"] + Defaults[key] = newValue + XCTAssertEqual(Defaults[key]?["0"], newValue["0"]) + } + + func testNestedKey() { + let key = Defaults.Key<[String: [String: String]]>("independentDictionaryNestedKey", default: ["0": fixtureDictionary]) + XCTAssertEqual(Defaults[key]["0"]?["0"], "Hank") + let newName = "Chen" + Defaults[key]["0"]?["0"] = newName + XCTAssertEqual(Defaults[key]["0"]?["0"], newName) + } + + func testArrayKey() { + let key = Defaults.Key<[String: [String]]>("independentDictionaryArrayKey", default: ["0": fixtureArray]) + XCTAssertEqual(Defaults[key]["0"], fixtureArray) + let newName = "Chen" + Defaults[key]["0"]?[0] = newName + XCTAssertEqual(Defaults[key]["0"], [newName, fixtureArray[1]]) + } + + func testType() { + XCTAssertEqual(Defaults[.dictionary]["0"], fixtureDictionary["0"]) + let newName = "Chen" + Defaults[.dictionary]["0"] = newName + XCTAssertEqual(Defaults[.dictionary]["0"], newName) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveKeyCombine() { + let key = Defaults.Key<[String: String]>("observeDictionaryKeyCombine", default: fixtureDictionary) + let expect = expectation(description: "Observation closure being called") + let newName = "John" + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let cancellable = publisher.sink { tuples in + for (index, expected) in [(fixtureDictionary["0"]!, newName), (newName, fixtureDictionary["0"]!)].enumerated() { + XCTAssertEqual(expected.0, tuples[index].0["0"]) + XCTAssertEqual(expected.1, tuples[index].1["0"]) + } + + expect.fulfill() + } + + Defaults[key]["0"] = newName + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveOptionalKeyCombine() { + let key = Defaults.Key<[String: String]?>("observeDictionaryOptionalKeyCombine") + let expect = expectation(description: "Observation closure being called") + let newName = ["0": "John"] + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(3) + + // swiftlint:disable discouraged_optional_collection + let expectedValues: [([String: String]?, [String: String]?)] = [(nil, fixtureDictionary), (fixtureDictionary, newName), (newName, nil)] + + let cancellable = publisher.sink { actualValues in + for (expected, actual) in zip(expectedValues, actualValues) { + XCTAssertEqual(expected.0, actual.0) + XCTAssertEqual(expected.1, actual.1) + } + + expect.fulfill() + } + + Defaults[key] = fixtureDictionary + Defaults[key] = newName + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + func testObserveKey() { + let key = Defaults.Key<[String: String]>("observeDictionaryKey", default: fixtureDictionary) + let expect = expectation(description: "Observation closure being called") + let newName = "John" + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue, fixtureDictionary) + XCTAssertEqual(change.newValue["1"], newName) + observation.invalidate() + expect.fulfill() + } + + Defaults[key]["1"] = newName + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveOptionalKey() { + let key = Defaults.Key<[String: String]?>("observeDictionaryOptionalKey") + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertNil(change.oldValue) + XCTAssertEqual(change.newValue!, fixtureDictionary) + observation.invalidate() + expect.fulfill() + } + + Defaults[key] = fixtureDictionary + observation.invalidate() + + waitForExpectations(timeout: 10) + } +} diff --git a/Tests/DefaultsTests/DefaultsEnumTests.swift b/Tests/DefaultsTests/DefaultsEnumTests.swift new file mode 100644 index 0000000..2ede6de --- /dev/null +++ b/Tests/DefaultsTests/DefaultsEnumTests.swift @@ -0,0 +1,308 @@ +import Foundation +import Defaults +import XCTest + +private enum FixtureEnum: String, Defaults.Serializable { + case tenMinutes = "10 Minutes" + case halfHour = "30 Minutes" + case oneHour = "1 Hour" +} + +extension Defaults.Keys { + fileprivate static let `enum` = Key("enum", default: .tenMinutes) + fileprivate static let enumArray = Key<[FixtureEnum]>("array_enum", default: [.tenMinutes]) + fileprivate static let enumDictionary = Key<[String: FixtureEnum]>("dictionary_enum", default: ["0": .tenMinutes]) +} + +final class DefaultsEnumTests: XCTestCase { + override func setUp() { + super.setUp() + Defaults.removeAll() + } + + override func tearDown() { + super.tearDown() + Defaults.removeAll() + } + + func testKey() { + let key = Defaults.Key("independentEnumKey", default: .tenMinutes) + XCTAssertEqual(Defaults[key], .tenMinutes) + Defaults[key] = .halfHour + XCTAssertEqual(Defaults[key], .halfHour) + } + + func testOptionalKey() { + let key = Defaults.Key("independentEnumOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = .tenMinutes + XCTAssertEqual(Defaults[key], .tenMinutes) + } + + func testArrayKey() { + let key = Defaults.Key<[FixtureEnum]>("independentEnumArrayKey", default: [.tenMinutes]) + XCTAssertEqual(Defaults[key][0], .tenMinutes) + Defaults[key].append(.halfHour) + XCTAssertEqual(Defaults[key][0], .tenMinutes) + XCTAssertEqual(Defaults[key][1], .halfHour) + } + + func testArrayOptionalKey() { + let key = Defaults.Key<[FixtureEnum]?>("independentEnumArrayOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = [.tenMinutes] + Defaults[key]?.append(.halfHour) + XCTAssertEqual(Defaults[key]?[0], .tenMinutes) + XCTAssertEqual(Defaults[key]?[1], .halfHour) + } + + func testNestedArrayKey() { + let key = Defaults.Key<[[FixtureEnum]]>("independentEnumNestedArrayKey", default: [[.tenMinutes]]) + XCTAssertEqual(Defaults[key][0][0], .tenMinutes) + Defaults[key][0].append(.halfHour) + Defaults[key].append([.oneHour]) + XCTAssertEqual(Defaults[key][0][1], .halfHour) + XCTAssertEqual(Defaults[key][1][0], .oneHour) + } + + func testArrayDictionaryKey() { + let key = Defaults.Key<[[String: FixtureEnum]]>("independentEnumArrayDictionaryKey", default: [["0": .tenMinutes]]) + XCTAssertEqual(Defaults[key][0]["0"], .tenMinutes) + Defaults[key][0]["1"] = .halfHour + Defaults[key].append(["0": .oneHour]) + XCTAssertEqual(Defaults[key][0]["1"], .halfHour) + XCTAssertEqual(Defaults[key][1]["0"], .oneHour) + } + + func testDictionaryKey() { + let key = Defaults.Key<[String: FixtureEnum]>("independentEnumDictionaryKey", default: ["0": .tenMinutes]) + XCTAssertEqual(Defaults[key]["0"], .tenMinutes) + Defaults[key]["1"] = .halfHour + XCTAssertEqual(Defaults[key]["0"], .tenMinutes) + XCTAssertEqual(Defaults[key]["1"], .halfHour) + } + + func testDictionaryOptionalKey() { + let key = Defaults.Key<[String: FixtureEnum]?>("independentEnumDictionaryOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = ["0": .tenMinutes] + XCTAssertEqual(Defaults[key]?["0"], .tenMinutes) + } + + func testDictionaryArrayKey() { + let key = Defaults.Key<[String: [FixtureEnum]]>("independentEnumDictionaryKey", default: ["0": [.tenMinutes]]) + XCTAssertEqual(Defaults[key]["0"]?[0], .tenMinutes) + Defaults[key]["0"]?.append(.halfHour) + Defaults[key]["1"] = [.oneHour] + XCTAssertEqual(Defaults[key]["0"]?[1], .halfHour) + XCTAssertEqual(Defaults[key]["1"]?[0], .oneHour) + } + + func testType() { + XCTAssertEqual(Defaults[.enum], .tenMinutes) + Defaults[.enum] = .halfHour + XCTAssertEqual(Defaults[.enum], .halfHour) + } + + func testArrayType() { + XCTAssertEqual(Defaults[.enumArray][0], .tenMinutes) + Defaults[.enumArray][0] = .oneHour + XCTAssertEqual(Defaults[.enumArray][0], .oneHour) + } + + func testDictionaryType() { + XCTAssertEqual(Defaults[.enumDictionary]["0"], .tenMinutes) + Defaults[.enumDictionary]["0"] = .halfHour + XCTAssertEqual(Defaults[.enumDictionary]["0"], .halfHour) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveKeyCombine() { + let key = Defaults.Key("observeEnumKeyCombine", default: .tenMinutes) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(3) + + let expectedValue: [(FixtureEnum, FixtureEnum)] = [(.tenMinutes, .halfHour), (.halfHour, .oneHour), (.oneHour, .tenMinutes)] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0, tuples[index].0) + XCTAssertEqual(expected.1, tuples[index].1) + } + + expect.fulfill() + } + + Defaults[key] = .tenMinutes + Defaults[key] = .halfHour + Defaults[key] = .oneHour + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveOptionalKeyCombine() { + let key = Defaults.Key("observeEnumOptionalKeyCombine") + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(4) + + let expectedValue: [(FixtureEnum?, FixtureEnum?)] = [(nil, .tenMinutes), (.tenMinutes, .halfHour), (.halfHour, .oneHour), (.oneHour, nil)] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0, tuples[index].0) + XCTAssertEqual(expected.1, tuples[index].1) + } + + expect.fulfill() + } + + Defaults[key] = .tenMinutes + Defaults[key] = .halfHour + Defaults[key] = .oneHour + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveArrayKeyCombine() { + let key = Defaults.Key<[FixtureEnum]>("observeEnumArrayKeyCombine", default: [.tenMinutes]) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let expectedValue: [(FixtureEnum, FixtureEnum)] = [(.tenMinutes, .halfHour), (.halfHour, .oneHour)] + + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0, tuples[index].0[0]) + XCTAssertEqual(expected.1, tuples[index].1[0]) + } + + expect.fulfill() + } + + Defaults[key][0] = .halfHour + Defaults[key][0] = .oneHour + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveDictionaryKeyCombine() { + let key = Defaults.Key<[String: FixtureEnum]>("observeEnumDictionaryKeyCombine", default: ["0": .tenMinutes]) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let expectedValue: [(FixtureEnum, FixtureEnum)] = [(.tenMinutes, .halfHour), (.halfHour, .oneHour)] + + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0, tuples[index].0["0"]) + XCTAssertEqual(expected.1, tuples[index].1["0"]) + } + + expect.fulfill() + } + + Defaults[key]["0"] = .halfHour + Defaults[key]["0"] = .oneHour + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + func testObserveKey() { + let key = Defaults.Key("observeEnumKey", default: .tenMinutes) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue, .tenMinutes) + XCTAssertEqual(change.newValue, .halfHour) + observation.invalidate() + expect.fulfill() + } + + Defaults[key] = .halfHour + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveOptionalKey() { + let key = Defaults.Key("observeEnumOptionalKey") + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertNil(change.oldValue) + XCTAssertEqual(change.newValue, .tenMinutes) + observation.invalidate() + expect.fulfill() + } + + Defaults[key] = .tenMinutes + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveArrayKey() { + let key = Defaults.Key<[FixtureEnum]>("observeEnumArrayKey", default: [.tenMinutes]) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue[0], .tenMinutes) + XCTAssertEqual(change.newValue[1], .halfHour) + observation.invalidate() + expect.fulfill() + } + + Defaults[key].append(.halfHour) + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveDictionaryKey() { + let key = Defaults.Key<[String: FixtureEnum]>("observeEnumDictionaryKey", default: ["0": .tenMinutes]) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue["0"], .tenMinutes) + XCTAssertEqual(change.newValue["1"], .halfHour) + observation.invalidate() + expect.fulfill() + } + + Defaults[key]["1"] = .halfHour + observation.invalidate() + + waitForExpectations(timeout: 10) + } +} diff --git a/Tests/DefaultsTests/DefaultsMigrationTests.swift b/Tests/DefaultsTests/DefaultsMigrationTests.swift new file mode 100644 index 0000000..dffc7a1 --- /dev/null +++ b/Tests/DefaultsTests/DefaultsMigrationTests.swift @@ -0,0 +1,1166 @@ +import Defaults +import Foundation +import XCTest + +// Create an unique id to test whether LosslessStringConvertible works. +private struct UniqueID: LosslessStringConvertible, Hashable { + var id: Int64 + + var description: String { + "\(id)" + } + + init(id: Int64) { + self.id = id + } + + init?(_ description: String) { + self.init(id: Int64(description) ?? 0) + } +} + +private struct TimeZone: Hashable { + var id: String + var name: String +} + +extension TimeZone: Defaults.NativeType { + /// Associated `CodableForm` to `CodableTimeZone` + typealias CodableForm = CodableTimeZone + + static let bridge = TimeZoneBridge() +} + +private struct CodableTimeZone { + var id: String + var name: String +} + +extension CodableTimeZone: Defaults.CodableType { + /// Convert from `Codable` to `Native` + func toNative() -> TimeZone { + TimeZone(id: id, name: name) + } +} + +private struct TimeZoneBridge: Defaults.Bridge { + typealias Value = TimeZone + typealias Serializable = [String: Any] + + func serialize(_ value: TimeZone?) -> Serializable? { + guard let value = value else { + return nil + } + + return ["id": value.id, "name": value.name] + } + + func deserialize(_ object: Serializable?) -> TimeZone? { + guard + let dictionary = object, + let id = dictionary["id"] as? String, + let name = dictionary["name"] as? String + else { + return nil + } + + return TimeZone(id: id, name: name) + } +} + +private struct ChosenTimeZone: Codable, Hashable { + var id: String + var name: String +} + +extension ChosenTimeZone: Defaults.Serializable { + static let bridge = ChosenTimeZoneBridge() +} + +private struct ChosenTimeZoneBridge: Defaults.Bridge { + typealias Value = ChosenTimeZone + typealias Serializable = [String: Any] + + func serialize(_ value: Value?) -> Serializable? { + guard let value = value else { + return nil + } + + return ["id": value.id, "name": value.name] + } + + func deserialize(_ object: Serializable?) -> Value? { + guard + let dictionary = object, + let id = dictionary["id"] as? String, + let name = dictionary["name"] as? String + else { + return nil + } + + return ChosenTimeZone(id: id, name: name) + } +} + +private protocol BagForm { + associatedtype Element + var items: [Element] { get set } +} + +extension BagForm { + var startIndex: Int { + items.startIndex + } + + var endIndex: Int { + items.endIndex + } + + mutating func insert(element: Element, at: Int) { + items.insert(element, at: at) + } + + func index(after index: Int) -> Int { + items.index(after: index) + } + + subscript(position: Int) -> Element { + get { items[position] } + set { items[position] = newValue } + } +} + +private struct MyBag: BagForm, Defaults.CollectionSerializable, Defaults.NativeType { + var items: [Element] + + init(_ elements: [Element]) { + self.items = elements + } +} + +private struct CodableBag: BagForm, Defaults.CollectionSerializable, Codable { + var items: [Element] + + init(_ elements: [Element]) { + self.items = elements + } +} + +private protocol SetForm: SetAlgebra where Element: Hashable { + var store: Set { get set } +} + +extension SetForm { + func contains(_ member: Element) -> Bool { + store.contains(member) + } + + func union(_ other: Self) -> Self { + Self(store.union(other.store)) + } + + func intersection(_ other: Self) + -> Self { + var setForm = Self() + setForm.store = store.intersection(other.store) + return setForm + } + + func symmetricDifference(_ other: Self) + -> Self { + var setForm = Self() + setForm.store = store.symmetricDifference(other.store) + return setForm + } + + @discardableResult + mutating func insert(_ newMember: Element) + -> (inserted: Bool, memberAfterInsert: Element) { + store.insert(newMember) + } + + mutating func remove(_ member: Element) -> Element? { + store.remove(member) + } + + mutating func update(with newMember: Element) -> Element? { + store.update(with: newMember) + } + + mutating func formUnion(_ other: Self) { + store.formUnion(other.store) + } + + mutating func formSymmetricDifference(_ other: Self) { + store.formSymmetricDifference(other.store) + } + + mutating func formIntersection(_ other: Self) { + store.formIntersection(other.store) + } + + func toArray() -> [Element] { + Array(store) + } +} + +private struct MySet: SetForm, Defaults.SetAlgebraSerializable, Defaults.NativeType { + var store: Set + + init() { + store = [] + } + + init(_ elements: [Element]) { + self.store = Set(elements) + } +} + +private struct CodableSet: SetForm, Defaults.SetAlgebraSerializable, Codable { + var store: Set + + init() { + store = [] + } + + init(_ elements: [Element]) { + self.store = Set(elements) + } +} + +private enum EnumForm: String { + case tenMinutes = "10 Minutes" + case halfHour = "30 Minutes" + case oneHour = "1 Hour" +} + +extension EnumForm: Defaults.NativeType { + typealias CodableForm = CodableEnumForm +} + +private enum CodableEnumForm: String { + case tenMinutes = "10 Minutes" + case halfHour = "30 Minutes" + case oneHour = "1 Hour" +} + +extension CodableEnumForm: Defaults.CodableType { + typealias NativeForm = EnumForm +} + +private func setCodable(forKey keyName: String, data: Value) { + guard + let text = try? JSONEncoder().encode(data), + let string = String(data: text, encoding: .utf8) + else { + XCTAssert(false) + return + } + + UserDefaults.standard.set(string, forKey: keyName) +} + +extension Defaults.Keys { + fileprivate static let nativeArray = Key<[String]?>("arrayToNativeStaticArrayKey") +} + +final class DefaultsMigrationTests: XCTestCase { + override func setUp() { + super.setUp() + Defaults.removeAll() + } + + override func tearDown() { + super.tearDown() + Defaults.removeAll() + } + + func testDataToNativeData() { + let answer = "Hello World!" + let keyName = "dataToNativeData" + let data = answer.data(using: .utf8) + setCodable(forKey: keyName, data: data) + let key = Defaults.Key(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(answer, String(data: Defaults[key]!, encoding: .utf8)) + let newName = " Hank Chen" + Defaults[key]?.append(newName.data(using: .utf8)!) + XCTAssertEqual(answer + newName, String(data: Defaults[key]!, encoding: .utf8)) + } + + func testArrayDataToNativeCollectionData() { + let answer = "Hello World!" + let keyName = "arrayDataToNativeCollectionData" + let data = answer.data(using: .utf8) + setCodable(forKey: keyName, data: [data]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(answer, String(data: Defaults[key]!.first!, encoding: .utf8)) + let newName = " Hank Chen" + Defaults[key]?[0].append(newName.data(using: .utf8)!) + XCTAssertEqual(answer + newName, String(data: Defaults[key]!.first!, encoding: .utf8)) + } + + func testArrayDataToCodableCollectionData() { + let answer = "Hello World!" + let keyName = "arrayDataToCodableCollectionData" + let data = answer.data(using: .utf8) + setCodable(forKey: keyName, data: CodableBag([data])) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(answer, String(data: Defaults[key]!.first!, encoding: .utf8)) + let newName = " Hank Chen" + Defaults[key]?[0].append(newName.data(using: .utf8)!) + XCTAssertEqual(answer + newName, String(data: Defaults[key]!.first!, encoding: .utf8)) + } + + func testArrayDataToNativeSetAlgebraData() { + let answer = "Hello World!" + let keyName = "arrayDataToNativeSetAlgebraData" + let data = answer.data(using: .utf8) + setCodable(forKey: keyName, data: CodableSet([data])) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(answer, String(data: Defaults[key]!.store.first!, encoding: .utf8)) + let newName = " Hank Chen" + Defaults[key]?.store.insert(newName.data(using: .utf8)!) + XCTAssertEqual(Set([answer.data(using: .utf8)!, newName.data(using: .utf8)!]), Defaults[key]?.store) + } + + func testDateToNativeDate() { + let date = Date() + let keyName = "dateToNativeDate" + setCodable(forKey: keyName, data: date) + let key = Defaults.Key(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(date, Defaults[key]) + let newDate = Date() + Defaults[key] = newDate + XCTAssertEqual(newDate, Defaults[key]) + } + + func testDateToNativeCollectionDate() { + let date = Date() + let keyName = "dateToNativeCollectionDate" + setCodable(forKey: keyName, data: [date]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(date, Defaults[key]!.first) + let newDate = Date() + Defaults[key]?[0] = newDate + XCTAssertEqual(newDate, Defaults[key]!.first) + } + + func testDateToCodableCollectionDate() { + let date = Date() + let keyName = "dateToCodableCollectionDate" + setCodable(forKey: keyName, data: CodableBag([date])) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(date, Defaults[key]!.first) + let newDate = Date() + Defaults[key]?[0] = newDate + XCTAssertEqual(newDate, Defaults[key]!.first) + } + + func testBoolToNativeBool() { + let bool = false + let keyName = "boolToNativeBool" + setCodable(forKey: keyName, data: bool) + let key = Defaults.Key(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key], bool) + let newBool = true + Defaults[key] = newBool + XCTAssertEqual(Defaults[key], newBool) + } + + func testBoolToNativeCollectionBool() { + let bool = false + let keyName = "boolToNativeCollectionBool" + setCodable(forKey: keyName, data: [bool]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], bool) + let newBool = true + Defaults[key]?[0] = newBool + XCTAssertEqual(Defaults[key]?[0], newBool) + } + + func testBoolToCodableCollectionBool() { + let bool = false + let keyName = "boolToCodableCollectionBool" + setCodable(forKey: keyName, data: CodableBag([bool])) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], bool) + let newBool = true + Defaults[key]?[0] = newBool + XCTAssertEqual(Defaults[key]?[0], newBool) + } + + func testIntToNativeInt() { + let int = Int.min + let keyName = "intToNativeInt" + setCodable(forKey: keyName, data: int) + let key = Defaults.Key(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key], int) + let newInt = Int.max + Defaults[key] = newInt + XCTAssertEqual(Defaults[key], newInt) + } + + func testIntToNativeCollectionInt() { + let int = Int.min + let keyName = "intToNativeCollectionInt" + setCodable(forKey: keyName, data: [int]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], int) + let newInt = Int.max + Defaults[key]?[0] = newInt + XCTAssertEqual(Defaults[key]?[0], newInt) + } + + func testIntToCodableCollectionInt() { + let int = Int.min + let keyName = "intToCodableCollectionInt" + setCodable(forKey: keyName, data: CodableBag([int])) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], int) + let newInt = Int.max + Defaults[key]?[0] = newInt + XCTAssertEqual(Defaults[key]?[0], newInt) + } + + func testUIntToNativeUInt() { + let uInt = UInt.min + let keyName = "uIntToNativeUInt" + setCodable(forKey: keyName, data: uInt) + let key = Defaults.Key(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key], uInt) + let newUInt = UInt.max + Defaults[key] = newUInt + XCTAssertEqual(Defaults[key], newUInt) + } + + func testUIntToNativeCollectionUInt() { + let uInt = UInt.min + let keyName = "uIntToNativeCollectionUInt" + setCodable(forKey: keyName, data: [uInt]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], uInt) + let newUInt = UInt.max + Defaults[key]?[0] = newUInt + XCTAssertEqual(Defaults[key]?[0], newUInt) + } + + func testUIntToCodableCollectionUInt() { + let uInt = UInt.min + let keyName = "uIntToCodableCollectionUInt" + setCodable(forKey: keyName, data: CodableBag([uInt])) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], uInt) + let newUInt = UInt.max + Defaults[key]?[0] = newUInt + XCTAssertEqual(Defaults[key]?[0], newUInt) + } + + func testDoubleToNativeDouble() { + let double = Double.zero + let keyName = "doubleToNativeDouble" + setCodable(forKey: keyName, data: double) + let key = Defaults.Key(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key], double) + let newDouble = Double.infinity + Defaults[key] = newDouble + XCTAssertEqual(Defaults[key], newDouble) + } + + func testDoubleToNativeCollectionDouble() { + let double = Double.zero + let keyName = "doubleToNativeCollectionDouble" + setCodable(forKey: keyName, data: [double]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], double) + let newDouble = Double.infinity + Defaults[key]?[0] = newDouble + XCTAssertEqual(Defaults[key]?[0], newDouble) + } + + func testDoubleToCodableCollectionDouble() { + let double = Double.zero + let keyName = "doubleToCodableCollectionDouble" + setCodable(forKey: keyName, data: CodableBag([double])) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], double) + let newDouble = Double.infinity + Defaults[key]?[0] = newDouble + XCTAssertEqual(Defaults[key]?[0], newDouble) + } + + func testFloatToNativeFloat() { + let float = Float.zero + let keyName = "floatToNativeFloat" + setCodable(forKey: keyName, data: float) + let key = Defaults.Key(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key], float) + let newFloat = Float.infinity + Defaults[key] = newFloat + XCTAssertEqual(Defaults[key], newFloat) + } + + func testFloatToNativeCollectionFloat() { + let float = Float.zero + let keyName = "floatToNativeCollectionFloat" + setCodable(forKey: keyName, data: [float]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], float) + let newFloat = Float.infinity + Defaults[key]?[0] = newFloat + XCTAssertEqual(Defaults[key]?[0], newFloat) + } + + func testFloatToCodableCollectionFloat() { + let float = Float.zero + let keyName = "floatToCodableCollectionFloat" + setCodable(forKey: keyName, data: CodableBag([float])) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], float) + let newFloat = Float.infinity + Defaults[key]?[0] = newFloat + XCTAssertEqual(Defaults[key]?[0], newFloat) + } + + func testCGFloatToNativeCGFloat() { + let cgFloat = CGFloat.zero + let keyName = "cgFloatToNativeCGFloat" + setCodable(forKey: keyName, data: cgFloat) + let key = Defaults.Key(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key], cgFloat) + let newCGFloat = CGFloat.infinity + Defaults[key] = newCGFloat + XCTAssertEqual(Defaults[key], newCGFloat) + } + + func testCGFloatToNativeCollectionCGFloat() { + let cgFloat = CGFloat.zero + let keyName = "cgFloatToNativeCollectionCGFloat" + setCodable(forKey: keyName, data: [cgFloat]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], cgFloat) + let newCGFloat = CGFloat.infinity + Defaults[key]?[0] = newCGFloat + XCTAssertEqual(Defaults[key]?[0], newCGFloat) + } + + func testCGFloatToCodableCollectionCGFloat() { + let cgFloat = CGFloat.zero + let keyName = "cgFloatToCodableCollectionCGFloat" + setCodable(forKey: keyName, data: CodableBag([cgFloat])) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], cgFloat) + let newCGFloat = CGFloat.infinity + Defaults[key]?[0] = newCGFloat + XCTAssertEqual(Defaults[key]?[0], newCGFloat) + } + + func testInt8ToNativeInt8() { + let int8 = Int8.min + let keyName = "int8ToNativeInt8" + setCodable(forKey: keyName, data: int8) + let key = Defaults.Key(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key], int8) + let newInt8 = Int8.max + Defaults[key] = newInt8 + XCTAssertEqual(Defaults[key], newInt8) + } + + func testInt8ToNativeCollectionInt8() { + let int8 = Int8.min + let keyName = "int8ToNativeCollectionInt8" + setCodable(forKey: keyName, data: [int8]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], int8) + let newInt8 = Int8.max + Defaults[key]?[0] = newInt8 + XCTAssertEqual(Defaults[key]?[0], newInt8) + } + + func testInt8ToCodableCollectionInt8() { + let int8 = Int8.min + let keyName = "int8ToCodableCollectionInt8" + setCodable(forKey: keyName, data: CodableBag([int8])) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], int8) + let newInt8 = Int8.max + Defaults[key]?[0] = newInt8 + XCTAssertEqual(Defaults[key]?[0], newInt8) + } + + func testUInt8ToNativeUInt8() { + let uInt8 = UInt8.min + let keyName = "uInt8ToNativeUInt8" + setCodable(forKey: keyName, data: uInt8) + let key = Defaults.Key(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key], uInt8) + let newUInt8 = UInt8.max + Defaults[key] = newUInt8 + XCTAssertEqual(Defaults[key], newUInt8) + } + + func testUInt8ToNativeCollectionUInt8() { + let uInt8 = UInt8.min + let keyName = "uInt8ToNativeCollectionUInt8" + setCodable(forKey: keyName, data: [uInt8]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], uInt8) + let newUInt8 = UInt8.max + Defaults[key]?[0] = newUInt8 + XCTAssertEqual(Defaults[key]?[0], newUInt8) + } + + func testUInt8ToCodableCollectionUInt8() { + let uInt8 = UInt8.min + let keyName = "uInt8ToCodableCollectionUInt8" + setCodable(forKey: keyName, data: CodableBag([uInt8])) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], uInt8) + let newUInt8 = UInt8.max + Defaults[key]?[0] = newUInt8 + XCTAssertEqual(Defaults[key]?[0], newUInt8) + } + + func testInt16ToNativeInt16() { + let int16 = Int16.min + let keyName = "int16ToNativeInt16" + setCodable(forKey: keyName, data: int16) + let key = Defaults.Key(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key], int16) + let newInt16 = Int16.max + Defaults[key] = newInt16 + XCTAssertEqual(Defaults[key], newInt16) + } + + func testInt16ToNativeCollectionInt16() { + let int16 = Int16.min + let keyName = "int16ToNativeCollectionInt16" + setCodable(forKey: keyName, data: [int16]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], int16) + let newInt16 = Int16.max + Defaults[key]?[0] = newInt16 + XCTAssertEqual(Defaults[key]?[0], newInt16) + } + + func testInt16ToCodableCollectionInt16() { + let int16 = Int16.min + let keyName = "int16ToCodableCollectionInt16" + setCodable(forKey: keyName, data: CodableBag([int16])) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], int16) + let newInt16 = Int16.max + Defaults[key]?[0] = newInt16 + XCTAssertEqual(Defaults[key]?[0], newInt16) + } + + func testUInt16ToNativeUInt16() { + let uInt16 = UInt16.min + let keyName = "uInt16ToNativeUInt16" + setCodable(forKey: keyName, data: uInt16) + let key = Defaults.Key(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key], uInt16) + let newUInt16 = UInt16.max + Defaults[key] = newUInt16 + XCTAssertEqual(Defaults[key], newUInt16) + } + + func testUInt16ToNativeCollectionUInt16() { + let uInt16 = UInt16.min + let keyName = "uInt16ToNativeCollectionUInt16" + setCodable(forKey: keyName, data: [uInt16]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], uInt16) + let newUInt16 = UInt16.max + Defaults[key]?[0] = newUInt16 + XCTAssertEqual(Defaults[key]?[0], newUInt16) + } + + func testUInt16ToCodableCollectionUInt16() { + let uInt16 = UInt16.min + let keyName = "uInt16ToCodableCollectionUInt16" + setCodable(forKey: keyName, data: CodableBag([uInt16])) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], uInt16) + let newUInt16 = UInt16.max + Defaults[key]?[0] = newUInt16 + XCTAssertEqual(Defaults[key]?[0], newUInt16) + } + + func testInt32ToNativeInt32() { + let int32 = Int32.min + let keyName = "int32ToNativeInt32" + setCodable(forKey: keyName, data: int32) + let key = Defaults.Key(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key], int32) + let newInt32 = Int32.max + Defaults[key] = newInt32 + XCTAssertEqual(Defaults[key], newInt32) + } + + func testInt32ToNativeCollectionInt32() { + let int32 = Int32.min + let keyName = "int32ToNativeCollectionInt32" + setCodable(forKey: keyName, data: [int32]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], int32) + let newInt32 = Int32.max + Defaults[key]?[0] = newInt32 + XCTAssertEqual(Defaults[key]?[0], newInt32) + } + + func testInt32ToCodableCollectionInt32() { + let int32 = Int32.min + let keyName = "int32ToCodableCollectionInt32" + setCodable(forKey: keyName, data: CodableBag([int32])) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], int32) + let newInt32 = Int32.max + Defaults[key]?[0] = newInt32 + XCTAssertEqual(Defaults[key]?[0], newInt32) + } + + func testUInt32ToNativeUInt32() { + let uInt32 = UInt32.min + let keyName = "uInt32ToNativeUInt32" + setCodable(forKey: keyName, data: uInt32) + let key = Defaults.Key(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key], uInt32) + let newUInt32 = UInt32.max + Defaults[key] = newUInt32 + XCTAssertEqual(Defaults[key], newUInt32) + } + + func testUInt32ToNativeCollectionUInt32() { + let uInt32 = UInt32.min + let keyName = "uInt32ToNativeCollectionUInt32" + setCodable(forKey: keyName, data: [uInt32]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], uInt32) + let newUInt32 = UInt32.max + Defaults[key]?[0] = newUInt32 + XCTAssertEqual(Defaults[key]?[0], newUInt32) + } + + func testUInt32ToCodableCollectionUInt32() { + let uInt32 = UInt32.min + let keyName = "uInt32ToCodableCollectionUInt32" + setCodable(forKey: keyName, data: CodableBag([uInt32])) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], uInt32) + let newUInt32 = UInt32.max + Defaults[key]?[0] = newUInt32 + XCTAssertEqual(Defaults[key]?[0], newUInt32) + } + + func testInt64ToNativeInt64() { + let int64 = Int64.min + let keyName = "int64ToNativeInt64" + setCodable(forKey: keyName, data: int64) + let key = Defaults.Key(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key], int64) + let newInt64 = Int64.max + Defaults[key] = newInt64 + XCTAssertEqual(Defaults[key], newInt64) + } + + func testInt64ToNativeCollectionInt64() { + let int64 = Int64.min + let keyName = "int64ToNativeCollectionInt64" + setCodable(forKey: keyName, data: [int64]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], int64) + let newInt64 = Int64.max + Defaults[key]?[0] = newInt64 + XCTAssertEqual(Defaults[key]?[0], newInt64) + } + + func testInt64ToCodableCollectionInt64() { + let int64 = Int64.min + let keyName = "int64ToCodableCollectionInt64" + setCodable(forKey: keyName, data: CodableBag([int64])) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], int64) + let newInt64 = Int64.max + Defaults[key]?[0] = newInt64 + XCTAssertEqual(Defaults[key]?[0], newInt64) + } + + func testUInt64ToNativeUInt64() { + let uInt64 = UInt64.min + let keyName = "uInt64ToNativeUInt64" + setCodable(forKey: keyName, data: uInt64) + let key = Defaults.Key(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key], uInt64) + let newUInt64 = UInt64.max + Defaults[key] = newUInt64 + XCTAssertEqual(Defaults[key], newUInt64) + } + + func testUInt64ToNativeCollectionUInt64() { + let uInt64 = UInt64.min + let keyName = "uInt64ToNativeCollectionUInt64" + setCodable(forKey: keyName, data: [uInt64]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], uInt64) + let newUInt64 = UInt64.max + Defaults[key]?[0] = newUInt64 + XCTAssertEqual(Defaults[key]?[0], newUInt64) + } + + func testUInt64ToCodableCollectionUInt64() { + let uInt64 = UInt64.min + let keyName = "uInt64ToCodableCollectionUInt64" + setCodable(forKey: keyName, data: CodableBag([uInt64])) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], uInt64) + let newUInt64 = UInt64.max + Defaults[key]?[0] = newUInt64 + XCTAssertEqual(Defaults[key]?[0], newUInt64) + } + + func testArrayURLToNativeArrayURL() { + let url = URL(string: "https://sindresorhus.com")! + let keyName = "arrayURLToNativeArrayURL" + setCodable(forKey: keyName, data: [url]) + let key = Defaults.Key<[URL]?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], url) + let newURL = URL(string: "https://example.com")! + Defaults[key]?.append(newURL) + XCTAssertEqual(Defaults[key]?[1], newURL) + } + + func testArrayURLToNativeCollectionURL() { + let url = URL(string: "https://sindresorhus.com")! + let keyName = "arrayURLToNativeCollectionURL" + setCodable(forKey: keyName, data: [url]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], url) + let newURL = URL(string: "https://example.com")! + Defaults[key]?.insert(element: newURL, at: 1) + XCTAssertEqual(Defaults[key]?[1], newURL) + } + + func testArrayToNativeArray() { + let keyName = "arrayToNativeArrayKey" + setCodable(forKey: keyName, data: ["a", "b", "c"]) + let key = Defaults.Key<[String]>(keyName, default: []) + Defaults.migrate(key, to: .v5) + let newValue = "d" + Defaults[key].append(newValue) + XCTAssertEqual(Defaults[key][0], "a") + XCTAssertEqual(Defaults[key][1], "b") + XCTAssertEqual(Defaults[key][2], "c") + XCTAssertEqual(Defaults[key][3], newValue) + } + + func testArrayToNativeStaticOptionalArray() { + let keyName = "arrayToNativeStaticArrayKey" + setCodable(forKey: keyName, data: ["a", "b", "c"]) + Defaults.migrate(.nativeArray, to: .v5) + let newValue = "d" + Defaults[.nativeArray]?.append(newValue) + XCTAssertEqual(Defaults[.nativeArray]?[0], "a") + XCTAssertEqual(Defaults[.nativeArray]?[1], "b") + XCTAssertEqual(Defaults[.nativeArray]?[2], "c") + XCTAssertEqual(Defaults[.nativeArray]?[3], newValue) + } + + func testArrayToNativeOptionalArray() { + let keyName = "arrayToNativeArrayKey" + setCodable(forKey: keyName, data: ["a", "b", "c"]) + let key = Defaults.Key<[String]?>(keyName) + Defaults.migrate(key, to: .v5) + let newValue = "d" + Defaults[key]?.append(newValue) + XCTAssertEqual(Defaults[key]?[0], "a") + XCTAssertEqual(Defaults[key]?[1], "b") + XCTAssertEqual(Defaults[key]?[2], "c") + XCTAssertEqual(Defaults[key]?[3], newValue) + } + + func testArrayDictionaryStringIntToNativeArray() { + let keyName = "arrayDictionaryStringIntToNativeArray" + setCodable(forKey: keyName, data: [["a": 0, "b": 1]]) + let key = Defaults.Key<[[String: Int]]?>(keyName) + Defaults.migrate(key, to: .v5) + let newValue = 2 + let newDictionary = ["d": 3] + Defaults[key]?[0]["c"] = newValue + Defaults[key]?.append(newDictionary) + XCTAssertEqual(Defaults[key]?[0]["a"], 0) + XCTAssertEqual(Defaults[key]?[0]["b"], 1) + XCTAssertEqual(Defaults[key]?[0]["c"], newValue) + XCTAssertEqual(Defaults[key]?[1]["d"], newDictionary["d"]) + } + + func testArrayToNativeSet() { + let keyName = "arrayToNativeSet" + setCodable(forKey: keyName, data: ["a", "b", "c"]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + let newValue = "d" + Defaults[key]?.insert(newValue) + XCTAssertEqual(Defaults[key], Set(["a", "b", "c", "d"])) + } + + func testArrayToNativeCollectionType() { + let string = "Hello World!" + let keyName = "arrayToNativeCollectionType" + setCodable(forKey: keyName, data: [string]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], string) + let newString = "Hank Chen" + Defaults[key]?[0] = newString + XCTAssertEqual(Defaults[key]?[0], newString) + } + + func testArrayToCodableCollectionType() { + let keyName = "arrayToCodableCollectionType" + setCodable(forKey: keyName, data: CodableBag(["a", "b", "c"])) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + let newValue = "d" + Defaults[key]?.insert(element: newValue, at: 3) + XCTAssertEqual(Defaults[key]?[0], "a") + XCTAssertEqual(Defaults[key]?[1], "b") + XCTAssertEqual(Defaults[key]?[2], "c") + XCTAssertEqual(Defaults[key]?[3], newValue) + } + + func testArrayAndCodableElementToNativeCollectionType() { + let keyName = "arrayAndCodableElementToNativeCollectionType" + setCodable(forKey: keyName, data: [CodableTimeZone(id: "0", name: "Asia/Taipei")]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0].id, "0") + let newName = "Asia/Tokyo" + Defaults[key]?.insert(element: .init(id: "1", name: newName), at: 1) + XCTAssertEqual(Defaults[key]?[1].name, newName) + } + + func testArrayAndCodableElementToNativeSetAlgebraType() { + let keyName = "arrayAndCodableElementToNativeSetAlgebraType" + setCodable(forKey: keyName, data: [CodableTimeZone(id: "0", name: "Asia/Taipei")]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?.store.first?.id, "0") + let newName = "Asia/Tokyo" + Defaults[key]?.insert(.init(id: "1", name: newName)) + XCTAssertEqual(Set([TimeZone(id: "0", name: "Asia/Taipei"), TimeZone(id: "1", name: newName)]), Defaults[key]?.store) + } + + func testCodableToNativeType() { + let keyName = "codableCodableToNativeType" + setCodable(forKey: keyName, data: CodableTimeZone(id: "0", name: "Asia/Taipei")) + let key = Defaults.Key(keyName, default: .init(id: "1", name: "Asia/Tokio")) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key].id, "0") + let newName = "Asia/Tokyo" + Defaults[key].name = newName + XCTAssertEqual(Defaults[key].name, newName) + } + + func testCodableToNativeOptionalType() { + let keyName = "codableCodableToNativeOptionalType" + setCodable(forKey: keyName, data: CodableTimeZone(id: "0", name: "Asia/Taipei")) + let key = Defaults.Key(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?.id, "0") + let newName = "Asia/Tokyo" + Defaults[key]?.name = newName + XCTAssertEqual(Defaults[key]?.name, newName) + } + + func testArrayAndCodableElementToNativeArray() { + let keyName = "codableArrayAndCodableElementToNativeArray" + setCodable(forKey: keyName, data: [CodableTimeZone(id: "0", name: "Asia/Taipei")]) + let key = Defaults.Key<[TimeZone]?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0].id, "0") + let newName = "Asia/Tokyo" + Defaults[key]?[0].name = newName + XCTAssertEqual(Defaults[key]?[0].name, newName) + } + + func testArrayAndCodableElementToNativeSet() { + let keyName = "arrayAndCodableElementToNativeSet" + setCodable(forKey: keyName, data: [CodableTimeZone(id: "0", name: "Asia/Taipei")]) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key], Set([TimeZone(id: "0", name: "Asia/Taipei")])) + let newId = "1" + let newName = "Asia/Tokyo" + Defaults[key]?.insert(.init(id: newId, name: newName)) + XCTAssertEqual(Defaults[key], Set([TimeZone(id: "0", name: "Asia/Taipei"), TimeZone(id: newId, name: newName)])) + } + + func testCodableToNativeCodableOptionalType() { + let keyName = "codableToNativeCodableOptionalType" + setCodable(forKey: keyName, data: ChosenTimeZone(id: "0", name: "Asia/Taipei")) + let key = Defaults.Key(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?.id, "0") + let newName = "Asia/Tokyo" + Defaults[key]?.name = newName + XCTAssertEqual(Defaults[key]?.name, newName) + } + + func testCodableArrayToNativeCodableArrayType() { + let keyName = "codableToNativeCodableArrayType" + setCodable(forKey: keyName, data: [ChosenTimeZone(id: "0", name: "Asia/Taipei")]) + let key = Defaults.Key<[ChosenTimeZone]?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0].id, "0") + let newName = "Asia/Tokyo" + Defaults[key]?[0].name = newName + XCTAssertEqual(Defaults[key]?[0].name, newName) + } + + func testCodableArrayToNativeCollectionType() { + let keyName = "codableToNativeCollectionType" + setCodable(forKey: keyName, data: CodableBag([ChosenTimeZone(id: "0", name: "Asia/Taipei")])) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0].id, "0") + let newName = "Asia/Tokyo" + Defaults[key]?[0].name = newName + XCTAssertEqual(Defaults[key]?[0].name, newName) + } + + func testDictionaryToNativelyDictionary() { + let keyName = "codableDictionaryToNativelyDictionary" + setCodable(forKey: keyName, data: ["Hank": "Chen"]) + let key = Defaults.Key<[String: String]?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?["Hank"], "Chen") + } + + func testDictionaryAndCodableValueToNativeDictionary() { + let keyName = "codableArrayAndCodableElementToNativeArray" + setCodable(forKey: keyName, data: ["0": CodableTimeZone(id: "0", name: "Asia/Taipei")]) + let key = Defaults.Key<[String: TimeZone]?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?["0"]?.id, "0") + let newName = "Asia/Tokyo" + Defaults[key]?["0"]?.name = newName + XCTAssertEqual(Defaults[key]?["0"]?.name, newName) + } + + func testDictionaryCodableKeyAndCodableValueToNativeDictionary() { + let keyName = "dictionaryCodableKeyAndCodableValueToNativeDictionary" + setCodable(forKey: keyName, data: [123: CodableTimeZone(id: "0", name: "Asia/Taipei")]) + let key = Defaults.Key<[UInt32: TimeZone]?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[123]?.id, "0") + let newName = "Asia/Tokyo" + Defaults[key]?[123]?.name = newName + XCTAssertEqual(Defaults[key]?[123]?.name, newName) + } + + func testDictionaryCustomKeyAndCodableValueToNativeDictionary() { + let keyName = "dictionaryCustomAndCodableValueToNativeDictionary" + setCodable(forKey: keyName, data: [1234: CodableTimeZone(id: "0", name: "Asia/Taipei")]) + let key = Defaults.Key<[UniqueID: TimeZone]?>(keyName) + Defaults.migrate(key, to: .v5) + let id = UniqueID(id: 1234) + XCTAssertEqual(Defaults[key]?[id]?.id, "0") + let newName = "Asia/Tokyo" + Defaults[key]?[id]?.name = newName + XCTAssertEqual(Defaults[key]?[id]?.name, newName) + } + + func testNestedDictionaryCustomKeyAndCodableValueToNativeNestedDictionary() { + let keyName = "nestedDictionaryCustomKeyAndCodableValueToNativeNestedDictionary" + setCodable(forKey: keyName, data: [12_345: [1234: CodableTimeZone(id: "0", name: "Asia/Taipei")]]) + let key = Defaults.Key<[UniqueID: [UniqueID: TimeZone]]?>(keyName) + Defaults.migrate(key, to: .v5) + let firstId = UniqueID(id: 12_345) + let secondId = UniqueID(id: 1234) + XCTAssertEqual(Defaults[key]?[firstId]?[secondId]?.id, "0") + let newName = "Asia/Tokyo" + Defaults[key]?[firstId]?[secondId]?.name = newName + XCTAssertEqual(Defaults[key]?[firstId]?[secondId]?.name, newName) + } + + func testEnumToNativeEnum() { + let keyName = "enumToNativeEnum" + setCodable(forKey: keyName, data: CodableEnumForm.tenMinutes) + let key = Defaults.Key(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key], .tenMinutes) + Defaults[key] = .halfHour + XCTAssertEqual(Defaults[key], .halfHour) + } + + func testArrayEnumToNativeArrayEnum() { + let keyName = "arrayEnumToNativeArrayEnum" + setCodable(forKey: keyName, data: [CodableEnumForm.tenMinutes]) + let key = Defaults.Key<[EnumForm]?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?[0], .tenMinutes) + Defaults[key]?.append(.halfHour) + XCTAssertEqual(Defaults[key]?[1], .halfHour) + } + + func testArrayEnumToNativeSetEnum() { + let keyName = "arrayEnumToNativeSetEnum" + setCodable(forKey: keyName, data: Set([CodableEnumForm.tenMinutes])) + let key = Defaults.Key?>(keyName) + Defaults.migrate(key, to: .v5) + XCTAssertEqual(Defaults[key]?.first, .tenMinutes) + Defaults[key]?.insert(.halfHour) + XCTAssertEqual(Defaults[key], Set([.tenMinutes, .halfHour])) + } +} diff --git a/Tests/DefaultsTests/DefaultsNSColorTests.swift b/Tests/DefaultsTests/DefaultsNSColorTests.swift new file mode 100644 index 0000000..051d39f --- /dev/null +++ b/Tests/DefaultsTests/DefaultsNSColorTests.swift @@ -0,0 +1,306 @@ +import Foundation +import Defaults +import XCTest +import AppKit + +private let fixtureColor = NSColor(red: CGFloat(103) / CGFloat(0xFF), green: CGFloat(132) / CGFloat(0xFF), blue: CGFloat(255) / CGFloat(0xFF), alpha: 1) +private let fixtureColor1 = NSColor(red: CGFloat(255) / CGFloat(0xFF), green: CGFloat(241) / CGFloat(0xFF), blue: CGFloat(180) / CGFloat(0xFF), alpha: 1) +private let fixtureColor2 = NSColor(red: CGFloat(255) / CGFloat(0xFF), green: CGFloat(180) / CGFloat(0xFF), blue: CGFloat(194) / CGFloat(0xFF), alpha: 1) + +extension Defaults.Keys { + fileprivate static let color = Defaults.Key("NSColor", default: fixtureColor) + fileprivate static let colorArray = Defaults.Key<[NSColor]>("NSColorArray", default: [fixtureColor]) + fileprivate static let colorDictionary = Defaults.Key<[String: NSColor]>("NSColorArray", default: ["0": fixtureColor]) +} + +final class DefaultsNSColorTests: XCTestCase { + override func setUp() { + super.setUp() + Defaults.removeAll() + } + + override func tearDown() { + super.tearDown() + Defaults.removeAll() + } + + func testKey() { + let key = Defaults.Key("independentNSColorKey", default: fixtureColor) + XCTAssertTrue(Defaults[key].isEqual(fixtureColor)) + Defaults[key] = fixtureColor1 + XCTAssertTrue(Defaults[key].isEqual(fixtureColor1)) + } + + func testOptionalKey() { + let key = Defaults.Key("independentNSColorOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = fixtureColor + XCTAssertTrue(Defaults[key]?.isEqual(fixtureColor) ?? false) + } + + func testArrayKey() { + let key = Defaults.Key<[NSColor]>("independentNSColorArrayKey", default: [fixtureColor]) + XCTAssertTrue(Defaults[key][0].isEqual(fixtureColor)) + Defaults[key].append(fixtureColor1) + XCTAssertTrue(Defaults[key][1].isEqual(fixtureColor1)) + } + + func testArrayOptionalKey() { + let key = Defaults.Key<[NSColor]?>("independentNSColorOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = [fixtureColor] + Defaults[key]?.append(fixtureColor1) + XCTAssertTrue(Defaults[key]?[0].isEqual(fixtureColor) ?? false) + XCTAssertTrue(Defaults[key]?[1].isEqual(fixtureColor1) ?? false) + } + + func testNestedArrayKey() { + let key = Defaults.Key<[[NSColor]]>("independentNSColorNestedArrayKey", default: [[fixtureColor]]) + XCTAssertTrue(Defaults[key][0][0].isEqual(fixtureColor)) + Defaults[key][0].append(fixtureColor1) + Defaults[key].append([fixtureColor2]) + XCTAssertTrue(Defaults[key][0][1].isEqual(fixtureColor1)) + XCTAssertTrue(Defaults[key][1][0].isEqual(fixtureColor2)) + } + + func testArrayDictionaryKey() { + let key = Defaults.Key<[[String: NSColor]]>("independentNSColorArrayDictionaryKey", default: [["0": fixtureColor]]) + XCTAssertTrue(Defaults[key][0]["0"]?.isEqual(fixtureColor) ?? false) + Defaults[key][0]["1"] = fixtureColor1 + Defaults[key].append(["0": fixtureColor2]) + XCTAssertTrue(Defaults[key][0]["1"]?.isEqual(fixtureColor1) ?? false) + XCTAssertTrue(Defaults[key][1]["0"]?.isEqual(fixtureColor2) ?? false) + } + + func testDictionaryKey() { + let key = Defaults.Key<[String: NSColor]>("independentNSColorDictionaryKey", default: ["0": fixtureColor]) + XCTAssertTrue(Defaults[key]["0"]?.isEqual(fixtureColor) ?? false) + Defaults[key]["1"] = fixtureColor1 + XCTAssertTrue(Defaults[key]["1"]?.isEqual(fixtureColor1) ?? false) + } + + func testDictionaryOptionalKey() { + let key = Defaults.Key<[String: NSColor]?>("independentNSColorDictionaryOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = ["0": fixtureColor] + Defaults[key]?["1"] = fixtureColor1 + XCTAssertTrue(Defaults[key]?["0"]?.isEqual(fixtureColor) ?? false) + XCTAssertTrue(Defaults[key]?["1"]?.isEqual(fixtureColor1) ?? false) + } + + func testDictionaryArrayKey() { + let key = Defaults.Key<[String: [NSColor]]>("independentNSColorDictionaryArrayKey", default: ["0": [fixtureColor]]) + XCTAssertTrue(Defaults[key]["0"]?[0].isEqual(fixtureColor) ?? false) + Defaults[key]["0"]?.append(fixtureColor1) + Defaults[key]["1"] = [fixtureColor2] + XCTAssertTrue(Defaults[key]["0"]?[1].isEqual(fixtureColor1) ?? false) + XCTAssertTrue(Defaults[key]["1"]?[0].isEqual(fixtureColor2) ?? false) + } + + func testType() { + XCTAssert(Defaults[.color].isEqual(fixtureColor)) + Defaults[.color] = fixtureColor1 + XCTAssert(Defaults[.color].isEqual(fixtureColor1)) + } + + func testArrayType() { + XCTAssertTrue(Defaults[.colorArray][0].isEqual(fixtureColor)) + Defaults[.colorArray][0] = fixtureColor1 + XCTAssertTrue(Defaults[.colorArray][0].isEqual(fixtureColor1)) + } + + func testDictionaryType() { + XCTAssertTrue(Defaults[.colorDictionary]["0"]?.isEqual(fixtureColor) ?? false) + Defaults[.colorDictionary]["0"] = fixtureColor1 + XCTAssertTrue(Defaults[.colorDictionary]["0"]?.isEqual(fixtureColor1) ?? false) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveKeyCombine() { + let key = Defaults.Key("observeNSColorKeyCombine", default: fixtureColor) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let cancellable = publisher.sink { tuples in + for (index, expected) in [(fixtureColor, fixtureColor1), (fixtureColor1, fixtureColor)].enumerated() { + XCTAssertTrue(expected.0.isEqual(tuples[index].0)) + XCTAssertTrue(expected.1.isEqual(tuples[index].1)) + } + + expect.fulfill() + } + + Defaults[key] = fixtureColor1 + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveOptionalKeyCombine() { + let key = Defaults.Key("observeNSColorOptionalKeyCombine") + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(3) + + let expectedValue: [(NSColor?, NSColor?)] = [(nil, fixtureColor), (fixtureColor, fixtureColor1), (fixtureColor1, nil)] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + guard let oldValue = expected.0 else { + XCTAssertNil(tuples[index].0) + continue + } + guard let newValue = expected.1 else { + XCTAssertNil(tuples[index].1) + continue + } + XCTAssertTrue(oldValue.isEqual(tuples[index].0)) + XCTAssertTrue(newValue.isEqual(tuples[index].1)) + } + + expect.fulfill() + } + + Defaults[key] = fixtureColor + Defaults[key] = fixtureColor1 + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveArrayKeyCombine() { + let key = Defaults.Key<[NSColor]>("observeNSColorArrayKeyCombine", default: [fixtureColor]) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let cancellable = publisher.sink { tuples in + for (index, expected) in [(fixtureColor, fixtureColor1), (fixtureColor1, fixtureColor)].enumerated() { + XCTAssertTrue(expected.0.isEqual(tuples[index].0[0])) + XCTAssertTrue(expected.1.isEqual(tuples[index].1[0])) + } + + expect.fulfill() + } + + Defaults[key][0] = fixtureColor1 + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveDictionaryKeyCombine() { + let key = Defaults.Key<[String: NSColor]>("observeNSColorDictionaryKeyCombine", default: ["0": fixtureColor]) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let cancellable = publisher.sink { tuples in + for (index, expected) in [(fixtureColor, fixtureColor1), (fixtureColor1, fixtureColor)].enumerated() { + XCTAssertTrue(expected.0.isEqual(tuples[index].0["0"])) + XCTAssertTrue(expected.1.isEqual(tuples[index].1["0"])) + } + + expect.fulfill() + } + + Defaults[key]["0"] = fixtureColor1 + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + func testObserveKey() { + let key = Defaults.Key("observeNSColorKey", default: fixtureColor) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertTrue(change.oldValue.isEqual(fixtureColor)) + XCTAssertTrue(change.newValue.isEqual(fixtureColor1)) + observation.invalidate() + expect.fulfill() + } + + Defaults[key] = fixtureColor1 + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveOptionalKey() { + let key = Defaults.Key("observeNSColorOptionalKey") + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertNil(change.oldValue) + XCTAssertTrue(change.newValue?.isEqual(fixtureColor) ?? false) + observation.invalidate() + expect.fulfill() + } + + Defaults[key] = fixtureColor + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveArrayKey() { + let key = Defaults.Key<[NSColor]>("observeNSColorArrayKey", default: [fixtureColor]) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertTrue(change.oldValue[0].isEqual(fixtureColor)) + XCTAssertTrue(change.newValue[0].isEqual(fixtureColor)) + XCTAssertTrue(change.newValue[1].isEqual(fixtureColor1)) + observation.invalidate() + expect.fulfill() + } + + Defaults[key].append(fixtureColor1) + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveDictionaryKey() { + let key = Defaults.Key<[String: NSColor]>("observeNSColorDictionaryKey", default: ["0": fixtureColor]) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertTrue(change.oldValue["0"]?.isEqual(fixtureColor) ?? false) + XCTAssertTrue(change.newValue["0"]?.isEqual(fixtureColor) ?? false) + XCTAssertTrue(change.newValue["1"]?.isEqual(fixtureColor1) ?? false) + observation.invalidate() + expect.fulfill() + } + + Defaults[key]["1"] = fixtureColor1 + observation.invalidate() + + waitForExpectations(timeout: 10) + } +} diff --git a/Tests/DefaultsTests/DefaultsNSSecureCodingTests.swift b/Tests/DefaultsTests/DefaultsNSSecureCodingTests.swift new file mode 100644 index 0000000..b10f2fb --- /dev/null +++ b/Tests/DefaultsTests/DefaultsNSSecureCodingTests.swift @@ -0,0 +1,474 @@ +import Foundation +import CoreData +import Defaults +import XCTest + +@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) +private final class ExamplePersistentHistory: NSPersistentHistoryToken, Defaults.Serializable { + let value: String + + init(value: String) { + self.value = value + super.init() + } + + required init?(coder: NSCoder) { + self.value = coder.decodeObject(forKey: "value") as! String + super.init() + } + + override func encode(with coder: NSCoder) { + coder.encode(value, forKey: "value") + } + + override class var supportsSecureCoding: Bool { true } +} + +// NSSecureCoding +@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) +private let persistentHistoryValue = ExamplePersistentHistory(value: "ExampleToken") + +@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) +extension Defaults.Keys { + fileprivate static let persistentHistory = Key("persistentHistory", default: persistentHistoryValue) + fileprivate static let persistentHistoryArray = Key<[ExamplePersistentHistory]>("array_persistentHistory", default: [persistentHistoryValue]) + fileprivate static let persistentHistoryDictionary = Key<[String: ExamplePersistentHistory]>("dictionary_persistentHistory", default: ["0": persistentHistoryValue]) +} + +@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) +final class DefaultsNSSecureCodingTests: XCTestCase { + override func setUp() { + super.setUp() + Defaults.removeAll() + } + + override func tearDown() { + super.tearDown() + Defaults.removeAll() + } + + func testKey() { + let key = Defaults.Key("independentNSSecureCodingKey", default: persistentHistoryValue) + XCTAssertEqual(Defaults[key].value, persistentHistoryValue.value) + let newPersistentHistory = ExamplePersistentHistory(value: "NewValue") + Defaults[key] = newPersistentHistory + XCTAssertEqual(Defaults[key].value, newPersistentHistory.value) + } + + func testOptionalKey() { + let key = Defaults.Key("independentNSSecureCodingOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = persistentHistoryValue + XCTAssertEqual(Defaults[key]?.value, persistentHistoryValue.value) + Defaults[key] = nil + XCTAssertNil(Defaults[key]) + let newPersistentHistory = ExamplePersistentHistory(value: "NewValue") + Defaults[key] = newPersistentHistory + XCTAssertEqual(Defaults[key]?.value, newPersistentHistory.value) + } + + func testArrayKey() { + let key = Defaults.Key<[ExamplePersistentHistory]>("independentNSSecureCodingArrayKey", default: [persistentHistoryValue]) + XCTAssertEqual(Defaults[key][0].value, persistentHistoryValue.value) + let newPersistentHistory1 = ExamplePersistentHistory(value: "NewValue1") + Defaults[key].append(newPersistentHistory1) + XCTAssertEqual(Defaults[key][1].value, newPersistentHistory1.value) + let newPersistentHistory2 = ExamplePersistentHistory(value: "NewValue2") + Defaults[key][1] = newPersistentHistory2 + XCTAssertEqual(Defaults[key][1].value, newPersistentHistory2.value) + XCTAssertEqual(Defaults[key][0].value, persistentHistoryValue.value) + } + + func testArrayOptionalKey() { + let key = Defaults.Key<[ExamplePersistentHistory]?>("independentNSSecureCodingArrayOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = [persistentHistoryValue] + XCTAssertEqual(Defaults[key]?[0].value, persistentHistoryValue.value) + Defaults[key] = nil + XCTAssertNil(Defaults[key]) + } + + func testNestedArrayKey() { + let key = Defaults.Key<[[ExamplePersistentHistory]]>("independentNSSecureCodingNestedArrayKey", default: [[persistentHistoryValue]]) + XCTAssertEqual(Defaults[key][0][0].value, persistentHistoryValue.value) + let newPersistentHistory1 = ExamplePersistentHistory(value: "NewValue1") + Defaults[key][0].append(newPersistentHistory1) + let newPersistentHistory2 = ExamplePersistentHistory(value: "NewValue2") + Defaults[key].append([newPersistentHistory2]) + XCTAssertEqual(Defaults[key][0][1].value, newPersistentHistory1.value) + XCTAssertEqual(Defaults[key][1][0].value, newPersistentHistory2.value) + } + + func testArrayDictionaryKey() { + let key = Defaults.Key<[[String: ExamplePersistentHistory]]>("independentNSSecureCodingArrayDictionaryKey", default: [["0": persistentHistoryValue]]) + XCTAssertEqual(Defaults[key][0]["0"]?.value, persistentHistoryValue.value) + let newPersistentHistory1 = ExamplePersistentHistory(value: "NewValue1") + Defaults[key][0]["1"] = newPersistentHistory1 + let newPersistentHistory2 = ExamplePersistentHistory(value: "NewValue2") + Defaults[key].append(["0": newPersistentHistory2]) + XCTAssertEqual(Defaults[key][0]["1"]?.value, newPersistentHistory1.value) + XCTAssertEqual(Defaults[key][1]["0"]?.value, newPersistentHistory2.value) + } + + func testDictionaryKey() { + let key = Defaults.Key<[String: ExamplePersistentHistory]>("independentNSSecureCodingDictionaryKey", default: ["0": persistentHistoryValue]) + XCTAssertEqual(Defaults[key]["0"]?.value, persistentHistoryValue.value) + let newPersistentHistory1 = ExamplePersistentHistory(value: "NewValue1") + Defaults[key]["1"] = newPersistentHistory1 + XCTAssertEqual(Defaults[key]["1"]?.value, newPersistentHistory1.value) + let newPersistentHistory2 = ExamplePersistentHistory(value: "NewValue2") + Defaults[key]["1"] = newPersistentHistory2 + XCTAssertEqual(Defaults[key]["1"]?.value, newPersistentHistory2.value) + XCTAssertEqual(Defaults[key]["0"]?.value, persistentHistoryValue.value) + } + + func testDictionaryOptionalKey() { + let key = Defaults.Key<[String: ExamplePersistentHistory]?>("independentNSSecureCodingDictionaryOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = ["0": persistentHistoryValue] + XCTAssertEqual(Defaults[key]?["0"]?.value, persistentHistoryValue.value) + } + + func testDictionaryArrayKey() { + let key = Defaults.Key<[String: [ExamplePersistentHistory]]>("independentNSSecureCodingDictionaryArrayKey", default: ["0": [persistentHistoryValue]]) + XCTAssertEqual(Defaults[key]["0"]?[0].value, persistentHistoryValue.value) + let newPersistentHistory1 = ExamplePersistentHistory(value: "NewValue1") + Defaults[key]["0"]?.append(newPersistentHistory1) + let newPersistentHistory2 = ExamplePersistentHistory(value: "NewValue2") + Defaults[key]["1"] = [newPersistentHistory2] + XCTAssertEqual(Defaults[key]["0"]?[1].value, newPersistentHistory1.value) + XCTAssertEqual(Defaults[key]["1"]?[0].value, newPersistentHistory2.value) + } + + func testType() { + XCTAssertEqual(Defaults[.persistentHistory].value, persistentHistoryValue.value) + let newPersistentHistory = ExamplePersistentHistory(value: "NewValue") + Defaults[.persistentHistory] = newPersistentHistory + XCTAssertEqual(Defaults[.persistentHistory].value, newPersistentHistory.value) + } + + func testArrayType() { + XCTAssertEqual(Defaults[.persistentHistoryArray][0].value, persistentHistoryValue.value) + let newPersistentHistory = ExamplePersistentHistory(value: "NewValue") + Defaults[.persistentHistoryArray][0] = newPersistentHistory + XCTAssertEqual(Defaults[.persistentHistoryArray][0].value, newPersistentHistory.value) + } + + func testDictionaryType() { + XCTAssertEqual(Defaults[.persistentHistoryDictionary]["0"]?.value, persistentHistoryValue.value) + let newPersistentHistory = ExamplePersistentHistory(value: "NewValue") + Defaults[.persistentHistoryDictionary]["0"] = newPersistentHistory + XCTAssertEqual(Defaults[.persistentHistoryDictionary]["0"]?.value, newPersistentHistory.value) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveKeyCombine() { + let key = Defaults.Key("observeNSSecureCodingKeyCombine", default: persistentHistoryValue) + let newPersistentHistory = ExamplePersistentHistory(value: "NewValue") + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue.value, $0.newValue.value) } + .collect(2) + + let cancellable = publisher.sink { tuples in + for (index, expected) in [(persistentHistoryValue.value, newPersistentHistory.value), (newPersistentHistory.value, persistentHistoryValue.value)].enumerated() { + XCTAssertEqual(expected.0, tuples[index].0) + XCTAssertEqual(expected.1, tuples[index].1) + } + + expect.fulfill() + } + + Defaults[key] = newPersistentHistory + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveOptionalKeyCombine() { + let key = Defaults.Key("observeNSSecureCodingOptionalKeyCombine") + let newPersistentHistory = ExamplePersistentHistory(value: "NewValue") + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue?.value, $0.newValue?.value) } + .collect(3) + + let expectedValue: [(ExamplePersistentHistory?, ExamplePersistentHistory?)] = [(nil, persistentHistoryValue), (persistentHistoryValue, newPersistentHistory), (newPersistentHistory, nil)] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0?.value, tuples[index].0) + XCTAssertEqual(expected.1?.value, tuples[index].1) + } + + expect.fulfill() + } + + Defaults[key] = persistentHistoryValue + Defaults[key] = newPersistentHistory + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveArrayKeyCombine() { + let key = Defaults.Key<[ExamplePersistentHistory]>("observeNSSecureCodingArrayKeyCombine", default: [persistentHistoryValue]) + let newPersistentHistory = ExamplePersistentHistory(value: "NewValue") + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let expectedValue: [(ExamplePersistentHistory, ExamplePersistentHistory)] = [(persistentHistoryValue, newPersistentHistory), (newPersistentHistory, persistentHistoryValue)] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0.value, tuples[index].0[0].value) + XCTAssertEqual(expected.1.value, tuples[index].1[0].value) + } + + expect.fulfill() + } + + Defaults[key][0] = newPersistentHistory + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveDictionaryKeyCombine() { + let key = Defaults.Key<[String: ExamplePersistentHistory]>("observeNSSecureCodingDictionaryKeyCombine", default: ["0": persistentHistoryValue]) + let newPersistentHistory = ExamplePersistentHistory(value: "NewValue") + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let expectedValue: [(ExamplePersistentHistory, ExamplePersistentHistory)] = [(persistentHistoryValue, newPersistentHistory), (newPersistentHistory, persistentHistoryValue)] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0.value, tuples[index].0["0"]?.value) + XCTAssertEqual(expected.1.value, tuples[index].1["0"]?.value) + } + + expect.fulfill() + } + + Defaults[key]["0"] = newPersistentHistory + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveMultipleNSSecureKeysCombine() { + let key1 = Defaults.Key("observeMultipleNSSecureCodingKey1", default: ExamplePersistentHistory(value: "TestValue")) + let key2 = Defaults.Key("observeMultipleNSSecureCodingKey2", default: ExamplePersistentHistory(value: "TestValue")) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults.publisher(keys: key1, key2, options: []).collect(2) + + let cancellable = publisher.sink { _ in + expect.fulfill() + } + + Defaults[key1] = ExamplePersistentHistory(value: "NewTestValue1") + Defaults[key2] = ExamplePersistentHistory(value: "NewTestValue2") + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveMultipleNSSecureOptionalKeysCombine() { + let key1 = Defaults.Key("observeMultipleNSSecureCodingOptionalKey1") + let key2 = Defaults.Key("observeMultipleNSSecureCodingOptionalKeyKey2") + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults.publisher(keys: key1, key2, options: []).collect(2) + + let cancellable = publisher.sink { _ in + expect.fulfill() + } + + Defaults[key1] = ExamplePersistentHistory(value: "NewTestValue1") + Defaults[key2] = ExamplePersistentHistory(value: "NewTestValue2") + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) + func testObserveMultipleNSSecureKeys() { + let key1 = Defaults.Key("observeNSSecureCodingKey1", default: ExamplePersistentHistory(value: "TestValue")) + let key2 = Defaults.Key("observeNSSecureCodingKey2", default: ExamplePersistentHistory(value: "TestValue")) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + var counter = 0 + observation = Defaults.observe(keys: key1, key2, options: []) { + counter += 1 + if counter == 2 { + expect.fulfill() + } else if counter > 2 { + XCTFail() + } + } + + Defaults[key1] = ExamplePersistentHistory(value: "NewTestValue1") + Defaults[key2] = ExamplePersistentHistory(value: "NewTestValue2") + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testRemoveDuplicatesObserveNSSecureCodingKeyCombine() { + let key = Defaults.Key("observeNSSecureCodingKey", default: ExamplePersistentHistory(value: "TestValue")) + let expect = expectation(description: "Observation closure being called") + + let inputArray = ["NewTestValue", "NewTestValue", "NewTestValue", "NewTestValue2", "NewTestValue2", "NewTestValue2", "NewTestValue3", "NewTestValue3"] + let expectedArray = ["NewTestValue", "NewTestValue2", "NewTestValue3"] + + let cancellable = Defaults + .publisher(key, options: []) + .removeDuplicates() + .map(\.newValue.value) + .collect(expectedArray.count) + .sink { result in + print("Result array: \(result)") + result == expectedArray ? expect.fulfill() : XCTFail("Expected Array is not matched") + } + + inputArray.forEach { + Defaults[key] = ExamplePersistentHistory(value: $0) + } + + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testRemoveDuplicatesObserveNSSecureCodingOptionalKeyCombine() { + let key = Defaults.Key("observeNSSecureCodingOptionalKey") + let expect = expectation(description: "Observation closure being called") + + let inputArray = ["NewTestValue", "NewTestValue", "NewTestValue", "NewTestValue2", "NewTestValue2", "NewTestValue2", "NewTestValue3", "NewTestValue3"] + let expectedArray = ["NewTestValue", "NewTestValue2", "NewTestValue3", nil] + + let cancellable = Defaults + .publisher(key, options: []) + .removeDuplicates() + .map(\.newValue) + .map { $0?.value } + .collect(expectedArray.count) + .sink { result in + print("Result array: \(result)") + result == expectedArray ? expect.fulfill() : XCTFail("Expected Array is not matched") + } + + inputArray.forEach { + Defaults[key] = ExamplePersistentHistory(value: $0) + } + + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + func testObserveKey() { + let key = Defaults.Key("observeNSSecureCodingKey", default: persistentHistoryValue) + let newPersistentHistory = ExamplePersistentHistory(value: "NewValue") + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue.value, persistentHistoryValue.value) + XCTAssertEqual(change.newValue.value, newPersistentHistory.value) + observation.invalidate() + expect.fulfill() + } + + Defaults[key] = newPersistentHistory + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveOptionalKey() { + let key = Defaults.Key("observeNSSecureCodingOptionalKey") + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertNil(change.oldValue) + XCTAssertEqual(change.newValue?.value, persistentHistoryValue.value) + observation.invalidate() + expect.fulfill() + } + + Defaults[key] = persistentHistoryValue + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveArrayKey() { + let key = Defaults.Key<[ExamplePersistentHistory]>("observeNSSecureCodingArrayKey", default: [persistentHistoryValue]) + let newPersistentHistory = ExamplePersistentHistory(value: "NewValue") + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue[0].value, persistentHistoryValue.value) + XCTAssertEqual(change.newValue.map { $0.value }, [persistentHistoryValue, newPersistentHistory].map { $0.value }) + observation.invalidate() + expect.fulfill() + } + + Defaults[key].append(newPersistentHistory) + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveDictionaryKey() { + let key = Defaults.Key<[String: ExamplePersistentHistory]>("observeNSSecureCodingDictionaryKey", default: ["0": persistentHistoryValue]) + let newPersistentHistory = ExamplePersistentHistory(value: "NewValue") + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue["0"]?.value, persistentHistoryValue.value) + XCTAssertEqual(change.newValue["0"]?.value, persistentHistoryValue.value) + XCTAssertEqual(change.newValue["1"]?.value, newPersistentHistory.value) + + observation.invalidate() + expect.fulfill() + } + + Defaults[key]["1"] = newPersistentHistory + observation.invalidate() + + waitForExpectations(timeout: 10) + } +} diff --git a/Tests/DefaultsTests/DefaultsSetAlgebraCustomElementTests.swift b/Tests/DefaultsTests/DefaultsSetAlgebraCustomElementTests.swift new file mode 100644 index 0000000..b221f2d --- /dev/null +++ b/Tests/DefaultsTests/DefaultsSetAlgebraCustomElementTests.swift @@ -0,0 +1,361 @@ +import Foundation +import XCTest +import Defaults + +private struct Item: Equatable, Hashable { + let name: String + let count: UInt +} + +extension Item: Defaults.Serializable { + static let bridge = ItemBridge() +} + +private struct ItemBridge: Defaults.Bridge { + typealias Value = Item + typealias Serializable = [String: String] + func serialize(_ value: Value?) -> Serializable? { + guard let value = value else { + return nil + } + + return ["name": value.name, "count": String(value.count)] + } + + func deserialize(_ object: Serializable?) -> Value? { + guard + let object = object, + let name = object["name"], + let count = UInt(object["count"] ?? "0") + else { + return nil + } + + return Value(name: name, count: count) + } +} + +private let fixtureSetAlgebra = Item(name: "Apple", count: 10) +private let fixtureSetAlgebra1 = Item(name: "Banana", count: 20) +private let fixtureSetAlgebra2 = Item(name: "Grape", count: 30) +private let fixtureSetAlgebra3 = Item(name: "Guava", count: 40) + +extension Defaults.Keys { + fileprivate static let setAlgebraCustomElement = Key>("setAlgebraCustomElement", default: .init([fixtureSetAlgebra])) + fileprivate static let setAlgebraCustomElementArray = Key<[DefaultsSetAlgebra]>("setAlgebraArrayCustomElement", default: [.init([fixtureSetAlgebra])]) + fileprivate static let setAlgebraCustomElementDictionary = Key<[String: DefaultsSetAlgebra]>("setAlgebraDictionaryCustomElement", default: ["0": .init([fixtureSetAlgebra])]) +} + +final class DefaultsSetAlgebraCustomElementTests: XCTestCase { + override func setUp() { + super.setUp() + Defaults.removeAll() + } + + override func tearDown() { + super.tearDown() + Defaults.removeAll() + } + + func testKey() { + let key = Defaults.Key>("independentSetAlgebraKey", default: .init([fixtureSetAlgebra])) + Defaults[key].insert(fixtureSetAlgebra) + XCTAssertEqual(Defaults[key], .init([fixtureSetAlgebra])) + Defaults[key].insert(fixtureSetAlgebra1) + XCTAssertEqual(Defaults[key], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + } + + func testOptionalKey() { + let key = Defaults.Key?>("independentSetAlgebraOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = .init([fixtureSetAlgebra]) + Defaults[key]?.insert(fixtureSetAlgebra) + XCTAssertEqual(Defaults[key], .init([fixtureSetAlgebra])) + Defaults[key]?.insert(fixtureSetAlgebra1) + XCTAssertEqual(Defaults[key], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + } + + func testArrayKey() { + let key = Defaults.Key<[DefaultsSetAlgebra]>("independentSetAlgebraArrayKey", default: [.init([fixtureSetAlgebra])]) + Defaults[key][0].insert(fixtureSetAlgebra1) + Defaults[key].append(.init([fixtureSetAlgebra2])) + Defaults[key][1].insert(fixtureSetAlgebra3) + XCTAssertEqual(Defaults[key][0], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + XCTAssertEqual(Defaults[key][1], .init([fixtureSetAlgebra2, fixtureSetAlgebra3])) + } + + func testArrayOptionalKey() { + let key = Defaults.Key<[DefaultsSetAlgebra]?>("independentSetAlgebraArrayOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = [.init([fixtureSetAlgebra])] + Defaults[key]?[0].insert(fixtureSetAlgebra1) + Defaults[key]?.append(.init([fixtureSetAlgebra2])) + Defaults[key]?[1].insert(fixtureSetAlgebra3) + XCTAssertEqual(Defaults[key]?[0], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + XCTAssertEqual(Defaults[key]?[1], .init([fixtureSetAlgebra2, fixtureSetAlgebra3])) + } + + func testNestedArrayKey() { + let key = Defaults.Key<[[DefaultsSetAlgebra]]>("independentSetAlgebraNestedArrayKey", default: [[.init([fixtureSetAlgebra])]]) + Defaults[key][0][0].insert(fixtureSetAlgebra1) + Defaults[key][0].append(.init([fixtureSetAlgebra1])) + Defaults[key][0][1].insert(fixtureSetAlgebra2) + Defaults[key].append([.init([fixtureSetAlgebra3])]) + Defaults[key][1][0].insert(fixtureSetAlgebra2) + XCTAssertEqual(Defaults[key][0][0], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + XCTAssertEqual(Defaults[key][0][1], .init([fixtureSetAlgebra1, fixtureSetAlgebra2])) + XCTAssertEqual(Defaults[key][1][0], .init([fixtureSetAlgebra2, fixtureSetAlgebra3])) + } + + func testArrayDictionaryKey() { + let key = Defaults.Key<[[String: DefaultsSetAlgebra]]>("independentSetAlgebraArrayDictionaryKey", default: [["0": .init([fixtureSetAlgebra])]]) + Defaults[key][0]["0"]?.insert(fixtureSetAlgebra1) + Defaults[key][0]["1"] = .init([fixtureSetAlgebra1]) + Defaults[key][0]["1"]?.insert(fixtureSetAlgebra2) + Defaults[key].append(["0": .init([fixtureSetAlgebra3])]) + Defaults[key][1]["0"]?.insert(fixtureSetAlgebra2) + XCTAssertEqual(Defaults[key][0]["0"], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + XCTAssertEqual(Defaults[key][0]["1"], .init([fixtureSetAlgebra1, fixtureSetAlgebra2])) + XCTAssertEqual(Defaults[key][1]["0"], .init([fixtureSetAlgebra2, fixtureSetAlgebra3])) + } + + func testDictionaryKey() { + let key = Defaults.Key<[String: DefaultsSetAlgebra]>("independentSetAlgebraDictionaryKey", default: ["0": .init([fixtureSetAlgebra])]) + Defaults[key]["0"]?.insert(fixtureSetAlgebra1) + Defaults[key]["1"] = .init([fixtureSetAlgebra2]) + Defaults[key]["1"]?.insert(fixtureSetAlgebra3) + XCTAssertEqual(Defaults[key]["0"], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + XCTAssertEqual(Defaults[key]["1"], .init([fixtureSetAlgebra2, fixtureSetAlgebra3])) + } + + func testDictionaryOptionalKey() { + let key = Defaults.Key<[String: DefaultsSetAlgebra]?>("independentSetAlgebraDictionaryOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = ["0": .init([fixtureSetAlgebra])] + Defaults[key]?["0"]?.insert(fixtureSetAlgebra1) + Defaults[key]?["1"] = .init([fixtureSetAlgebra2]) + Defaults[key]?["1"]?.insert(fixtureSetAlgebra3) + XCTAssertEqual(Defaults[key]?["0"], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + XCTAssertEqual(Defaults[key]?["1"], .init([fixtureSetAlgebra2, fixtureSetAlgebra3])) + } + + func testDictionaryArrayKey() { + let key = Defaults.Key<[String: [DefaultsSetAlgebra]]>("independentSetAlgebraDictionaryArrayKey", default: ["0": [.init([fixtureSetAlgebra])]]) + Defaults[key]["0"]?[0].insert(fixtureSetAlgebra1) + Defaults[key]["0"]?.append(.init([fixtureSetAlgebra1])) + Defaults[key]["0"]?[1].insert(fixtureSetAlgebra2) + Defaults[key]["1"] = [.init([fixtureSetAlgebra3])] + Defaults[key]["1"]?[0].insert(fixtureSetAlgebra2) + XCTAssertEqual(Defaults[key]["0"]?[0], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + XCTAssertEqual(Defaults[key]["0"]?[1], .init([fixtureSetAlgebra1, fixtureSetAlgebra2])) + XCTAssertEqual(Defaults[key]["1"]?[0], .init([fixtureSetAlgebra2, fixtureSetAlgebra3])) + } + + func testType() { + let (inserted, _) = Defaults[.setAlgebraCustomElement].insert(fixtureSetAlgebra) + XCTAssertFalse(inserted) + Defaults[.setAlgebraCustomElement].insert(fixtureSetAlgebra1) + XCTAssertEqual(Defaults[.setAlgebraCustomElement], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + } + + func testArrayType() { + Defaults[.setAlgebraCustomElementArray][0].insert(fixtureSetAlgebra1) + Defaults[.setAlgebraCustomElementArray].append(.init([fixtureSetAlgebra2])) + Defaults[.setAlgebraCustomElementArray][1].insert(fixtureSetAlgebra3) + XCTAssertEqual(Defaults[.setAlgebraCustomElementArray][0], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + XCTAssertEqual(Defaults[.setAlgebraCustomElementArray][1], .init([fixtureSetAlgebra2, fixtureSetAlgebra3])) + } + + func testDictionaryType() { + Defaults[.setAlgebraCustomElementDictionary]["0"]?.insert(fixtureSetAlgebra1) + Defaults[.setAlgebraCustomElementDictionary]["1"] = .init([fixtureSetAlgebra2]) + Defaults[.setAlgebraCustomElementDictionary]["1"]?.insert(fixtureSetAlgebra3) + XCTAssertEqual(Defaults[.setAlgebraCustomElementDictionary]["0"], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + XCTAssertEqual(Defaults[.setAlgebraCustomElementDictionary]["1"], .init([fixtureSetAlgebra2, fixtureSetAlgebra3])) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveKeyCombine() { + let key = Defaults.Key>("observeSetAlgebraKeyCombine", default: .init([fixtureSetAlgebra])) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let expectedValue: [(DefaultsSetAlgebra, DefaultsSetAlgebra)] = [(.init([fixtureSetAlgebra]), .init([fixtureSetAlgebra, fixtureSetAlgebra1])), (.init([fixtureSetAlgebra, fixtureSetAlgebra1]), .init([fixtureSetAlgebra]))] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0, tuples[index].0) + XCTAssertEqual(expected.1, tuples[index].1) + } + + expect.fulfill() + } + + Defaults[key].insert(fixtureSetAlgebra1) + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveOptionalKeyCombine() { + let key = Defaults.Key?>("observeSetAlgebraOptionalKeyCombine") + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(3) + + let expectedValue: [(DefaultsSetAlgebra?, DefaultsSetAlgebra?)] = [(nil, .init([fixtureSetAlgebra])), (.init([fixtureSetAlgebra]), .init([fixtureSetAlgebra, fixtureSetAlgebra1])), (.init([fixtureSetAlgebra, fixtureSetAlgebra1]), nil)] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0, tuples[index].0) + XCTAssertEqual(expected.1, tuples[index].1) + } + + expect.fulfill() + } + + Defaults[key] = .init([fixtureSetAlgebra]) + Defaults[key]?.insert(fixtureSetAlgebra1) + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveArrayKeyCombine() { + let key = Defaults.Key<[DefaultsSetAlgebra]>("observeSetAlgebraArrayKeyCombine", default: [.init([fixtureSetAlgebra])]) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let expectedValue: [(DefaultsSetAlgebra, DefaultsSetAlgebra)] = [(.init([fixtureSetAlgebra]), .init([fixtureSetAlgebra, fixtureSetAlgebra1])), (.init([fixtureSetAlgebra, fixtureSetAlgebra1]), .init([fixtureSetAlgebra]))] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0, tuples[index].0[0]) + XCTAssertEqual(expected.1, tuples[index].1[0]) + } + + expect.fulfill() + } + + Defaults[key][0].insert(fixtureSetAlgebra1) + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveDictionaryKeyCombine() { + let key = Defaults.Key<[String: DefaultsSetAlgebra]>("observeSetAlgebraDictionaryKeyCombine", default: ["0": .init([fixtureSetAlgebra])]) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let expectedValue: [(DefaultsSetAlgebra, DefaultsSetAlgebra)] = [(.init([fixtureSetAlgebra]), .init([fixtureSetAlgebra, fixtureSetAlgebra1])), (.init([fixtureSetAlgebra, fixtureSetAlgebra1]), .init([fixtureSetAlgebra]))] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0, tuples[index].0["0"]) + XCTAssertEqual(expected.1, tuples[index].1["0"]) + } + + expect.fulfill() + } + + Defaults[key]["0"]?.insert(fixtureSetAlgebra1) + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + func testObserveKey() { + let key = Defaults.Key>("observeSetAlgebraKey", default: .init([fixtureSetAlgebra])) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue, .init([fixtureSetAlgebra])) + XCTAssertEqual(change.newValue, .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + observation.invalidate() + expect.fulfill() + } + + Defaults[key].insert(fixtureSetAlgebra1) + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveOptionalKey() { + let key = Defaults.Key?>("observeSetAlgebraOptionalKey") + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertNil(change.oldValue) + XCTAssertEqual(change.newValue, .init([fixtureSetAlgebra])) + observation.invalidate() + expect.fulfill() + } + + Defaults[key] = .init([fixtureSetAlgebra]) + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveArrayKey() { + let key = Defaults.Key<[DefaultsSetAlgebra]>("observeSetAlgebraArrayKey", default: [.init([fixtureSetAlgebra])]) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue[0], .init([fixtureSetAlgebra])) + XCTAssertEqual(change.newValue[1], .init([fixtureSetAlgebra])) + observation.invalidate() + expect.fulfill() + } + + Defaults[key].append(.init([fixtureSetAlgebra])) + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveDictioanryKey() { + let key = Defaults.Key<[String: DefaultsSetAlgebra]>("observeSetAlgebraDictionaryKey", default: ["0": .init([fixtureSetAlgebra])]) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue["0"], .init([fixtureSetAlgebra])) + XCTAssertEqual(change.newValue["1"], .init([fixtureSetAlgebra])) + observation.invalidate() + expect.fulfill() + } + + Defaults[key]["1"] = .init([fixtureSetAlgebra]) + observation.invalidate() + + waitForExpectations(timeout: 10) + } +} diff --git a/Tests/DefaultsTests/DefaultsSetAlgebraTests.swift b/Tests/DefaultsTests/DefaultsSetAlgebraTests.swift new file mode 100644 index 0000000..5b72ca8 --- /dev/null +++ b/Tests/DefaultsTests/DefaultsSetAlgebraTests.swift @@ -0,0 +1,396 @@ +import Foundation +import XCTest +import Defaults + +struct DefaultsSetAlgebra: SetAlgebra { + var store = Set() + + init() {} + + init(_ sequence: __owned S) where Element == S.Element { + self.store = Set(sequence) + } + + init(_ store: Set) { + self.store = store + } + + func contains(_ member: Element) -> Bool { + store.contains(member) + } + + func union(_ other: DefaultsSetAlgebra) -> DefaultsSetAlgebra { + DefaultsSetAlgebra(store.union(other.store)) + } + + func intersection(_ other: DefaultsSetAlgebra) + -> DefaultsSetAlgebra { + var defaultsSetAlgebra = DefaultsSetAlgebra() + defaultsSetAlgebra.store = store.intersection(other.store) + return defaultsSetAlgebra + } + + func symmetricDifference(_ other: DefaultsSetAlgebra) + -> DefaultsSetAlgebra { + var defaultedSetAlgebra = DefaultsSetAlgebra() + defaultedSetAlgebra.store = store.symmetricDifference(other.store) + return defaultedSetAlgebra + } + + @discardableResult + mutating func insert(_ newMember: Element) + -> (inserted: Bool, memberAfterInsert: Element) { + store.insert(newMember) + } + + mutating func remove(_ member: Element) -> Element? { + store.remove(member) + } + + mutating func update(with newMember: Element) -> Element? { + store.update(with: newMember) + } + + mutating func formUnion(_ other: DefaultsSetAlgebra) { + store.formUnion(other.store) + } + + mutating func formSymmetricDifference(_ other: DefaultsSetAlgebra) { + store.formSymmetricDifference(other.store) + } + + mutating func formIntersection(_ other: DefaultsSetAlgebra) { + store.formIntersection(other.store) + } +} + +extension DefaultsSetAlgebra: Defaults.SetAlgebraSerializable { + func toArray() -> [Element] { + Array(store) + } +} + +private let fixtureSetAlgebra = 0 +private let fixtureSetAlgebra1 = 1 +private let fixtureSetAlgebra2 = 2 +private let fixtureSetAlgebra3 = 3 + +extension Defaults.Keys { + fileprivate static let setAlgebra = Key>("setAlgebra", default: .init([fixtureSetAlgebra])) + fileprivate static let setAlgebraArray = Key<[DefaultsSetAlgebra]>("setAlgebraArray", default: [.init([fixtureSetAlgebra])]) + fileprivate static let setAlgebraDictionary = Key<[String: DefaultsSetAlgebra]>("setAlgebraDictionary", default: ["0": .init([fixtureSetAlgebra])]) +} + +final class DefaultsSetAlgebraTests: XCTestCase { + override func setUp() { + super.setUp() + Defaults.removeAll() + } + + override func tearDown() { + super.tearDown() + Defaults.removeAll() + } + + func testKey() { + let key = Defaults.Key>("independentSetAlgebraKey", default: .init([fixtureSetAlgebra])) + Defaults[key].insert(fixtureSetAlgebra) + XCTAssertEqual(Defaults[key], .init([fixtureSetAlgebra])) + Defaults[key].insert(fixtureSetAlgebra1) + XCTAssertEqual(Defaults[key], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + } + + func testOptionalKey() { + let key = Defaults.Key?>("independentSetAlgebraOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = .init([fixtureSetAlgebra]) + Defaults[key]?.insert(fixtureSetAlgebra) + XCTAssertEqual(Defaults[key], .init([fixtureSetAlgebra])) + Defaults[key]?.insert(fixtureSetAlgebra1) + XCTAssertEqual(Defaults[key], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + } + + func testArrayKey() { + let key = Defaults.Key<[DefaultsSetAlgebra]>("independentSetAlgebraArrayKey", default: [.init([fixtureSetAlgebra])]) + Defaults[key][0].insert(fixtureSetAlgebra1) + Defaults[key].append(.init([fixtureSetAlgebra2])) + Defaults[key][1].insert(fixtureSetAlgebra3) + XCTAssertEqual(Defaults[key][0], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + XCTAssertEqual(Defaults[key][1], .init([fixtureSetAlgebra2, fixtureSetAlgebra3])) + } + + func testArrayOptionalKey() { + let key = Defaults.Key<[DefaultsSetAlgebra]?>("independentSetAlgebraArrayOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = [.init([fixtureSetAlgebra])] + Defaults[key]?[0].insert(fixtureSetAlgebra1) + Defaults[key]?.append(.init([fixtureSetAlgebra2])) + Defaults[key]?[1].insert(fixtureSetAlgebra3) + XCTAssertEqual(Defaults[key]?[0], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + XCTAssertEqual(Defaults[key]?[1], .init([fixtureSetAlgebra2, fixtureSetAlgebra3])) + } + + func testNestedArrayKey() { + let key = Defaults.Key<[[DefaultsSetAlgebra]]>("independentSetAlgebraNestedArrayKey", default: [[.init([fixtureSetAlgebra])]]) + Defaults[key][0][0].insert(fixtureSetAlgebra1) + Defaults[key][0].append(.init([fixtureSetAlgebra1])) + Defaults[key][0][1].insert(fixtureSetAlgebra2) + Defaults[key].append([.init([fixtureSetAlgebra3])]) + Defaults[key][1][0].insert(fixtureSetAlgebra2) + XCTAssertEqual(Defaults[key][0][0], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + XCTAssertEqual(Defaults[key][0][1], .init([fixtureSetAlgebra1, fixtureSetAlgebra2])) + XCTAssertEqual(Defaults[key][1][0], .init([fixtureSetAlgebra2, fixtureSetAlgebra3])) + } + + func testArrayDictionaryKey() { + let key = Defaults.Key<[[String: DefaultsSetAlgebra]]>("independentSetAlgebraArrayDictionaryKey", default: [["0": .init([fixtureSetAlgebra])]]) + Defaults[key][0]["0"]?.insert(fixtureSetAlgebra1) + Defaults[key][0]["1"] = .init([fixtureSetAlgebra1]) + Defaults[key][0]["1"]?.insert(fixtureSetAlgebra2) + Defaults[key].append(["0": .init([fixtureSetAlgebra3])]) + Defaults[key][1]["0"]?.insert(fixtureSetAlgebra2) + XCTAssertEqual(Defaults[key][0]["0"], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + XCTAssertEqual(Defaults[key][0]["1"], .init([fixtureSetAlgebra1, fixtureSetAlgebra2])) + XCTAssertEqual(Defaults[key][1]["0"], .init([fixtureSetAlgebra2, fixtureSetAlgebra3])) + } + + func testDictionaryKey() { + let key = Defaults.Key<[String: DefaultsSetAlgebra]>("independentSetAlgebraDictionaryKey", default: ["0": .init([fixtureSetAlgebra])]) + Defaults[key]["0"]?.insert(fixtureSetAlgebra1) + Defaults[key]["1"] = .init([fixtureSetAlgebra2]) + Defaults[key]["1"]?.insert(fixtureSetAlgebra3) + XCTAssertEqual(Defaults[key]["0"], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + XCTAssertEqual(Defaults[key]["1"], .init([fixtureSetAlgebra2, fixtureSetAlgebra3])) + } + + func testDictionaryOptionalKey() { + let key = Defaults.Key<[String: DefaultsSetAlgebra]?>("independentSetAlgebraDictionaryOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = ["0": .init([fixtureSetAlgebra])] + Defaults[key]?["0"]?.insert(fixtureSetAlgebra1) + Defaults[key]?["1"] = .init([fixtureSetAlgebra2]) + Defaults[key]?["1"]?.insert(fixtureSetAlgebra3) + XCTAssertEqual(Defaults[key]?["0"], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + XCTAssertEqual(Defaults[key]?["1"], .init([fixtureSetAlgebra2, fixtureSetAlgebra3])) + } + + func testDictionaryArrayKey() { + let key = Defaults.Key<[String: [DefaultsSetAlgebra]]>("independentSetAlgebraDictionaryArrayKey", default: ["0": [.init([fixtureSetAlgebra])]]) + Defaults[key]["0"]?[0].insert(fixtureSetAlgebra1) + Defaults[key]["0"]?.append(.init([fixtureSetAlgebra1])) + Defaults[key]["0"]?[1].insert(fixtureSetAlgebra2) + Defaults[key]["1"] = [.init([fixtureSetAlgebra3])] + Defaults[key]["1"]?[0].insert(fixtureSetAlgebra2) + XCTAssertEqual(Defaults[key]["0"]?[0], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + XCTAssertEqual(Defaults[key]["0"]?[1], .init([fixtureSetAlgebra1, fixtureSetAlgebra2])) + XCTAssertEqual(Defaults[key]["1"]?[0], .init([fixtureSetAlgebra2, fixtureSetAlgebra3])) + } + + func testType() { + let (inserted, _) = Defaults[.setAlgebra].insert(fixtureSetAlgebra) + XCTAssertFalse(inserted) + Defaults[.setAlgebra].insert(fixtureSetAlgebra1) + XCTAssertEqual(Defaults[.setAlgebra], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + } + + func testArrayType() { + Defaults[.setAlgebraArray][0].insert(fixtureSetAlgebra1) + Defaults[.setAlgebraArray].append(.init([fixtureSetAlgebra2])) + Defaults[.setAlgebraArray][1].insert(fixtureSetAlgebra3) + XCTAssertEqual(Defaults[.setAlgebraArray][0], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + XCTAssertEqual(Defaults[.setAlgebraArray][1], .init([fixtureSetAlgebra2, fixtureSetAlgebra3])) + } + + func testDictionaryType() { + Defaults[.setAlgebraDictionary]["0"]?.insert(fixtureSetAlgebra1) + Defaults[.setAlgebraDictionary]["1"] = .init([fixtureSetAlgebra2]) + Defaults[.setAlgebraDictionary]["1"]?.insert(fixtureSetAlgebra3) + XCTAssertEqual(Defaults[.setAlgebraDictionary]["0"], .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + XCTAssertEqual(Defaults[.setAlgebraDictionary]["1"], .init([fixtureSetAlgebra2, fixtureSetAlgebra3])) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveKeyCombine() { + let key = Defaults.Key>("observeSetAlgebraKeyCombine", default: .init([fixtureSetAlgebra])) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let expectedValue: [(DefaultsSetAlgebra, DefaultsSetAlgebra)] = [(.init([fixtureSetAlgebra]), .init([fixtureSetAlgebra, fixtureSetAlgebra1])), (.init([fixtureSetAlgebra, fixtureSetAlgebra1]), .init([fixtureSetAlgebra]))] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0, tuples[index].0) + XCTAssertEqual(expected.1, tuples[index].1) + } + + expect.fulfill() + } + + Defaults[key].insert(fixtureSetAlgebra1) + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveOptionalKeyCombine() { + let key = Defaults.Key?>("observeSetAlgebraOptionalKeyCombine") + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(3) + + let expectedValue: [(DefaultsSetAlgebra?, DefaultsSetAlgebra?)] = [(nil, .init([fixtureSetAlgebra])), (.init([fixtureSetAlgebra]), .init([fixtureSetAlgebra, fixtureSetAlgebra1])), (.init([fixtureSetAlgebra, fixtureSetAlgebra1]), nil)] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0, tuples[index].0) + XCTAssertEqual(expected.1, tuples[index].1) + } + + expect.fulfill() + } + + Defaults[key] = .init([fixtureSetAlgebra]) + Defaults[key]?.insert(fixtureSetAlgebra1) + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveArrayKeyCombine() { + let key = Defaults.Key<[DefaultsSetAlgebra]>("observeSetAlgebraArrayKeyCombine", default: [.init([fixtureSetAlgebra])]) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let expectedValue: [(DefaultsSetAlgebra, DefaultsSetAlgebra)] = [(.init([fixtureSetAlgebra]), .init([fixtureSetAlgebra, fixtureSetAlgebra1])), (.init([fixtureSetAlgebra, fixtureSetAlgebra1]), .init([fixtureSetAlgebra]))] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0, tuples[index].0[0]) + XCTAssertEqual(expected.1, tuples[index].1[0]) + } + + expect.fulfill() + } + + Defaults[key][0].insert(fixtureSetAlgebra1) + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveDictionaryKeyCombine() { + let key = Defaults.Key<[String: DefaultsSetAlgebra]>("observeSetAlgebraDictionaryKeyCombine", default: ["0": .init([fixtureSetAlgebra])]) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let expectedValue: [(DefaultsSetAlgebra, DefaultsSetAlgebra)] = [(.init([fixtureSetAlgebra]), .init([fixtureSetAlgebra, fixtureSetAlgebra1])), (.init([fixtureSetAlgebra, fixtureSetAlgebra1]), .init([fixtureSetAlgebra]))] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + XCTAssertEqual(expected.0, tuples[index].0["0"]) + XCTAssertEqual(expected.1, tuples[index].1["0"]) + } + + expect.fulfill() + } + + Defaults[key]["0"]?.insert(fixtureSetAlgebra1) + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + func testObserveKey() { + let key = Defaults.Key>("observeSetAlgebraKey", default: .init([fixtureSetAlgebra])) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue, .init([fixtureSetAlgebra])) + XCTAssertEqual(change.newValue, .init([fixtureSetAlgebra, fixtureSetAlgebra1])) + observation.invalidate() + expect.fulfill() + } + + Defaults[key].insert(fixtureSetAlgebra1) + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveOptionalKey() { + let key = Defaults.Key?>("observeSetAlgebraOptionalKey") + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertNil(change.oldValue) + XCTAssertEqual(change.newValue, .init([fixtureSetAlgebra])) + observation.invalidate() + expect.fulfill() + } + + Defaults[key] = .init([fixtureSetAlgebra]) + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveArrayKey() { + let key = Defaults.Key<[DefaultsSetAlgebra]>("observeSetAlgebraArrayKey", default: [.init([fixtureSetAlgebra])]) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue[0], .init([fixtureSetAlgebra])) + XCTAssertEqual(change.newValue[1], .init([fixtureSetAlgebra])) + observation.invalidate() + expect.fulfill() + } + + Defaults[key].append(.init([fixtureSetAlgebra])) + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveDictioanryKey() { + let key = Defaults.Key<[String: DefaultsSetAlgebra]>("observeSetAlgebraDictionaryKey", default: ["0": .init([fixtureSetAlgebra])]) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertEqual(change.oldValue["0"], .init([fixtureSetAlgebra])) + XCTAssertEqual(change.newValue["1"], .init([fixtureSetAlgebra])) + observation.invalidate() + expect.fulfill() + } + + Defaults[key]["1"] = .init([fixtureSetAlgebra]) + observation.invalidate() + + waitForExpectations(timeout: 10) + } +} diff --git a/Tests/DefaultsTests/DefaultsSetTests.swift b/Tests/DefaultsTests/DefaultsSetTests.swift new file mode 100644 index 0000000..c171ef4 --- /dev/null +++ b/Tests/DefaultsTests/DefaultsSetTests.swift @@ -0,0 +1,55 @@ +import Foundation +import Defaults +import XCTest + +private let fixtureSet = Set(1...5) + +extension Defaults.Keys { + fileprivate static let set = Key>("setInt", default: fixtureSet) +} + +final class DefaultsSetTests: XCTestCase { + override func setUp() { + super.setUp() + Defaults.removeAll() + } + + override func tearDown() { + super.tearDown() + Defaults.removeAll() + } + + func testKey() { + let key = Defaults.Key>("independentSetKey", default: fixtureSet) + XCTAssertEqual(Defaults[key].count, fixtureSet.count) + Defaults[key].insert(6) + XCTAssertEqual(Defaults[key], Set(1...6)) + } + + func testOptionalKey() { + let key = Defaults.Key?>("independentSetOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = fixtureSet + XCTAssertEqual(Defaults[key]?.count, fixtureSet.count) + Defaults[key]?.insert(6) + XCTAssertEqual(Defaults[key], Set(1...6)) + } + + func testArrayKey() { + let key = Defaults.Key<[Set]>("independentSetArrayKey", default: [fixtureSet]) + XCTAssertEqual(Defaults[key][0].count, fixtureSet.count) + Defaults[key][0].insert(6) + XCTAssertEqual(Defaults[key][0], Set(1...6)) + Defaults[key].append(Set(1...4)) + XCTAssertEqual(Defaults[key][1], Set(1...4)) + } + + func testDictionaryKey() { + let key = Defaults.Key<[String: Set]>("independentSetArrayKey", default: ["0": fixtureSet]) + XCTAssertEqual(Defaults[key]["0"]?.count, fixtureSet.count) + Defaults[key]["0"]?.insert(6) + XCTAssertEqual(Defaults[key]["0"], Set(1...6)) + Defaults[key]["1"] = Set(1...4) + XCTAssertEqual(Defaults[key]["1"], Set(1...4)) + } +} diff --git a/Tests/DefaultsTests/DefaultsSwiftUITests.swift b/Tests/DefaultsTests/DefaultsSwiftUITests.swift new file mode 100644 index 0000000..4e1ace5 --- /dev/null +++ b/Tests/DefaultsTests/DefaultsSwiftUITests.swift @@ -0,0 +1,48 @@ +import XCTest +import Foundation +import SwiftUI +import Defaults + +extension Defaults.Keys { + fileprivate static let hasUnicorn = Key("swiftui_hasUnicorn", default: false) + fileprivate static let user = Key("swiftui_user", default: User(username: "Hank", password: "123456")) + fileprivate static let setInt = Key>("swiftui_setInt", default: Set(1...3)) +} + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +struct ContentView: View { + @Default(.hasUnicorn) var hasUnicorn + @Default(.user) var user + @Default(.setInt) var setInt + + var body: some View { + Text("User \(user.username) has Unicorn: \(String(hasUnicorn))") + Toggle("Toggle Unicorn", isOn: $hasUnicorn) + } +} + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +final class DefaultsSwiftUITests: XCTestCase { + override func setUp() { + super.setUp() + Defaults.removeAll() + } + + override func tearDown() { + super.tearDown() + Defaults.removeAll() + } + + func testSwiftUIObserve() { + let view = ContentView() + XCTAssertFalse(view.hasUnicorn) + XCTAssertEqual(view.user.username, "Hank") + XCTAssertEqual(view.setInt.count, 3) + view.user = User(username: "Chen", password: "123456") + view.hasUnicorn.toggle() + view.setInt.insert(4) + XCTAssertTrue(view.hasUnicorn) + XCTAssertEqual(view.user.username, "Chen") + XCTAssertEqual(view.setInt, Set(1...4)) + } +} diff --git a/Tests/DefaultsTests/DefaultsTests.swift b/Tests/DefaultsTests/DefaultsTests.swift index 451bd32..dce3a1a 100644 --- a/Tests/DefaultsTests/DefaultsTests.swift +++ b/Tests/DefaultsTests/DefaultsTests.swift @@ -1,53 +1,20 @@ import Foundation -import CoreData import Combine import XCTest import Defaults let fixtureURL = URL(string: "https://sindresorhus.com")! +let fixtureFileURL = URL(string: "file://~/icon.png")! let fixtureURL2 = URL(string: "https://example.com")! -enum FixtureEnum: String, Codable { - case tenMinutes = "10 Minutes" - case halfHour = "30 Minutes" - case oneHour = "1 Hour" -} - let fixtureDate = Date() -@available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) -final class ExamplePersistentHistory: NSPersistentHistoryToken { - let value: String - - init(value: String) { - self.value = value - super.init() - } - - required init?(coder: NSCoder) { - self.value = coder.decodeObject(forKey: "value") as! String - super.init() - } - - override func encode(with coder: NSCoder) { - coder.encode(value, forKey: "value") - } - - override class var supportsSecureCoding: Bool { true } -} - extension Defaults.Keys { static let key = Key("key", default: false) static let url = Key("url", default: fixtureURL) - static let `enum` = Key("enum", default: .oneHour) + static let file = Key("fileURL", default: fixtureFileURL) static let data = Key("data", default: Data([])) static let date = Key("date", default: fixtureDate) - - // NSSecureCoding - @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) - static let persistentHistoryValue = ExamplePersistentHistory(value: "ExampleToken") - @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) - static let persistentHistory = NSSecureCodingKey("persistentHistory", default: persistentHistoryValue) } final class DefaultsTests: XCTestCase { @@ -70,13 +37,21 @@ final class DefaultsTests: XCTestCase { func testOptionalKey() { let key = Defaults.Key("independentOptionalKey") + let url = Defaults.Key("independentOptionalURLKey") XCTAssertNil(Defaults[key]) + XCTAssertNil(Defaults[url]) Defaults[key] = true + Defaults[url] = fixtureURL XCTAssertTrue(Defaults[key]!) + XCTAssertEqual(Defaults[url], fixtureURL) Defaults[key] = nil + Defaults[url] = nil XCTAssertNil(Defaults[key]) + XCTAssertNil(Defaults[url]) Defaults[key] = false + Defaults[url] = fixtureURL2 XCTAssertFalse(Defaults[key]!) + XCTAssertEqual(Defaults[url], fixtureURL2) } func testKeyRegistersDefault() { @@ -104,29 +79,15 @@ final class DefaultsTests: XCTestCase { XCTAssertTrue(Defaults[.key]) } - @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) - func testNSSecureCodingKeys() { - XCTAssertEqual(Defaults.Keys.persistentHistoryValue.value, Defaults[.persistentHistory].value) - let newPersistentHistory = ExamplePersistentHistory(value: "NewValue") - Defaults[.persistentHistory] = newPersistentHistory - XCTAssertEqual(newPersistentHistory.value, Defaults[.persistentHistory].value) - } - func testUrlType() { XCTAssertEqual(Defaults[.url], fixtureURL) - let newUrl = URL(string: "https://twitter.com")! Defaults[.url] = newUrl XCTAssertEqual(Defaults[.url], newUrl) } - func testEnumType() { - XCTAssertEqual(Defaults[.enum], FixtureEnum.oneHour) - } - func testDataType() { XCTAssertEqual(Defaults[.data], Data([])) - let newData = Data([0xFF]) Defaults[.data] = newData XCTAssertEqual(Defaults[.data], newData) @@ -134,12 +95,15 @@ final class DefaultsTests: XCTestCase { func testDateType() { XCTAssertEqual(Defaults[.date], fixtureDate) - let newDate = Date() Defaults[.date] = newDate XCTAssertEqual(Defaults[.date], newDate) } + func testFileURLType() { + XCTAssertEqual(Defaults[.file], fixtureFileURL) + } + func testRemoveAll() { let key = Defaults.Key("removeAll", default: false) let key2 = Defaults.Key("removeAll2", default: false) @@ -189,39 +153,6 @@ final class DefaultsTests: XCTestCase { waitForExpectations(timeout: 10) } - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) - func testObserveNSSecureCodingKeyCombine() { - let key = Defaults.NSSecureCodingKey("observeNSSecureCodingKey", default: ExamplePersistentHistory(value: "TestValue")) - let expect = expectation(description: "Observation closure being called") - - let publisher = Defaults - .publisher(key, options: []) - .map { ($0.oldValue.value, $0.newValue.value) } - .collect(3) - - let expectedValues = [ - ("TestValue", "NewTestValue"), - ("NewTestValue", "NewTestValue2"), - ("NewTestValue2", "TestValue") - ] - - let cancellable = publisher.sink { actualValues in - for (expected, actual) in zip(expectedValues, actualValues) { - XCTAssertEqual(expected.0, actual.0) - XCTAssertEqual(expected.1, actual.1) - } - - expect.fulfill() - } - - Defaults[key] = ExamplePersistentHistory(value: "NewTestValue") - Defaults[key] = ExamplePersistentHistory(value: "NewTestValue2") - Defaults.reset(key) - cancellable.cancel() - - waitForExpectations(timeout: 10) - } - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) func testObserveOptionalKeyCombine() { let key = Defaults.Key("observeOptionalKey") @@ -251,40 +182,6 @@ final class DefaultsTests: XCTestCase { waitForExpectations(timeout: 10) } - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) - func testObserveNSSecureCodingOptionalKeyCombine() { - let key = Defaults.NSSecureCodingOptionalKey("observeNSSecureCodingOptionalKey") - let expect = expectation(description: "Observation closure being called") - - let publisher = Defaults - .publisher(key, options: []) - .map { ($0.oldValue?.value, $0.newValue?.value) } - .collect(3) - - let expectedValues: [(String?, String?)] = [ - (nil, "NewTestValue"), - ("NewTestValue", "NewTestValue2"), - ("NewTestValue2", nil) - ] - - let cancellable = publisher.sink { actualValues in - for (expected, actual) in zip(expectedValues, actualValues) { - XCTAssertEqual(expected.0, actual.0) - XCTAssertEqual(expected.1, actual.1) - } - - expect.fulfill() - } - - XCTAssertNil(Defaults[key]) - Defaults[key] = ExamplePersistentHistory(value: "NewTestValue") - Defaults[key] = ExamplePersistentHistory(value: "NewTestValue2") - Defaults.reset(key) - cancellable.cancel() - - waitForExpectations(timeout: 10) - } - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) func testObserveMultipleKeysCombine() { let key1 = Defaults.Key("observeKey1", default: "x") @@ -304,26 +201,6 @@ final class DefaultsTests: XCTestCase { waitForExpectations(timeout: 10) } - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) - func testObserveMultipleNSSecureKeysCombine() { - let key1 = Defaults.NSSecureCodingKey("observeNSSecureCodingKey1", default: ExamplePersistentHistory(value: "TestValue")) - let key2 = Defaults.NSSecureCodingKey("observeNSSecureCodingKey2", default: ExamplePersistentHistory(value: "TestValue")) - let expect = expectation(description: "Observation closure being called") - - let publisher = Defaults.publisher(keys: key1, key2, options: []).collect(2) - - let cancellable = publisher.sink { _ in - expect.fulfill() - } - - Defaults[key1] = ExamplePersistentHistory(value: "NewTestValue1") - Defaults[key2] = ExamplePersistentHistory(value: "NewTestValue2") - cancellable.cancel() - - waitForExpectations(timeout: 10) - } - - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) func testObserveMultipleOptionalKeysCombine() { let key1 = Defaults.Key("observeOptionalKey1") @@ -343,25 +220,6 @@ final class DefaultsTests: XCTestCase { waitForExpectations(timeout: 10) } - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) - func testObserveMultipleNSSecureOptionalKeysCombine() { - let key1 = Defaults.NSSecureCodingOptionalKey("observeNSSecureCodingKey1") - let key2 = Defaults.NSSecureCodingOptionalKey("observeNSSecureCodingKey2") - let expect = expectation(description: "Observation closure being called") - - let publisher = Defaults.publisher(keys: key1, key2, options: []).collect(2) - - let cancellable = publisher.sink { _ in - expect.fulfill() - } - - Defaults[key1] = ExamplePersistentHistory(value: "NewTestValue1") - Defaults[key2] = ExamplePersistentHistory(value: "NewTestValue2") - cancellable.cancel() - - waitForExpectations(timeout: 10) - } - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) func testReceiveValueBeforeSubscriptionCombine() { let key = Defaults.Key("receiveValueBeforeSubscription", default: "hello") @@ -400,24 +258,6 @@ final class DefaultsTests: XCTestCase { waitForExpectations(timeout: 10) } - @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) - func testObserveNSSecureCodingKey() { - let key = Defaults.NSSecureCodingKey("observeNSSecureCodingKey", default: ExamplePersistentHistory(value: "TestValue")) - let expect = expectation(description: "Observation closure being called") - - var observation: Defaults.Observation! - observation = Defaults.observe(key, options: []) { change in - XCTAssertEqual(change.oldValue.value, "TestValue") - XCTAssertEqual(change.newValue.value, "NewTestValue") - observation.invalidate() - expect.fulfill() - } - - Defaults[key] = ExamplePersistentHistory(value: "NewTestValue") - - waitForExpectations(timeout: 10) - } - func testObserveOptionalKey() { let key = Defaults.Key("observeOptionalKey") let expect = expectation(description: "Observation closure being called") @@ -435,24 +275,6 @@ final class DefaultsTests: XCTestCase { waitForExpectations(timeout: 10) } - @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) - func testObserveNSSecureCodingOptionalKey() { - let key = Defaults.NSSecureCodingOptionalKey("observeNSSecureCodingOptionalKey") - let expect = expectation(description: "Observation closure being called") - - var observation: Defaults.Observation! - observation = Defaults.observe(key, options: []) { change in - XCTAssertNil(change.oldValue) - XCTAssertEqual(change.newValue?.value, "NewOptionalValue") - observation.invalidate() - expect.fulfill() - } - - Defaults[key] = ExamplePersistentHistory(value: "NewOptionalValue") - - waitForExpectations(timeout: 10) - } - func testObserveMultipleKeys() { let key1 = Defaults.Key("observeKey1", default: "x") let key2 = Defaults.Key("observeKey2", default: true) @@ -476,33 +298,7 @@ final class DefaultsTests: XCTestCase { waitForExpectations(timeout: 10) } - @available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) - func testObserveMultipleNSSecureKeys() { - let key1 = Defaults.NSSecureCodingKey("observeNSSecureCodingKey1", default: ExamplePersistentHistory(value: "TestValue")) - let key2 = Defaults.NSSecureCodingKey("observeNSSecureCodingKey2", default: ExamplePersistentHistory(value: "TestValue")) - let expect = expectation(description: "Observation closure being called") - - var observation: Defaults.Observation! - var counter = 0 - observation = Defaults.observe(keys: key1, key2, options: []) { - counter += 1 - if counter == 2 { - expect.fulfill() - } else if counter > 2 { - XCTFail() - } - } - - Defaults[key1] = ExamplePersistentHistory(value: "NewTestValue1") - Defaults[key2] = ExamplePersistentHistory(value: "NewTestValue2") - observation.invalidate() - - waitForExpectations(timeout: 10) - } - func testObserveKeyURL() { - let fixtureURL = URL(string: "https://sindresorhus.com")! - let fixtureURL2 = URL(string: "https://example.com")! let key = Defaults.Key("observeKeyURL", default: fixtureURL) let expect = expectation(description: "Observation closure being called") @@ -519,23 +315,6 @@ final class DefaultsTests: XCTestCase { waitForExpectations(timeout: 10) } - func testObserveKeyEnum() { - let key = Defaults.Key("observeKeyEnum", default: .oneHour) - let expect = expectation(description: "Observation closure being called") - - var observation: Defaults.Observation! - observation = Defaults.observe(key, options: []) { change in - XCTAssertEqual(change.oldValue, .oneHour) - XCTAssertEqual(change.newValue, .tenMinutes) - observation.invalidate() - expect.fulfill() - } - - Defaults[key] = .tenMinutes - - waitForExpectations(timeout: 10) - } - func testObservePreventPropagation() { let key1 = Defaults.Key("preventPropagation0", default: nil) let expect = expectation(description: "No infinite recursion") @@ -594,7 +373,7 @@ final class DefaultsTests: XCTestCase { XCTAssert(Defaults[key1]! == 4) expect.fulfill() } else { - usleep(100_000) + usleep(300_000) print("--- Release: \(Thread.isMainThread)") } } @@ -699,120 +478,6 @@ final class DefaultsTests: XCTestCase { waitForExpectations(timeout: 10) } - func testResetKey() { - let defaultFixture1 = "foo1" - let defaultFixture2 = 0 - let defaultFixture3 = "foo3" - let newFixture1 = "bar1" - let newFixture2 = 1 - let newFixture3 = "bar3" - let key1 = Defaults.Key("key1", default: defaultFixture1) - let key2 = Defaults.Key("key2", default: defaultFixture2) - Defaults[key1] = newFixture1 - Defaults[key2] = newFixture2 - Defaults.reset(key1) - XCTAssertEqual(Defaults[key1], defaultFixture1) - XCTAssertEqual(Defaults[key2], newFixture2) - - if #available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) { - let key3 = Defaults.NSSecureCodingKey("key3", default: ExamplePersistentHistory(value: defaultFixture3)) - Defaults[key3] = ExamplePersistentHistory(value: newFixture3) - Defaults.reset(key3) - - XCTAssertEqual(Defaults[key3].value, defaultFixture3) - } - } - - func testResetMultipleKeys() { - let defaultFxiture1 = "foo1" - let defaultFixture2 = 0 - let defaultFixture3 = "foo3" - let newFixture1 = "bar1" - let newFixture2 = 1 - let newFixture3 = "bar3" - let key1 = Defaults.Key("akey1", default: defaultFxiture1) - let key2 = Defaults.Key("akey2", default: defaultFixture2) - let key3 = Defaults.Key("akey3", default: defaultFixture3) - Defaults[key1] = newFixture1 - Defaults[key2] = newFixture2 - Defaults[key3] = newFixture3 - Defaults.reset(key1, key2) - XCTAssertEqual(Defaults[key1], defaultFxiture1) - XCTAssertEqual(Defaults[key2], defaultFixture2) - XCTAssertEqual(Defaults[key3], newFixture3) - } - - func testResetOptionalKey() { - let newString1 = "bar1" - let newString2 = "bar2" - let newString3 = "bar3" - let key1 = Defaults.Key("optionalKey1") - let key2 = Defaults.Key("optionalKey2") - Defaults[key1] = newString1 - Defaults[key2] = newString2 - Defaults.reset(key1) - XCTAssertNil(Defaults[key1]) - XCTAssertEqual(Defaults[key2], newString2) - - if #available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, iOSApplicationExtension 11.0, macOSApplicationExtension 10.13, tvOSApplicationExtension 11.0, watchOSApplicationExtension 4.0, *) { - let key3 = Defaults.NSSecureCodingOptionalKey("optionalKey3") - Defaults[key3] = ExamplePersistentHistory(value: newString3) - Defaults.reset(key3) - XCTAssertNil(Defaults[key3]) - } - } - - func testResetMultipleOptionalKeys() { - let newFixture1 = "bar1" - let newFixture2 = 1 - let newFixture3 = "bar3" - let key1 = Defaults.Key("aoptionalKey1") - let key2 = Defaults.Key("aoptionalKey2") - let key3 = Defaults.Key("aoptionalKey3") - Defaults[key1] = newFixture1 - Defaults[key2] = newFixture2 - Defaults[key3] = newFixture3 - Defaults.reset(key1, key2) - XCTAssertNil(Defaults[key1]) - XCTAssertNil(Defaults[key2]) - XCTAssertEqual(Defaults[key3], newFixture3) - } - - func testObserveWithLifetimeTie() { - let key = Defaults.Key("lifetimeTie", default: false) - let expect = expectation(description: "Observation closure being called") - - weak var observation: Defaults.Observation! - observation = Defaults.observe(key, options: []) { _ in - observation.invalidate() - expect.fulfill() - } - .tieToLifetime(of: self) - - Defaults[key] = true - - waitForExpectations(timeout: 10) - } - - func testObserveWithLifetimeTieManualBreak() { - let key = Defaults.Key("lifetimeTieManualBreak", default: false) - - weak var observation: Defaults.Observation? = Defaults.observe(key, options: []) { _ in }.tieToLifetime(of: self) - observation!.removeLifetimeTie() - - for index in 1...10 { - if observation == nil { - break - } - - sleep(1) - - if index == 10 { - XCTFail() - } - } - } - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) func testRemoveDuplicatesObserveKeyCombine() { let key = Defaults.Key("observeKey", default: false) @@ -869,61 +534,88 @@ final class DefaultsTests: XCTestCase { waitForExpectations(timeout: 10) } - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) - func testRemoveDuplicatesObserveNSSecureCodingKeyCombine() { - let key = Defaults.NSSecureCodingKey("observeNSSecureCodingKey", default: ExamplePersistentHistory(value: "TestValue")) + func testResetKey() { + let defaultFixture1 = "foo1" + let defaultFixture2 = 0 + let newFixture1 = "bar1" + let newFixture2 = 1 + let key1 = Defaults.Key("key1", default: defaultFixture1) + let key2 = Defaults.Key("key2", default: defaultFixture2) + Defaults[key1] = newFixture1 + Defaults[key2] = newFixture2 + Defaults.reset(key1) + XCTAssertEqual(Defaults[key1], defaultFixture1) + XCTAssertEqual(Defaults[key2], newFixture2) + } + + func testResetMultipleKeys() { + let defaultFxiture1 = "foo1" + let defaultFixture2 = 0 + let defaultFixture3 = "foo3" + let newFixture1 = "bar1" + let newFixture2 = 1 + let newFixture3 = "bar3" + let key1 = Defaults.Key("akey1", default: defaultFxiture1) + let key2 = Defaults.Key("akey2", default: defaultFixture2) + let key3 = Defaults.Key("akey3", default: defaultFixture3) + Defaults[key1] = newFixture1 + Defaults[key2] = newFixture2 + Defaults[key3] = newFixture3 + Defaults.reset(key1, key2) + XCTAssertEqual(Defaults[key1], defaultFxiture1) + XCTAssertEqual(Defaults[key2], defaultFixture2) + XCTAssertEqual(Defaults[key3], newFixture3) + } + + func testResetMultipleOptionalKeys() { + let newFixture1 = "bar1" + let newFixture2 = 1 + let newFixture3 = "bar3" + let key1 = Defaults.Key("aoptionalKey1") + let key2 = Defaults.Key("aoptionalKey2") + let key3 = Defaults.Key("aoptionalKey3") + Defaults[key1] = newFixture1 + Defaults[key2] = newFixture2 + Defaults[key3] = newFixture3 + Defaults.reset(key1, key2) + XCTAssertNil(Defaults[key1]) + XCTAssertNil(Defaults[key2]) + XCTAssertEqual(Defaults[key3], newFixture3) + } + + func testObserveWithLifetimeTie() { + let key = Defaults.Key("lifetimeTie", default: false) let expect = expectation(description: "Observation closure being called") - let inputArray = ["NewTestValue", "NewTestValue", "NewTestValue", "NewTestValue2", "NewTestValue2", "NewTestValue2", "NewTestValue3", "NewTestValue3"] - let expectedArray = ["NewTestValue", "NewTestValue2", "NewTestValue3"] - - let cancellable = Defaults - .publisher(key, options: []) - .removeDuplicates() - .map(\.newValue.value) - .collect(expectedArray.count) - .sink { result in - print("Result array: \(result)") - result == expectedArray ? expect.fulfill() : XCTFail("Expected Array is not matched") - } - - inputArray.forEach { - Defaults[key] = ExamplePersistentHistory(value: $0) + weak var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { _ in + observation.invalidate() + expect.fulfill() } + .tieToLifetime(of: self) - Defaults.reset(key) - cancellable.cancel() + Defaults[key] = true waitForExpectations(timeout: 10) } - @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) - func testRemoveDuplicatesObserveNSSecureCodingOptionalKeyCombine() { - let key = Defaults.NSSecureCodingOptionalKey("observeNSSecureCodingOptionalKey") - let expect = expectation(description: "Observation closure being called") + func testObserveWithLifetimeTieManualBreak() { + let key = Defaults.Key("lifetimeTieManualBreak", default: false) - let inputArray = ["NewTestValue", "NewTestValue", "NewTestValue", "NewTestValue2", "NewTestValue2", "NewTestValue2", "NewTestValue3", "NewTestValue3"] - let expectedArray = ["NewTestValue", "NewTestValue2", "NewTestValue3", nil] + weak var observation: Defaults.Observation? = Defaults.observe(key, options: []) { _ in }.tieToLifetime(of: self) + observation!.removeLifetimeTie() - let cancellable = Defaults - .publisher(key, options: []) - .removeDuplicates() - .map(\.newValue) - .map { $0?.value } - .collect(expectedArray.count) - .sink { result in - print("Result array: \(result)") - result == expectedArray ? expect.fulfill() : XCTFail("Expected Array is not matched") + for index in 1...10 { + if observation == nil { + break } - inputArray.forEach { - Defaults[key] = ExamplePersistentHistory(value: $0) + sleep(1) + + if index == 10 { + XCTFail() + } } - - Defaults.reset(key) - cancellable.cancel() - - waitForExpectations(timeout: 10) } @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) diff --git a/Tests/DefaultsTests/DefaultsUIColorTests.swift b/Tests/DefaultsTests/DefaultsUIColorTests.swift new file mode 100644 index 0000000..0e615fd --- /dev/null +++ b/Tests/DefaultsTests/DefaultsUIColorTests.swift @@ -0,0 +1,308 @@ +#if !os(macOS) +import Foundation +import Defaults +import XCTest +import UIKit + +private let fixtureColor = UIColor(red: CGFloat(103) / CGFloat(0xFF), green: CGFloat(132) / CGFloat(0xFF), blue: CGFloat(255) / CGFloat(0xFF), alpha: 1) +private let fixtureColor1 = UIColor(red: CGFloat(255) / CGFloat(0xFF), green: CGFloat(241) / CGFloat(0xFF), blue: CGFloat(180) / CGFloat(0xFF), alpha: 1) +private let fixtureColor2 = UIColor(red: CGFloat(255) / CGFloat(0xFF), green: CGFloat(180) / CGFloat(0xFF), blue: CGFloat(194) / CGFloat(0xFF), alpha: 1) + +extension Defaults.Keys { + fileprivate static let color = Defaults.Key("NSColor", default: fixtureColor) + fileprivate static let colorArray = Defaults.Key<[UIColor]>("NSColorArray", default: [fixtureColor]) + fileprivate static let colorDictionary = Defaults.Key<[String: UIColor]>("NSColorArray", default: ["0": fixtureColor]) +} + +final class DefaultsNSColorTests: XCTestCase { + override func setUp() { + super.setUp() + Defaults.removeAll() + } + + override func tearDown() { + super.tearDown() + Defaults.removeAll() + } + + func testKey() { + let key = Defaults.Key("independentNSColorKey", default: fixtureColor) + XCTAssertTrue(Defaults[key].isEqual(fixtureColor)) + Defaults[key] = fixtureColor1 + XCTAssertTrue(Defaults[key].isEqual(fixtureColor1)) + } + + func testOptionalKey() { + let key = Defaults.Key("independentNSColorOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = fixtureColor + XCTAssertTrue(Defaults[key]?.isEqual(fixtureColor) ?? false) + } + + func testArrayKey() { + let key = Defaults.Key<[UIColor]>("independentNSColorArrayKey", default: [fixtureColor]) + XCTAssertTrue(Defaults[key][0].isEqual(fixtureColor)) + Defaults[key].append(fixtureColor1) + XCTAssertTrue(Defaults[key][1].isEqual(fixtureColor1)) + } + + func testArrayOptionalKey() { + let key = Defaults.Key<[UIColor]?>("independentNSColorOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = [fixtureColor] + Defaults[key]?.append(fixtureColor1) + XCTAssertTrue(Defaults[key]?[0].isEqual(fixtureColor) ?? false) + XCTAssertTrue(Defaults[key]?[1].isEqual(fixtureColor1) ?? false) + } + + func testNestedArrayKey() { + let key = Defaults.Key<[[UIColor]]>("independentNSColorNestedArrayKey", default: [[fixtureColor]]) + XCTAssertTrue(Defaults[key][0][0].isEqual(fixtureColor)) + Defaults[key][0].append(fixtureColor1) + Defaults[key].append([fixtureColor2]) + XCTAssertTrue(Defaults[key][0][1].isEqual(fixtureColor1)) + XCTAssertTrue(Defaults[key][1][0].isEqual(fixtureColor2)) + } + + func testArrayDictionaryKey() { + let key = Defaults.Key<[[String: UIColor]]>("independentNSColorArrayDictionaryKey", default: [["0": fixtureColor]]) + XCTAssertTrue(Defaults[key][0]["0"]?.isEqual(fixtureColor) ?? false) + Defaults[key][0]["1"] = fixtureColor1 + Defaults[key].append(["0": fixtureColor2]) + XCTAssertTrue(Defaults[key][0]["1"]?.isEqual(fixtureColor1) ?? false) + XCTAssertTrue(Defaults[key][1]["0"]?.isEqual(fixtureColor2) ?? false) + } + + func testDictionaryKey() { + let key = Defaults.Key<[String: UIColor]>("independentNSColorDictionaryKey", default: ["0": fixtureColor]) + XCTAssertTrue(Defaults[key]["0"]?.isEqual(fixtureColor) ?? false) + Defaults[key]["1"] = fixtureColor1 + XCTAssertTrue(Defaults[key]["1"]?.isEqual(fixtureColor1) ?? false) + } + + func testDictionaryOptionalKey() { + let key = Defaults.Key<[String: UIColor]?>("independentNSColorDictionaryOptionalKey") + XCTAssertNil(Defaults[key]) + Defaults[key] = ["0": fixtureColor] + Defaults[key]?["1"] = fixtureColor1 + XCTAssertTrue(Defaults[key]?["0"]?.isEqual(fixtureColor) ?? false) + XCTAssertTrue(Defaults[key]?["1"]?.isEqual(fixtureColor1) ?? false) + } + + func testDictionaryArrayKey() { + let key = Defaults.Key<[String: [UIColor]]>("independentNSColorDictionaryArrayKey", default: ["0": [fixtureColor]]) + XCTAssertTrue(Defaults[key]["0"]?[0].isEqual(fixtureColor) ?? false) + Defaults[key]["0"]?.append(fixtureColor1) + Defaults[key]["1"] = [fixtureColor2] + XCTAssertTrue(Defaults[key]["0"]?[1].isEqual(fixtureColor1) ?? false) + XCTAssertTrue(Defaults[key]["1"]?[0].isEqual(fixtureColor2) ?? false) + } + + func testType() { + XCTAssert(Defaults[.color].isEqual(fixtureColor)) + Defaults[.color] = fixtureColor1 + XCTAssert(Defaults[.color].isEqual(fixtureColor1)) + } + + func testArrayType() { + XCTAssertTrue(Defaults[.colorArray][0].isEqual(fixtureColor)) + Defaults[.colorArray][0] = fixtureColor1 + XCTAssertTrue(Defaults[.colorArray][0].isEqual(fixtureColor1)) + } + + func testDictionaryType() { + XCTAssertTrue(Defaults[.colorDictionary]["0"]?.isEqual(fixtureColor) ?? false) + Defaults[.colorDictionary]["0"] = fixtureColor1 + XCTAssertTrue(Defaults[.colorDictionary]["0"]?.isEqual(fixtureColor1) ?? false) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveKeyCombine() { + let key = Defaults.Key("observeNSColorKeyCombine", default: fixtureColor) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let cancellable = publisher.sink { tuples in + for (index, expected) in [(fixtureColor, fixtureColor1), (fixtureColor1, fixtureColor)].enumerated() { + XCTAssertTrue(expected.0.isEqual(tuples[index].0)) + XCTAssertTrue(expected.1.isEqual(tuples[index].1)) + } + + expect.fulfill() + } + + Defaults[key] = fixtureColor1 + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveOptionalKeyCombine() { + let key = Defaults.Key("observeNSColorOptionalKeyCombine") + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(3) + + let expectedValue: [(UIColor?, UIColor?)] = [(nil, fixtureColor), (fixtureColor, fixtureColor1), (fixtureColor1, nil)] + + let cancellable = publisher.sink { tuples in + for (index, expected) in expectedValue.enumerated() { + guard let oldValue = expected.0 else { + XCTAssertNil(tuples[index].0) + continue + } + guard let newValue = expected.1 else { + XCTAssertNil(tuples[index].1) + continue + } + XCTAssertTrue(oldValue.isEqual(tuples[index].0)) + XCTAssertTrue(newValue.isEqual(tuples[index].1)) + } + + expect.fulfill() + } + + Defaults[key] = fixtureColor + Defaults[key] = fixtureColor1 + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveArrayKeyCombine() { + let key = Defaults.Key<[UIColor]>("observeNSColorArrayKeyCombine", default: [fixtureColor]) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let cancellable = publisher.sink { tuples in + for (index, expected) in [(fixtureColor, fixtureColor1), (fixtureColor1, fixtureColor)].enumerated() { + XCTAssertTrue(expected.0.isEqual(tuples[index].0[0])) + XCTAssertTrue(expected.1.isEqual(tuples[index].1[0])) + } + + expect.fulfill() + } + + Defaults[key][0] = fixtureColor1 + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, iOSApplicationExtension 13.0, macOSApplicationExtension 10.15, tvOSApplicationExtension 13.0, watchOSApplicationExtension 6.0, *) + func testObserveDictionaryKeyCombine() { + let key = Defaults.Key<[String: UIColor]>("observeNSColorDictionaryKeyCombine", default: ["0": fixtureColor]) + let expect = expectation(description: "Observation closure being called") + + let publisher = Defaults + .publisher(key, options: []) + .map { ($0.oldValue, $0.newValue) } + .collect(2) + + let cancellable = publisher.sink { tuples in + for (index, expected) in [(fixtureColor, fixtureColor1), (fixtureColor1, fixtureColor)].enumerated() { + XCTAssertTrue(expected.0.isEqual(tuples[index].0["0"])) + XCTAssertTrue(expected.1.isEqual(tuples[index].1["0"])) + } + + expect.fulfill() + } + + Defaults[key]["0"] = fixtureColor1 + Defaults.reset(key) + cancellable.cancel() + + waitForExpectations(timeout: 10) + } + + func testObserveKey() { + let key = Defaults.Key("observeNSColorKey", default: fixtureColor) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertTrue(change.oldValue.isEqual(fixtureColor)) + XCTAssertTrue(change.newValue.isEqual(fixtureColor1)) + observation.invalidate() + expect.fulfill() + } + + Defaults[key] = fixtureColor1 + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveOptionalKey() { + let key = Defaults.Key("observeNSColorOptionalKey") + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertNil(change.oldValue) + XCTAssertTrue(change.newValue?.isEqual(fixtureColor) ?? false) + observation.invalidate() + expect.fulfill() + } + + Defaults[key] = fixtureColor + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveArrayKey() { + let key = Defaults.Key<[UIColor]>("observeNSColorArrayKey", default: [fixtureColor]) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertTrue(change.oldValue[0].isEqual(fixtureColor)) + XCTAssertTrue(change.newValue[0].isEqual(fixtureColor)) + XCTAssertTrue(change.newValue[1].isEqual(fixtureColor1)) + observation.invalidate() + expect.fulfill() + } + + Defaults[key].append(fixtureColor1) + observation.invalidate() + + waitForExpectations(timeout: 10) + } + + func testObserveDictionaryKey() { + let key = Defaults.Key<[String: UIColor]>("observeNSColorDictionaryKey", default: ["0": fixtureColor]) + let expect = expectation(description: "Observation closure being called") + + var observation: Defaults.Observation! + observation = Defaults.observe(key, options: []) { change in + XCTAssertTrue(change.oldValue["0"]?.isEqual(fixtureColor) ?? false) + XCTAssertTrue(change.newValue["0"]?.isEqual(fixtureColor) ?? false) + XCTAssertTrue(change.newValue["1"]?.isEqual(fixtureColor1) ?? false) + observation.invalidate() + expect.fulfill() + } + + Defaults[key]["1"] = fixtureColor1 + observation.invalidate() + + waitForExpectations(timeout: 10) + } +} +#endif diff --git a/migration.md b/migration.md new file mode 100644 index 0000000..2e383f9 --- /dev/null +++ b/migration.md @@ -0,0 +1,404 @@ +# Migration Guide From v4 to v5 + +## Warning + +If the migration is not success or incomplete. Edit `Defaults.Key` might cause data loss. +**Please back up your UserDefaults data before migration.** + +## Summary + +Before v4, `Defaults` store `Codable` types as a JSON string. +After v5, `Defaults` store `Defaults.Serializable` types with `UserDefaults` native supported type. + +```swift +// Before +let key = Defaults.Key<[String: Int]>("key", default: ["0": 0]) + +UserDefaults.standard.string(forKey: "key") //=> "["0": 0]" + +// After v5 +let key = Defaults.Key<[String: Int]>("key", default: ["0": 0]) + +UserDefaults.standard.dictionary(forKey: "key") //=> [0: 0] +``` + +All types should conform to `Defaults.Serializable` in order to work with `Defaults`. +So this will require some migrations to resolve **TWO** major issues. + +## Issues + +1. **Compiler complain that `Defaults.Key` is not conform to `Defaults.Serializable`.** + Since we replace `Codable` with `Defaults.Serializable`, `Key` will have to conform to `Value: Defaults.Serializable`. + For this situation, please follow the guide below: + - [From `Codable` struct in Defaults v4 to `Codable` struct in Defaults v5](#from-codable-struct-in-defaults-v4-to-codable-struct-in-defaults-v5) + - [From `Codable` enum in Defaults v4 to `Codable` enum in Defaults v5](#from-codable-enum-in-defaults-v4-to-codable-enum-in-defaults-v5) + +2. **Previous value in UserDefaults is not readable. (ex. `Defaults[.array]` return `null`).** + In v5, `Defaults` reads value from `UserDefaults` as a native supported type. + But `UserDefaults` only contains JSON string before migration, `Defaults` will not be able to work with it. + For this situation, `Defaults` provides `Defaults.migrate` method to automate the migration process. + - [From `Codable Array/Dictionary/Set` in Defaults v4 to `Native Array/Dictionary/Set`(With Native Supported Elements) in Defaults v5](#from-codable-arraydictionaryset-in-defaults-v4-to-native-arraydictionarysetwith-native-supported-elements-in-defaults-v5) + - [From `Codable Array/Dictionary/Set` in Defaults v4 to `Native Array/Dictionary/Set`(With Codable Elements) in Defaults v5](#from-codable-arraydictionaryset-in-defaults-v4-to-native-arraydictionarysetwith-codable-elements-in-defaults-v5) + + **Caution:** + - This is a breaking change, there is no way to convert it back to `Codable Array/Dictionary/Set` so far. + +- **Optional migration** + `Defaults` also provide a migration guide to let users convert them `Codable` things into the UserDefaults native supported type, but it is optional. + - [From `Codable` enum in Defaults v4 to `RawRepresentable` in Defaults v5](#from-codable-enum-in-defaults-v4-to-rawrepresentable-in-defaults-v5-optional) + - [From `Codable` struct in Defaults v4 to `Dictionary` in Defaults v5](#from-codable-struct-in-defaults-v4-to-dictionary-in-defaults-v5-optional) + +## Testing + +We recommend user doing some tests after migration. +The most critical issue is the second one (Previous value in UserDefaults is not readable). +After migration, there is a need to make sure user can get the same value as before. +You can try to test it manually or making a test file to test it. + +Here is the guide for making a migration test: +For example you are trying to migrate a `Codable String` array to native array. + +1. Get previous value in UserDefaults (using `defaults` command or whatever you want). + +```swift +let string = "[\"a\",\"b\",\"c\"]" +``` + +2. Insert the value above into UserDefaults. + +```swift +UserDefaults.standard.set(string, forKey: "testKey") +``` + +3. Call `Defaults.migrate` and then using `Defaults` to get its value + +```swift +let key = Defaults.Key<[String]>("testKey", default: []) +Defaults.migrate(key, to: .v5) + +Defaults[key] //=> [a, b, c] +``` + +--- + +### From `Codable` struct in Defaults v4 to `Codable` struct in Defaults v5 + +Before v4, `struct` have to conform to `Codable` to store it as a JSON string. + +After v5, `struct` have to conform to `Defaults.Serializable & Codable` to store it as a JSON string. + +#### Before migration, your code should be like this + +```swift +private struct TimeZone: Codable { + var id: String + var name: String +} + +extension Defaults.Keys { + static let timezone = Defaults.Key("TimeZone") +} +``` + +#### Migration steps + +1. Let `TimeZone` conform to `Defaults.Serializable`. + +```swift +private struct TimeZone: Defaults.Serializable, Codable { + var id: String + var name: String +} +``` + +2. Now `Defaults[.timezone]` should be readable. + +--- + +### From `Codable` enum in Defaults v4 to `Codable` enum in Defaults v5 + +Before v4, `enum` have to conform to `Codable` to store it as a JSON string. + +After v5, struct have to conform to `Defaults.Serializable & Codable` to store it as a JSON string. + +#### Before migration, your code should be like this + +```swift +private enum Period: String, Codable { + case tenMinutes = "10 Minutes" + case halfHour = "30 Minutes" + case oneHour = "1 Hour" +} + +extension Defaults.Keys { + static let period = Defaults.Key("period") +} +``` + +#### Migration steps + +1. Let `Period` conform to `Defaults.Serializable`. + +```swift +private enum Period: String, Defaults.Serializable, Codable { + case tenMinutes = "10 Minutes" + case halfHour = "30 Minutes" + case oneHour = "1 Hour" +} +``` + +2. Now `Defaults[.period]` should be readable. + +--- + +### From `Codable Array/Dictionary/Set` in Defaults v4 to `Native Array/Dictionary/Set`(With Native Supported Elements) in Defaults v5 + +Before v4, `Defaults` will store array/dictionary as a JSON string(`["a", "b", "c"]`). + +After v5, `Defaults` will store it as a native array/dictionary with native supported elements(`[a, b, c]` ). + +#### Before migration, your code should be like this + +```swift +extension Defaults.Keys { + static let arrayString = Defaults.Key<[String]?>("arrayString") + static let setString = Defaults.Key?>("setString") + static let dictionaryStringInt = Defaults.Key<[String: Int]?>("dictionaryStringInt") + static let dictionaryStringIntInArray = Defaults.Key<[[String: Int]]?>("dictionaryStringIntInArray") +} +``` + +#### Migration steps + +1. **Call `Defaults.migration(.arrayString, to: .v5)`, `Defaults.migration(.setString, to: .v5)`, `Defaults.migration(.dictionaryStringInt, to: .v5)`, `Defaults.migration(.dictionaryStringIntInArray, to: .v5)`.** +2. Now `Defaults[.arrayString]`, `Defaults.[.setString]`, `Defaults[.dictionaryStringInt]`, `Defaults[.dictionaryStringIntInArray]` should be readable. + +--- + +### From `Codable Array/Dictionary/Set` in Defaults v4 to `Native Array/Dictionary/Set`(With Codable Elements) in Defaults v5 + +Before v4, `Defaults` will store array/dictionary as a JSON string(`"{ "id": "0", "name": "Asia/Taipei" }"`, `"["10 Minutes", "30 Minutes"]"`). + +After v5, `Defaults` will store it as a native array/dictionary with codable elements(`{ id: 0, name: Asia/Taipei }`, `[10 Minutes, 30 Minutes]`). + +#### Before migration, your code should be like this + +```swift +private struct TimeZone: Codable, Hashable { + var id: String + var name: String +} +private enum Period: String, Codable, Hashable { + case tenMinutes = "10 Minutes" + case halfHour = "30 Minutes" + case oneHour = "1 Hour" +} + +extension Defaults.Keys { + static let arrayTimezone = Defaults.Key<[TimeZone]?>("arrayTimezone") + static let setTimezone = Defaults.Key<[TimeZone]?>("setTimezone") + static let arrayPeriod = Defaults.Key<[Period]?>("arrayPeriod") + static let setPeriod = Defaults.Key<[Period]?>("setPeriod") + static let dictionaryTimezone = Defaults.Key<[String: TimeZone]?>("dictionaryTimezone") + static let dictionaryPeriod = Defaults.Key<[String: Period]?>("dictionaryPeriod") +} +``` + +#### Migration steps + +1. Let `TimeZone` and `Period` conform to `Defaults.Serializable` + +```swift +private struct TimeZone: Defaults.Serializable, Codable, Hashable { + var id: String + var name: String +} + +private enum Period: String, Defaults.Serializable, Codable, Hashable { + case tenMinutes = "10 Minutes" + case halfHour = "30 Minutes" + case oneHour = "1 Hour" +} +``` + +2. **Call `Defaults.migration(.arrayTimezone, to: .v5)`, `Defaults.migration(.setTimezone, to: .v5)`, `Defaults.migration(.dictionaryTimezone, to: .v5)`, `Defaults.migration(.arrayPeriod, to: .v5)`, `Defaults.migration(.setPeriod, to: .v5)` , `Defaults.migration(.dictionaryPeriod, to: .v5)`.** +3. Now `Defaults[.arrayTimezone]`, `Defaults[.setTimezone]`, `Defaults[.dictionaryTimezone]`, `Defaults[.arrayPeriod]`, `Defaults[.setPeriod]` , `Defaults[.dictionaryPeriod]` should be readable. + +--- + +### From `Codable` enum in Defaults v4 to `RawRepresentable` in Defaults v5 (Optional) + +Before v4, `Defaults` will store `enum` as a JSON string(`"10 Minutes"`). + +After v5, `Defaults` will store `enum` as a `RawRepresentable`(`10 Minutes`). + +#### Before migration, your code should be like this + +```swift +private enum Period: String, Codable { + case tenMinutes = "10 Minutes" + case halfHour = "30 Minutes" + case oneHour = "1 Hour" +} + +extension Defaults.Keys { + static let period = Defaults.Key("period") +} +``` + +#### Migration steps + +1. Create an enum call `CodablePeriod` and create an extension of it. Let it conform to `Defaults.CodableType` and associated `NativeForm` to `Period`. + +```swift +private enum CodablePeriod: String { + case tenMinutes = "10 Minutes" + case halfHour = "30 Minutes" + case oneHour = "1 Hour" +} + +extension CodablePeriod: Defaults.CodableType { + typealias NativeForm = Period +} +``` + +2. Remove `Codable`. So `Period` can be stored natively. + +```swift +private enum Period: String { + case tenMinutes = "10 Minutes" + case halfHour = "30 Minutes" + case oneHour = "1 Hour" +} +``` + +3. Create an extension of `Period`, let it conform to `Defaults.NativeType` and its `CodableForm` should be `CodablePeriod`. + +```swift +extension Period: Defaults.NativeType { + typealias CodableForm = CodablePeriod +} +``` + +4. **Call `Defaults.migration(.period)`** +5. Now `Defaults[.period]` should be readable. + +* hints: You can also implement `toNative` function at `Defaults.CodableType` in your own way. + +For example + +```swift +extension CodablePeriod: Defaults.CodableType { + typealias NativeForm = Period + + public func toNative() -> Period { + switch self { + case .tenMinutes: + return .tenMinutes + case .halfHour: + return .halfHour + case .oneHour: + return .oneHour + } + } +} +``` + + +--- + +### From `Codable` struct in Defaults v4 to `Dictionary` in Defaults v5 (Optional) + +This happens when you have a struct which is stored as a codable JSON string before, but now you want it to be stored as a native UserDefaults dictionary. + +#### Before migration, your code should be like this + +```swift +private struct TimeZone: Codable { + var id: String + var name: String +} + +extension Defaults.Keys { + static let timezone = Defaults.Key("TimeZone") + static let arrayTimezone = Defaults.Key<[TimeZone]?>("arrayTimezone") + static let setTimezone = Defaults.Key?>("setTimezone") + static let dictionaryTimezone = Defaults.Key<[String: TimeZone]?>("setTimezone") +} +``` + +#### Migration steps + +1. Create a `TimeZoneBridge` which conform to `Defaults.Bridge` and its `Value` is TimeZone, `Serializable` is `[String: String]`. + +```swift +private struct TimeZoneBridge: Defaults.Bridge { + typealias Value = TimeZone + typealias Serializable = [String: String] + + func serialize(_ value: TimeZone?) -> Serializable? { + guard let value = value else { + return nil + } + + return ["id": value.id, "name": value.name] + } + + func deserialize(_ object: Serializable?) -> TimeZone? { + guard + let dictionary = object, + let id = dictionary["id"], + let name = dictionary["name"] + else { + return nil + } + + return TimeZone(id: id, name: name) + } +} +``` + +2. Create an extension of `TimeZone`, let it conform to `Defaults.NativeType` and its static bridge is `TimeZoneBridge`(Compiler will complain that `TimeZone` is not conform to `Defaults.NativeType`, will resolve it later). + +```swift +private struct TimeZone: Hashable { + var id: String + var name: String +} + +extension TimeZone: Defaults.NativeType { + static let bridge = TimeZoneBridge() +} +``` + +3. Create an extension of `CodableTimeZone` and let it conform to `Defaults.CodableType` + +```swift +private struct CodableTimeZone { + var id: String + var name: String +} + +extension CodableTimeZone: Defaults.CodableType { + /// Convert from `Codable` to `Native` + func toNative() -> TimeZone { + TimeZone(id: id, name: name) + } +} +``` + +4. Associate `TimeZone.CodableForm` to `CodableTimeZone` + +```swift +extension TimeZone: Defaults.NativeType { + /// Associated `CodableForm` to `CodableTimeZone` + typealias CodableForm = CodableTimeZone + + static let bridge = TimeZoneBridge() +} +``` + +5. **Call `Defaults.migration(.timezone, to: .v5)`, `Defaults.migration(.arrayTimezone, to: .v5)`, `Defaults.migration(.setTimezone, to: .v5)`, `Defaults.migration(.dictionaryTimezone, to: .v5)`**. +6. Now `Defaults[.timezone]`, `Defaults[.arrayTimezone]` , `Defaults[.setTimezone]`, `Defaults[.dictionaryTimezone]` should be readable. + +**See [DefaultsMigrationTests.swift](./Tests/DefaultsTests/DefaultsMigrationTests.swift) for more example.** \ No newline at end of file diff --git a/readme.md b/readme.md index acd0096..61e90b4 100644 --- a/readme.md +++ b/readme.md @@ -19,6 +19,7 @@ For a real-world example, see my [Plash app](https://github.com/sindresorhus/Pla - **Publishers:** Combine publishers built-in. - **Observation:** Observe changes to keys. - **Debuggable:** The data is stored as JSON-serialized values. +- **Customizable:** You can serialize and deserialize your own type in your own way. ## Compatibility @@ -27,6 +28,10 @@ For a real-world example, see my [Plash app](https://github.com/sindresorhus/Pla - tvOS 10+ - watchOS 3+ +## Migration Guides + +#### [From v4 to v5](./migration.md) + ## Install #### Swift Package Manager @@ -45,6 +50,29 @@ github "sindresorhus/Defaults" pod 'Defaults' ``` +## Support types + +| Single Value | +|:------------------:| +| `Int(8/16/32/64)` | +| `UInt(8/16/32/64)` | +| `Double` | +| `Float` | +| `String` | +| `CGFloat` | +| `Bool` | +| `Date` | +| `Data` | +| `URL` | +| `NSColor` (macOS) | +| `UIColor` (iOS) | +| `Codable` | + +The list above only show the type that does not need further more configuration. +We also support them wrapped in `Array`, `Set`, `Dictionary` even wrapped in nested type. ex. `[[String: Set<[String: Int]>]]`. +For more types, see [Enum Example](#enum-example), [Codable Example](#codable-example) or [Advanced Usage](#advanced-usage). +For more examples, see [Tests/DefaultsTests](./Tests/DefaultsTests). + ## Usage You declare the defaults keys upfront with type and default value. @@ -92,30 +120,10 @@ The default value is then `nil`. --- -If you have `NSSecureCoding` classes which you want to save, you can use them as follows: - -```swift -extension Defaults.Keys { - static let someSecureCoding = NSSecureCodingKey("someSecureCoding", default: SomeNSSecureCodingClass(string: "Default", int: 5, bool: true)) - static let someOptionalSecureCoding = NSSecureCodingOptionalKey("someOptionalSecureCoding") -} - -Defaults[.someSecureCoding].string -//=> "Default" - -Defaults[.someSecureCoding].int -//=> 5 - -Defaults[.someSecureCoding].bool -//=> true -``` - -You can use those keys just like in all the other examples. The return value will be your `NSSecureCoding` class. - ### Enum example ```swift -enum DurationKeys: String, Codable { +enum DurationKeys: String, Defaults.Serializable { case tenMinutes = "10 Minutes" case halfHour = "30 Minutes" case oneHour = "1 Hour" @@ -129,6 +137,22 @@ Defaults[.defaultDuration].rawValue //=> "1 Hour" ``` +### Codable Example + +```swift +struct User: Codable, Defaults.Serializable { + let name: String + let age: String +} + +extension Defaults.Keys { + static let user = Key("user", default: .init(name: "Hello", age: "24")) +} + +Defaults[.user].name +//=> "Hello" +``` + ### Use keys directly You are not required to attach keys to `Defaults.Keys`. @@ -163,8 +187,6 @@ Note that it's `@Default`, not `@Defaults`. You cannot use `@Default` in an `ObservableObject`. It's meant to be used in a `View`. -This is only implemented for `Defaults.Key`. PR welcome for `Defaults.NSSecureCoding` if you need it. - ### Observe changes to a key ```swift @@ -317,6 +339,217 @@ print(UserDefaults.standard.bool(forKey: Defaults.Keys.isUnicornMode.name)) //=> true ``` +## Advanced Usage + +### Serialization of custom types + +Although `Defaults` already support many types internal, there might have some situations where you want to use your own type. +The guide below will show you how to make your own custom type works with `Defaults`. + +1. Create your own custom type. + +```swift +struct User { + let name: String + let age: String +} +``` + +2. Create a bridge which protocol conforms to `Defaults.Bridge`. + +```swift +struct UserBridge: Defaults.Bridge { + typealias Value = User + typealias Serializable = [String: String] + + public func serialize(_ value: Value?) -> Serializable? { + guard let value = value else { + return nil + } + + return ["name": value.name, "age": value.age] + } + + public func deserialize(_ object: Serializable?) -> Value? { + guard + let object = object, + let name = object["name"], + let age = object["age"] + else { + return nil + } + + return User(name: name, age: age) + } +} +``` + +3. Create an extension of `User`, let its protocol conforms to `Defaults.Serializable` and its static bridge should be the bridge we created above. + +```swift +struct User { + let name: String + let age: String +} + +extension User: Defaults.Serializable { + static let bridge = UserBridge() +} +``` + +4. Create some keys and enjoy it. + +```swift +extension Defaults.Keys { + static let user = Defaults.Key("user", default: User(name: "Hello", age: "24")) + static let arrayUser = Defaults.Key<[User]>("arrayUser", default: [User(name: "Hello", age: "24")]) + static let setUser = Defaults.Key>("user", default: Set([User(name: "Hello", age: "24")])) + static let dictionaryUser = Defaults.Key<[String: User]>("dictionaryUser", default: ["user": User(name: "Hello", age: "24")]) +} + +Defaults[.user].name //=> "Hello" +Defaults[.arrayUser][0].name //=> "Hello" +Defaults[.setUser].first?.name //=> "Hello" +Defaults[.dictionaryUser]["user"]?.name //=> "Hello" +``` + +### Serialization of Collection + +1. Create your Collection and its element should conforms to `Defaults.Serializable`. + +```swift +struct Bag: Collection { + var items: [Element] + + var startIndex: Int { + items.startIndex + } + + var endIndex: Int { + items.endIndex + } + + mutating func insert(element: Element, at: Int) { + items.insert(element, at: at) + } + + func index(after index: Int) -> Int { + items.index(after: index) + } + + subscript(position: Int) -> Element { + items[position] + } +} +``` + +2. Create an extension of `Bag`. let it conforms to `Defaults.CollectionSerializable` + +```swift +extension Bag: Defaults.CollectionSerializable { + init(_ elements: [Element]) { + self.items = elements + } +} + +``` + +3. Create some keys and enjoy it. + +```swift +extension Defaults.Keys { + static let stringBag = Key>("stringBag", default: Bag(["Hello", "World!"])) +} + +Defaults[.stringBag][0] //=> "Hello" +Defaults[.stringBag][1] //=> "World!" +``` + +### Serialization of SetAlgebra + +1. Create your SetAlgebra and its element should conforms to `Defaults.Serializable & Hashable` + +```swift +struct SetBag: SetAlgebra { + var store = Set() + + init() {} + + init(_ store: Set) { + self.store = store + } + + func contains(_ member: Element) -> Bool { + store.contains(member) + } + + func union(_ other: SetBag) -> SetBag { + SetBag(store.union(other.store)) + } + + func intersection(_ other: SetBag) + -> SetBag { + var setBag = SetBag() + setBag.store = store.intersection(other.store) + return setBag + } + + func symmetricDifference(_ other: SetBag) + -> SetBag { + var setBag = SetBag() + setBag.store = store.symmetricDifference(other.store) + return setBag + } + + @discardableResult + mutating func insert(_ newMember: Element) + -> (inserted: Bool, memberAfterInsert: Element) { + store.insert(newMember) + } + + mutating func remove(_ member: Element) -> Element? { + store.remove(member) + } + + mutating func update(with newMember: Element) -> Element? { + store.update(with: newMember) + } + + mutating func formUnion(_ other: SetBag) { + store.formUnion(other.store) + } + + mutating func formSymmetricDifference(_ other: SetBag) { + store.formSymmetricDifference(other.store) + } + + mutating func formIntersection(_ other: SetBag) { + store.formIntersection(other.store) + } +} +``` + +2. Create an extension of `SetBag`. Let it conforms to `Defaults.SetAlgebraSerializable` + +```swift +extension SetBag: Defaults.SetAlgebraSerializable { + func toArray() -> [Element] { + Array(store) + } +} +``` + +3. Create some keys and enjoy it. + +```swift +extension Defaults.Keys { + static let stringSet = Key>("stringSet", default: SetBag(["Hello", "World!"])) +} + +Defaults[.stringSet].contains("Hello") //=> true +Defaults[.stringSet].contains("World!") //=> true +``` + ## API ### `Defaults` @@ -327,7 +560,7 @@ Type: `class` Stores the keys. -#### `Defaults.Key` *(alias `Defaults.Keys.Key`)* +#### `Defaults.Key` _(alias `Defaults.Keys.Key`)_ ```swift Defaults.Key(_ key: String, default: T, suite: UserDefaults = .standard) @@ -339,27 +572,48 @@ Create a key with a default value. The default value is written to the actual `UserDefaults` and can be used elsewhere. For example, with a Interface Builder binding. -#### `Defaults.NSSecureCodingKey` *(alias `Defaults.Keys.NSSecureCodingKey`)* +#### `Defaults.Serializable` ```swift -Defaults.NSSecureCodingKey(_ key: String, default: T, suite: UserDefaults = .standard) +public protocol DefaultsSerializable { + typealias Value = Bridge.Value + typealias Serializable = Bridge.Serializable + associatedtype Bridge: Defaults.Bridge + + static var bridge: Bridge { get } +} ``` -Type: `class` +Type: `protocol` -Create a NSSecureCoding key with a default value. +All types conform to this protocol will be able to work with `Defaults`. -The default value is written to the actual `UserDefaults` and can be used elsewhere. For example, with a Interface Builder binding. +It should have a static variable `bridge` which protocol should conform to `Defaults.Bridge`. -#### `Defaults.NSSecureCodingOptionalKey` *(alias `Defaults.Keys.NSSecureCodingOptionalKey`)* +#### `Defaults.Bridge` ```swift -Defaults.NSSecureCodingOptionalKey(_ key: String, suite: UserDefaults = .standard) +public protocol DefaultsBridge { + associatedtype Value + associatedtype Serializable + func serialize(_ value: Value?) -> Serializable? + func deserialize(_ object: Serializable?) -> Value? +} ``` -Type: `class` +Type: `protocol` -Create a NSSecureCoding key with an optional value. +A Bridge can do serialization and de-serialization. + +Have two associate types `Value` and `Serializable`. + +`Value` is the type user want to use it. + +`Serializable` is the type stored in `UserDefaults`. + +`serialize` will be executed before storing to the `UserDefaults` . + +`deserialize` will be executed after retrieving its value from the `UserDefaults`. #### `Defaults.reset(keys…)` @@ -381,22 +635,6 @@ Defaults.observe( ) -> Defaults.Observation ``` -```swift -Defaults.observe( - _ key: Defaults.NSSecureCodingKey, - options: ObservationOptions = [.initial], - handler: @escaping (NSSecureCodingKeyChange) -> Void -) -> Defaults.Observation -``` - -```swift -Defaults.observe( - _ key: Defaults.NSSecureCodingOptionalKey, - options: ObservationOptions = [.initial], - handler: @escaping (NSSecureCodingOptionalKeyChange) -> Void -) -> Defaults.Observation -``` - Type: `func` Observe changes to a key or an optional key. @@ -420,20 +658,6 @@ Defaults.publisher( ) -> AnyPublisher, Never> ``` -```swift -Defaults.publisher( - _ key: Defaults.NSSecureCodingKey, - options: ObservationOptions = [.initial] -) -> AnyPublisher, Never> -``` - -```swift -Defaults.publisher( - _ key: Defaults.NSSecureCodingOptionalKey, - options: ObservationOptions = [.initial] -) -> AnyPublisher, Never> -``` - Type: `func` Observation API using [Publisher](https://developer.apple.com/documentation/combine/publisher) from the [Combine](https://developer.apple.com/documentation/combine) framework. @@ -505,21 +729,71 @@ Execute the closure without triggering change events. Any `Defaults` key changes made within the closure will not propagate to `Defaults` event listeners (`Defaults.observe()` and `Defaults.publisher()`). This can be useful to prevent infinite recursion when you want to change a key in the callback listening to changes for the same key. +#### `Defaults.migrate(keys..., to: Version)` + +```swift +Defaults.migrate(keys..., to: Version) +Defaults.migrate(keys..., to: Version) +``` + +Type: `func` + +Migrate the given keys to the specific version. + +You can specify up to 10 keys. If you need to specify more, call this method multiple times. + ### `@Default(_ key:)` Get/set a `Defaults` item and also have the view be updated when the value changes. -This is only implemented for `Defaults.Key`. PR welcome for `Defaults.NSSecureCoding` if you need it. +### Advanced + +#### `Defaults.CollectionSerializable` + +```swift +public protocol DefaultsCollectionSerializable: Collection, Defaults.Serializable { + init(_ elements: [Element]) +} +``` + +Type: `protocol` + +A `Collection` which can store into the native `UserDefaults`. + +It should have an initializer `init(_ elements: [Element])` to let `Defaults` do the de-serialization. + +#### `Defaults.SetAlgebraSerializable` + +```swift +public protocol DefaultsSetAlgebraSerializable: SetAlgebra, Defaults.Serializable { + func toArray() -> [Element] +} +``` + +Type: `protocol` + +A `SetAlgebra` which can store into the native `UserDefaults`. + +It should have a function `func toArray() -> [Element]` to let `Defaults` do the serialization. ## FAQ ### How can I store a dictionary of arbitrary values? -You cannot store `[String: Any]` directly as it cannot conform to `Codable`. However, you can use the [`AnyCodable`](https://github.com/Flight-School/AnyCodable) package to work around this `Codable` limitation: +After `Defaults` v5, you don't need to use `Codable` to store dictionary, `Defaults` supports storing dictionary natively. +For `Defaults` support types, see [Support types](#support-types). + +There might be situations where you want to use `[String: Any]` directly. +Unfortunately, since `Any` can not conform to `Defaults.Serializable`, `Defaults` can not support it. + +However, you can use the [`AnyCodable`](https://github.com/Flight-School/AnyCodable) package to work around this `Defaults.Serializable` limitation: ```swift import AnyCodable +/// Important: Let AnyCodable conforms to Defaults.Serializable +extension AnyCodable: Defaults.Serializable {} + extension Defaults.Keys { static let magic = Key<[String: AnyCodable]>("magic", default: [:]) }