├── .github └── workflows │ ├── deploy_documentation.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── PersistentKeyValueKit │ ├── Key-Value Persistible │ ├── Extension │ │ ├── Foundation │ │ │ ├── Data+KeyValuePersistible.swift │ │ │ └── URL+KeyValuePersistible.swift │ │ └── Standard Library │ │ │ ├── Array+KeyValuePersistible.swift │ │ │ ├── Bool+KeyValuePersistible.swift │ │ │ ├── Dictionary+KeyValuePersistible.swift │ │ │ ├── Double+KeyValuePersistible.swift │ │ │ ├── Float+KeyValuePersistible.swift │ │ │ ├── Int+KeyValuePersistible.swift │ │ │ ├── Optional+KeyValuePersistible.swift │ │ │ └── String+KeyValuePersistible.swift │ ├── KeyValuePersistible.swift │ └── PrimitiveKeyValuePersistible.swift │ ├── Persistent Key-Value Representation │ ├── Implementations │ │ ├── CodablePersistentKeyValueRepresentation.swift │ │ ├── LosslessStringConvertiblePersistentKeyValueRepresentation.swift │ │ ├── OptionalPersistentKeyValueRepresentation.swift │ │ ├── PrimitivePersistentKeyValueRepresentation.swift │ │ ├── ProxyPersistentKeyValueRepresentation.swift │ │ ├── ProxyPersistentKeyValueRepresentationProtocol.swift │ │ └── RawRepresentablePersistentKeyValueRepresentation.swift │ └── PersistentKeyValueRepresentation.swift │ ├── Persistent Key-Value Store │ ├── Extensions │ │ └── Foundation │ │ │ ├── NSUbiquitousKeyValueStore+PersistentKeyValueStore.swift │ │ │ └── UserDefaults+PersistentKeyValueStore.swift │ ├── Implementations │ │ └── InMemoryPersistentKeyValueStore.swift │ └── PersistentKeyValueStore.swift │ ├── Persistent Key │ ├── PersistentDebugKey.swift │ ├── PersistentKey.swift │ └── PersistentKeyProtocol.swift │ └── Property Wrapper │ ├── DefaultPersistentKeyValueStoreViewModifier.swift │ ├── PersistentKeyUIObservableObject.swift │ ├── PersistentValue+EnvironmentValues.swift │ └── PersistentValue.swift └── Tests └── PersistentKeyValueKitTests ├── Scaffolding ├── Custom Persistent Key-Value Representations │ └── ReferenceProxyPersistentKeyValueRepresentation.swift ├── Custom Persistible Types │ ├── CustomPersistibleType+Codable.swift │ ├── CustomPersistibleType+Comprehensive.swift │ ├── CustomPersistibleType+LosslessStringConvertible.swift │ ├── CustomPersistibleType+Proxy.swift │ ├── CustomPersistibleType+RawRepresentable.swift │ └── CustomPersistibleType.swift └── Mocks │ └── MockNSUbiquitousKeyValueStore.swift └── Tests ├── Key-Value Persistible ├── AbstractKeyValuePersistibleTests.swift ├── AbstractPrimitiveKeyValuePersistibleTests.swift └── Extensions │ ├── Foundation │ ├── DataKeyValuePersistibleTests.swift │ ├── DateKeyValuePersistibleTests.swift │ └── URLKeyValuePersistibleTests.swift │ └── Standard Library │ ├── ArrayOfPrimitivesKeyValuePersistibleTests.swift │ ├── ArrayOfProxiesKeyValuePersistibleTests.swift │ ├── BoolKeyValuePersistibleTests.swift │ ├── DictionaryOfPrimitivesKeyValuePersistibleTests.swift │ ├── DictionaryOfProxiesKeyValuePersistibleTests.swift │ ├── DoubleKeyValuePersistibleTests.swift │ ├── FloatKeyValuePersistibleTests.swift │ ├── IntKeyValuePersistibleTests.swift │ ├── OptionalKeyValuePersistibleTests.swift │ └── StringKeyValuePersistibleTests.swift ├── Persistent Key-Value Representation └── Implementations │ ├── LosslessStringConvertiblePersistentKeyValueRepresentationTests.swift │ └── ProxyPersistentKeyValueRepresentationTests.swift ├── Persistent Key-Value Store ├── AbstractPersistentKeyValueStoreTests.swift ├── AbstractPersistentKeyValueStoreTypeTests.swift ├── InMemoryPersistentKeyValueStore │ └── InMemoryPersistentKeyValueStoreTests.swift ├── NSUbiquitousKeyValueStore │ ├── AbstractNSUbiquitousKeyValueStoreTypeTests.swift │ ├── NSUbiquitousKeyValueStoreTests.swift │ └── Type Tests │ │ ├── Custom │ │ ├── CodableNSUbiquitousKeyValueStoreTests.swift │ │ ├── LosslessStringConvertibleNSUbiquitousKeyValueStoreTests.swift │ │ ├── ProxyNSUbiquitousKeyValueStoreTests.swift │ │ └── RawRepresentableNSUbiquitousKeyValueStoreTests.swift │ │ ├── Foundation │ │ ├── DataNSUbiquitousKeyValueStoreTests.swift │ │ └── URLNSUbiquitousKeyValueStoreTests.swift │ │ └── Standard Library │ │ ├── ArrayNSUbiquitousKeyValueStoreTests.swift │ │ ├── BoolNSUbiquitousKeyValueStoreTests.swift │ │ ├── DictionaryNSUbiquitousKeyValueStoreTests.swift │ │ ├── DoubleNSUbiquitousKeyValueStoreTests.swift │ │ ├── FloatNSUbiquitousKeyValueStoreTests.swift │ │ ├── IntNSUbiquitousKeyValueStoreTests.swift │ │ ├── OptionalNSUbiquitousKeyValueStoreTests.swift │ │ └── StringNSUbiquitousKeyValueStoreTests.swift └── UserDefaults │ ├── AbstractUserDefaultsTypeTests.swift │ ├── Type Tests │ ├── Custom │ │ ├── CodableUserDefaultsTests.swift │ │ ├── LosslessStringConvertibleUserDefaultsTests.swift │ │ ├── ProxyUserDefaultsTests.swift │ │ └── RawRepresentableUserDefaultsTests.swift │ ├── Foundation │ │ ├── DataUserDefaultsTests.swift │ │ └── URLUserDefaultsTests.swift │ └── Standard Library │ │ ├── ArrayUserDefaultsTests.swift │ │ ├── BoolUserDefaultsTests.swift │ │ ├── DictionaryUserDefaultsTests.swift │ │ ├── DoubleUserDefaultsTests.swift │ │ ├── FloatUserDefaultsTests.swift │ │ ├── IntUserDefaultsTests.swift │ │ ├── OptionalUserDefaultsTests.swift │ │ └── StringUserDefaultsTests.swift │ └── UserDefaultsTests.swift ├── Persistent Key ├── AbstractPersistentKeyTests.swift ├── Implementations │ ├── PersistentDebugKeyTests.swift │ └── PersistentKeyTests.swift ├── PersistentKeyObserverTests.swift └── PersistentKeyProtocolTests.swift └── Property Wrapper ├── DefaultPersistentKeyValueStoreViewModifierTests.swift └── PersistentValueTests.swift /.github/workflows/deploy_documentation.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Documentation 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | workflow_dispatch: 8 | 9 | # Set permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages. 10 | permissions: 11 | contents: read 12 | id-token: write 13 | pages: write 14 | 15 | # Allow one concurrent deployment. Do not cancel in-flight deployments because we don't want assets to be in a 16 | # a semi-deployed state. 17 | concurrency: 18 | group: "deploy-documentation" 19 | cancel-in-progress: false 20 | 21 | jobs: 22 | deploy-documentation: 23 | environment: 24 | name: github-pages 25 | url: ${{ steps.deployment.outputs.page_url }} 26 | runs-on: macos-15 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v3 30 | - name: Set Up GitHub Pages 31 | uses: actions/configure-pages@v3 32 | - name: Build Documentation 33 | run: | 34 | xcodebuild docbuild \ 35 | -scheme PersistentKeyValueKit \ 36 | -derivedDataPath /tmp/DerivedData \ 37 | -destination 'generic/platform=iOS'; 38 | mkdir _site; 39 | $(xcrun --find docc) process-archive \ 40 | transform-for-static-hosting /tmp/DerivedData/Build/Products/Debug-iphoneos/PersistentKeyValueKit.doccarchive \ 41 | --hosting-base-path PersistentKeyValueKit \ 42 | --output-path _site; 43 | - name: Create index.html 44 | run: | 45 | echo "" > _site/index.html; 46 | - name: Upload Documentation Artifact to GitHub Pages 47 | uses: actions/upload-pages-artifact@v1 48 | - name: Deploy to GitHub Pages 49 | id: deployment 50 | uses: actions/deploy-pages@v2 -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: ${{ github.ref_name }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | get-environment-details: 16 | strategy: 17 | matrix: 18 | include: 19 | - os: macos-15 20 | xcode: '16.0' 21 | name: Get Environment Details (Xcode ${{ matrix.xcode }}) 22 | runs-on: ${{ matrix.os }} 23 | steps: 24 | - name: Select Xcode 25 | run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app 26 | 27 | - name: Print OS SDKs 28 | run: xcodebuild -version -sdk 29 | 30 | - name: Print simulators 31 | run: | 32 | xcrun simctl delete unavailable 33 | xcrun simctl list 34 | 35 | test: 36 | needs: get-environment-details 37 | strategy: 38 | matrix: 39 | include: 40 | - os: macos-15 41 | xcode: '16.0' 42 | platform: iOS 43 | destination: "name=iPhone 16 Pro" 44 | sdk: iphonesimulator 45 | - os: macos-15 46 | xcode: '16.0' 47 | platform: tvOS 48 | destination: "name=Apple TV 4K (3rd generation)" 49 | sdk: appletvsimulator 50 | - os: macos-15 51 | xcode: '16.0' 52 | platform: visionOS 53 | destination: "name=Apple Vision Pro" 54 | sdk: xrsimulator 55 | - os: macos-15 56 | xcode: '16.0' 57 | platform: watchOS 58 | destination: "name=Apple Watch Ultra 2 (49mm)" 59 | sdk: watchsimulator 60 | - os: macos-15 61 | xcode: '16.0' 62 | platform: macOS 63 | name: Test ${{ matrix.platform }} (Xcode ${{ matrix.xcode }}) 64 | runs-on: ${{ matrix.os }} 65 | steps: 66 | - name: Checkout project 67 | uses: actions/checkout@master 68 | 69 | - name: Select Xcode 70 | run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app 71 | 72 | - name: Run tests (Xcode) 73 | if: matrix.platform != 'macOS' 74 | run: | 75 | set -o pipefail 76 | xcodebuild clean test -scheme PersistentKeyValueKit -sdk ${{ matrix.sdk }} -destination "${{ matrix.destination }}" -configuration Debug -enableCodeCoverage YES | xcpretty -c 77 | 78 | - name: Run tests (Swift) 79 | if: matrix.platform == 'macOS' 80 | run: swift test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/ 7 | .netrc 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Kyle Hughes 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "PersistentKeyValueKit", 7 | platforms: [ 8 | .iOS(.v15), 9 | .macOS(.v12), 10 | .tvOS(.v15), 11 | .visionOS(.v1), 12 | .watchOS(.v8), 13 | ], 14 | products: [ 15 | .library( 16 | name: "PersistentKeyValueKit", 17 | targets: [ 18 | "PersistentKeyValueKit", 19 | ] 20 | ), 21 | ], 22 | targets: [ 23 | .target( 24 | name: "PersistentKeyValueKit" 25 | ), 26 | .testTarget( 27 | name: "PersistentKeyValueKitTests", 28 | dependencies: [ 29 | "PersistentKeyValueKit", 30 | ] 31 | ), 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Key-Value Persistible/Extension/Foundation/Data+KeyValuePersistible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+KeyValuePersistible.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/13/24. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - KeyValuePersistible Extension 11 | 12 | extension Data { 13 | // MARK: Public Static Interface 14 | 15 | @inlinable 16 | public static var persistentKeyValueRepresentation: some PersistentKeyValueRepresentation { 17 | PrimitivePersistentKeyValueRepresentation() 18 | } 19 | } 20 | 21 | // MARK: - PrimitiveKeyValuePersistible Extension 22 | 23 | extension Data: PrimitiveKeyValuePersistible { 24 | // MARK: Interfacing with User Defaults 25 | 26 | @inlinable 27 | public static func get(_ userDefaultsKey: String, from userDefaults: UserDefaults) -> Self? { 28 | userDefaults.data(forKey: userDefaultsKey) 29 | } 30 | 31 | @inlinable 32 | public static func set(_ userDefaultsKey: String, to value: Self, in userDefaults: UserDefaults) { 33 | userDefaults.set(value, forKey: userDefaultsKey) 34 | } 35 | 36 | // MARK: Interfacing with Ubiquitous Key-Value Store 37 | 38 | @available(watchOS 9.0, *) 39 | @inlinable 40 | public static func get( 41 | _ ubiquitousStoreKey: String, 42 | from ubiquitousStore: NSUbiquitousKeyValueStore 43 | ) -> Self? { 44 | ubiquitousStore.data(forKey: ubiquitousStoreKey) 45 | } 46 | 47 | @available(watchOS 9.0, *) 48 | @inlinable 49 | public static func set( 50 | _ ubiquitousStoreKey: String, 51 | to value: Self, 52 | in ubiquitousStore: NSUbiquitousKeyValueStore 53 | ) { 54 | ubiquitousStore.set(value, forKey: ubiquitousStoreKey) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Key-Value Persistible/Extension/Foundation/URL+KeyValuePersistible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+KeyValuePersistible.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/13/24. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - KeyValuePersistible Extension 11 | 12 | extension URL { 13 | // MARK: Public Static Interface 14 | 15 | @inlinable 16 | public static var persistentKeyValueRepresentation: some PersistentKeyValueRepresentation { 17 | PrimitivePersistentKeyValueRepresentation() 18 | } 19 | } 20 | 21 | // MARK: - PrimitiveKeyValuePersistible Extension 22 | 23 | extension URL: PrimitiveKeyValuePersistible { 24 | // MARK: Interfacing with Property List Array 25 | 26 | public static func get(from propertyListArray: [Any]) -> [Self]? { 27 | var originalArray: [Self] = [] 28 | 29 | originalArray.reserveCapacity(propertyListArray.capacity) 30 | 31 | for value in propertyListArray { 32 | guard let storedValue = value as? String, let value = URL(string: storedValue) else { 33 | return nil 34 | } 35 | 36 | originalArray.append(value) 37 | } 38 | 39 | return originalArray 40 | } 41 | 42 | @inlinable 43 | public static func set(_ values: [Self], to propertyListArray: inout [Any]) { 44 | for value in values { 45 | propertyListArray.append(value.absoluteString) 46 | } 47 | } 48 | 49 | // MARK: Interfacing with Property List Dictionary 50 | 51 | @inlinable 52 | public static func get(_ dictionaryKey: String, from propertyListDictionary: [String: Any]) -> Self? { 53 | guard 54 | let storedValue = propertyListDictionary[dictionaryKey] as? String, 55 | let value = URL(string: storedValue) 56 | else { 57 | return nil 58 | } 59 | 60 | return value 61 | } 62 | 63 | @inlinable 64 | public static func set(_ dictionaryKey: String, to value: Self, in propertyListDictionary: inout [String: Any]) { 65 | propertyListDictionary[dictionaryKey] = value.absoluteString 66 | } 67 | 68 | // MARK: Interfacing with User Defaults 69 | 70 | @inlinable 71 | public static func get(_ userDefaultsKey: String, from userDefaults: UserDefaults) -> Self? { 72 | userDefaults.url(forKey: userDefaultsKey) 73 | } 74 | 75 | @inlinable 76 | public static func set(_ userDefaultsKey: String, to value: Self, in userDefaults: UserDefaults) { 77 | userDefaults.set(value, forKey: userDefaultsKey) 78 | } 79 | 80 | // MARK: Interfacing with Ubiquitous Key-Value Store 81 | 82 | @available(watchOS 9.0, *) 83 | @inlinable 84 | public static func get( 85 | _ ubiquitousStoreKey: String, 86 | from ubiquitousStore: NSUbiquitousKeyValueStore 87 | ) -> Self? { 88 | ubiquitousStore.object(forKey: ubiquitousStoreKey) as? Self 89 | } 90 | 91 | @available(watchOS 9.0, *) 92 | @inlinable 93 | public static func set( 94 | _ ubiquitousStoreKey: String, 95 | to value: Self, 96 | in ubiquitousStore: NSUbiquitousKeyValueStore 97 | ) { 98 | ubiquitousStore.set(value, forKey: ubiquitousStoreKey) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Key-Value Persistible/Extension/Standard Library/Array+KeyValuePersistible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+KeyValuePersistible.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/13/24. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - KeyValuePersistible Extension 11 | 12 | extension Array: KeyValuePersistible where Element: KeyValuePersistible { 13 | // MARK: Public Static Interface 14 | 15 | @inlinable 16 | public static var persistentKeyValueRepresentation: some PersistentKeyValueRepresentation { 17 | PrimitivePersistentKeyValueRepresentation() 18 | } 19 | } 20 | 21 | // MARK: - PrimitiveKeyValuePersistible Extension 22 | 23 | extension Array: PrimitiveKeyValuePersistible where Element: KeyValuePersistible { 24 | // MARK: Interfacing with Property List Array 25 | 26 | public static func get(from propertyListArray: [Any]) -> [Self]? { 27 | let representation = Element.persistentKeyValueRepresentation 28 | 29 | var originalArray: [Self] = [] 30 | 31 | originalArray.reserveCapacity(originalArray.capacity) 32 | 33 | for primitiveElement in propertyListArray { 34 | guard 35 | let primitiveChildArray = primitiveElement as? [Any], 36 | let childArray = representation.get(from: primitiveChildArray) 37 | else { 38 | return nil 39 | } 40 | 41 | originalArray.append(childArray) 42 | } 43 | 44 | return originalArray 45 | } 46 | 47 | public static func set(_ values: [Self], to propertyListArray: inout [Any]) { 48 | let representation = Element.persistentKeyValueRepresentation 49 | 50 | for value in values { 51 | var childPropertyListArray: [Any] = [] 52 | 53 | childPropertyListArray.reserveCapacity(value.capacity) 54 | 55 | representation.set(value, to: &childPropertyListArray) 56 | 57 | propertyListArray.append(childPropertyListArray) 58 | } 59 | } 60 | 61 | // MARK: Interfacing with Property List Dictionary 62 | 63 | @inlinable 64 | public static func get(_ dictionaryKey: String, from propertyListDictionary: [String: Any]) -> Self? { 65 | guard let propertyListArray = propertyListDictionary[dictionaryKey] as? [Any] else { 66 | return nil 67 | } 68 | 69 | return Element.persistentKeyValueRepresentation.get(from: propertyListArray) 70 | } 71 | 72 | @inlinable 73 | public static func set(_ dictionaryKey: String, to value: Self, in propertyListDictionary: inout [String: Any]) { 74 | var propertyListArray: [Any] = [] 75 | 76 | propertyListArray.reserveCapacity(value.capacity) 77 | 78 | Element.persistentKeyValueRepresentation.set(value, to: &propertyListArray) 79 | 80 | propertyListDictionary[dictionaryKey] = propertyListArray 81 | } 82 | 83 | // MARK: Interfacing with User Defaults 84 | 85 | @inlinable 86 | public static func get(_ userDefaultsKey: String, from userDefaults: UserDefaults) -> Self? { 87 | guard let propertyListArray = userDefaults.array(forKey: userDefaultsKey) else { 88 | return nil 89 | } 90 | 91 | return Element.persistentKeyValueRepresentation.get(from: propertyListArray) 92 | } 93 | 94 | @inlinable 95 | public static func set(_ userDefaultsKey: String, to value: Self, in userDefaults: UserDefaults) { 96 | var propertyListArray: [Any] = [] 97 | 98 | propertyListArray.reserveCapacity(value.capacity) 99 | 100 | Element.persistentKeyValueRepresentation.set(value, to: &propertyListArray) 101 | 102 | userDefaults.set(propertyListArray, forKey: userDefaultsKey) 103 | } 104 | 105 | // MARK: Interfacing with Ubiquitous Key-Value Store 106 | 107 | @available(watchOS 9.0, *) 108 | @inlinable 109 | public static func get( 110 | _ ubiquitousStoreKey: String, 111 | from ubiquitousStore: NSUbiquitousKeyValueStore 112 | ) -> Self? { 113 | guard let propertyListArray = ubiquitousStore.array(forKey: ubiquitousStoreKey) else { 114 | return nil 115 | } 116 | 117 | return Element.persistentKeyValueRepresentation.get(from: propertyListArray) 118 | } 119 | 120 | @available(watchOS 9.0, *) 121 | @inlinable 122 | public static func set( 123 | _ ubiquitousStoreKey: String, 124 | to value: Self, 125 | in ubiquitousStore: NSUbiquitousKeyValueStore 126 | ) { 127 | var propertyListArray: [Any] = [] 128 | 129 | propertyListArray.reserveCapacity(value.capacity) 130 | 131 | Element.persistentKeyValueRepresentation.set(value, to: &propertyListArray) 132 | 133 | ubiquitousStore.set(propertyListArray, forKey: ubiquitousStoreKey) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Key-Value Persistible/Extension/Standard Library/Bool+KeyValuePersistible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bool+KeyValuePersistible.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/13/24. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - KeyValuePersistible Extension 11 | 12 | extension Bool { 13 | // MARK: Public Static Interface 14 | 15 | @inlinable 16 | public static var persistentKeyValueRepresentation: some PersistentKeyValueRepresentation { 17 | PrimitivePersistentKeyValueRepresentation() 18 | } 19 | } 20 | 21 | // MARK: - PrimitiveKeyValuePersistible Extension 22 | 23 | extension Bool: PrimitiveKeyValuePersistible { 24 | // MARK: Interfacing with User Defaults 25 | 26 | @inlinable 27 | public static func get(_ userDefaultsKey: String, from userDefaults: UserDefaults) -> Self? { 28 | // We use the default implementation with `object(forKey)` so that we can differentiate a `nil` value from 29 | // a `false` value. 30 | userDefaults.object(forKey: userDefaultsKey) as? Self 31 | } 32 | 33 | @inlinable 34 | public static func set(_ userDefaultsKey: String, to value: Self, in userDefaults: UserDefaults) { 35 | userDefaults.set(value, forKey: userDefaultsKey) 36 | } 37 | 38 | // MARK: Interfacing with Ubiquitous Key-Value Store 39 | 40 | @available(watchOS 9.0, *) 41 | @inlinable 42 | public static func get( 43 | _ ubiquitousStoreKey: String, 44 | from ubiquitousStore: NSUbiquitousKeyValueStore 45 | ) -> Self? { 46 | // We use the default implementation with `object(forKey)` so that we can differentiate a `nil` value from 47 | // a `false` value. 48 | ubiquitousStore.object(forKey: ubiquitousStoreKey) as? Self 49 | } 50 | 51 | @available(watchOS 9.0, *) 52 | @inlinable 53 | public static func set( 54 | _ ubiquitousStoreKey: String, 55 | to value: Self, 56 | in ubiquitousStore: NSUbiquitousKeyValueStore 57 | ) { 58 | ubiquitousStore.set(value, forKey: ubiquitousStoreKey) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Key-Value Persistible/Extension/Standard Library/Dictionary+KeyValuePersistible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dictionary+KeyValuePersistible.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 10/7/24. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - KeyValuePersistible Extension 11 | 12 | extension Dictionary: KeyValuePersistible where Key == String, Value: KeyValuePersistible { 13 | // MARK: Public Static Interface 14 | 15 | @inlinable 16 | public static var persistentKeyValueRepresentation: some PersistentKeyValueRepresentation { 17 | PrimitivePersistentKeyValueRepresentation() 18 | } 19 | } 20 | 21 | // MARK: - PrimitiveKeyValuePersistible Extension 22 | 23 | extension Dictionary: PrimitiveKeyValuePersistible 24 | where 25 | Key == String, 26 | Value: KeyValuePersistible 27 | { 28 | // MARK: Interfacing with Property List Array 29 | 30 | public static func get(from propertyListArray: [Any]) -> [Self]? { 31 | let representation = Value.persistentKeyValueRepresentation 32 | 33 | var originalArray: [Self] = [] 34 | 35 | originalArray.reserveCapacity(propertyListArray.capacity) 36 | 37 | for propertyListElement in propertyListArray { 38 | guard let propertyListDictionary = propertyListElement as? [String: Any] else { 39 | return nil 40 | } 41 | 42 | originalArray.append(transformToOriginalDictionary(propertyListDictionary, using: representation)) 43 | } 44 | 45 | return originalArray 46 | } 47 | 48 | @inlinable 49 | public static func set(_ values: [Self], to propertyListArray: inout [Any]) { 50 | let representation = Value.persistentKeyValueRepresentation 51 | 52 | for value in values { 53 | propertyListArray.append(transformToPropertyListDictionary(value, using: representation)) 54 | } 55 | } 56 | 57 | // MARK: Interfacing with Property List Dictionary 58 | 59 | @inlinable 60 | public static func get(_ dictionaryKey: String, from propertyListDictionary: [String: Any]) -> Self? { 61 | guard let propertyListDictionary = propertyListDictionary[dictionaryKey] as? [String: Any] else { 62 | return nil 63 | } 64 | 65 | return transformToOriginalDictionary(propertyListDictionary) 66 | } 67 | 68 | @inlinable 69 | public static func set(_ dictionaryKey: String, to value: Self, in propertyListDictionary: inout [String: Any]) { 70 | propertyListDictionary[dictionaryKey] = transformToPropertyListDictionary(value) 71 | } 72 | 73 | // MARK: Interfacing with User Defaults 74 | 75 | @inlinable 76 | public static func get(_ userDefaultsKey: String, from userDefaults: UserDefaults) -> Self? { 77 | guard let propertyListDictionary = userDefaults.dictionary(forKey: userDefaultsKey) else { 78 | return nil 79 | } 80 | 81 | return transformToOriginalDictionary(propertyListDictionary) 82 | } 83 | 84 | @inlinable 85 | public static func set(_ userDefaultsKey: String, to value: Self, in userDefaults: UserDefaults) { 86 | userDefaults.set(transformToPropertyListDictionary(value), forKey: userDefaultsKey) 87 | } 88 | 89 | // MARK: Interfacing with Ubiquitous Key-Value Store 90 | 91 | @available(watchOS 9.0, *) 92 | @inlinable 93 | public static func get(_ ubiquitousStoreKey: String, from ubiquitousStore: NSUbiquitousKeyValueStore) -> Self? { 94 | guard let propertyListDictionary = ubiquitousStore.dictionary(forKey: ubiquitousStoreKey) else { 95 | return nil 96 | } 97 | 98 | return transformToOriginalDictionary(propertyListDictionary) 99 | } 100 | 101 | @available(watchOS 9.0, *) 102 | @inlinable 103 | public static func set( 104 | _ ubiquitousStoreKey: String, 105 | to value: Self, 106 | in ubiquitousStore: NSUbiquitousKeyValueStore 107 | ) { 108 | ubiquitousStore.set(transformToPropertyListDictionary(value), forKey: ubiquitousStoreKey) 109 | } 110 | 111 | // MARK: Internal Static Interface 112 | 113 | @usableFromInline 114 | internal static func transformToOriginalDictionary( 115 | _ propertyListDictionary: [String: Any], 116 | using representation: Value.PersistentKeyValueRepresentation = Value.persistentKeyValueRepresentation 117 | ) -> [String: Value] { 118 | var originalDictionary: [String: Value] = Dictionary(minimumCapacity: propertyListDictionary.capacity) 119 | 120 | for (key, _) in propertyListDictionary { 121 | originalDictionary[key] = representation.get(key, from: propertyListDictionary) 122 | } 123 | 124 | return originalDictionary 125 | } 126 | 127 | @usableFromInline 128 | internal static func transformToPropertyListDictionary( 129 | _ originalDictionary: Self, 130 | using representation: Value.PersistentKeyValueRepresentation = Value.persistentKeyValueRepresentation 131 | ) -> [String: Any] { 132 | var propertyListDictionary: [String: Any] = Dictionary(minimumCapacity: originalDictionary.capacity) 133 | 134 | for (key, value) in originalDictionary { 135 | representation.set(key, to: value, in: &propertyListDictionary) 136 | } 137 | 138 | return propertyListDictionary 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Key-Value Persistible/Extension/Standard Library/Double+KeyValuePersistible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Double+KeyValuePersistible.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/13/24. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - KeyValuePersistible Extension 11 | 12 | extension Double: KeyValuePersistible { 13 | // MARK: Public Static Interface 14 | 15 | @inlinable 16 | public static var persistentKeyValueRepresentation: some PersistentKeyValueRepresentation { 17 | PrimitivePersistentKeyValueRepresentation() 18 | } 19 | } 20 | 21 | // MARK: - PrimitiveKeyValuePersistible Extension 22 | 23 | extension Double: PrimitiveKeyValuePersistible { 24 | // MARK: Interfacing with User Defaults 25 | 26 | @inlinable 27 | public static func get(_ userDefaultsKey: String, from userDefaults: UserDefaults) -> Self? { 28 | // We use the default implementation with `object(forKey)` so that we can differentiate a `nil` value from 29 | // a 0 value. 30 | userDefaults.object(forKey: userDefaultsKey) as? Self 31 | } 32 | 33 | @inlinable 34 | public static func set(_ userDefaultsKey: String, to value: Self, in userDefaults: UserDefaults) { 35 | userDefaults.set(value, forKey: userDefaultsKey) 36 | } 37 | 38 | // MARK: Interfacing with Ubiquitous Key-Value Store 39 | 40 | @available(watchOS 9.0, *) 41 | @inlinable 42 | public static func get( 43 | _ ubiquitousStoreKey: String, 44 | from ubiquitousStore: NSUbiquitousKeyValueStore 45 | ) -> Self? { 46 | // We use the default implementation with `object(forKey)` so that we can differentiate a `nil` value from 47 | // a 0 value. 48 | ubiquitousStore.object(forKey: ubiquitousStoreKey) as? Self 49 | } 50 | 51 | @available(watchOS 9.0, *) 52 | @inlinable 53 | public static func set( 54 | _ ubiquitousStoreKey: String, 55 | to value: Self, 56 | in ubiquitousStore: NSUbiquitousKeyValueStore 57 | ) { 58 | ubiquitousStore.set(value, forKey: ubiquitousStoreKey) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Key-Value Persistible/Extension/Standard Library/Float+KeyValuePersistible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Float+KeyValuePersistible.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/13/24. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - KeyValuePersistible Extension 11 | 12 | extension Float: KeyValuePersistible { 13 | // MARK: Public Static Interface 14 | 15 | @inlinable 16 | public static var persistentKeyValueRepresentation: some PersistentKeyValueRepresentation { 17 | PrimitivePersistentKeyValueRepresentation() 18 | } 19 | } 20 | 21 | // MARK: - PrimitiveKeyValuePersistible Extension 22 | 23 | extension Float: PrimitiveKeyValuePersistible { 24 | // MARK: Interfacing with User Defaults 25 | 26 | @inlinable 27 | public static func get(_ userDefaultsKey: String, from userDefaults: UserDefaults) -> Self? { 28 | // We use the default implementation with `object(forKey)` so that we can differentiate a `nil` value from 29 | // a 0 value. 30 | userDefaults.object(forKey: userDefaultsKey) as? Self 31 | } 32 | 33 | @inlinable 34 | public static func set(_ userDefaultsKey: String, to value: Self, in userDefaults: UserDefaults) { 35 | userDefaults.set(value, forKey: userDefaultsKey) 36 | } 37 | 38 | // MARK: Interfacing with Ubiquitous Key-Value Store 39 | 40 | @available(watchOS 9.0, *) 41 | @inlinable 42 | public static func get( 43 | _ ubiquitousStoreKey: String, 44 | from ubiquitousStore: NSUbiquitousKeyValueStore 45 | ) -> Self? { 46 | // We use the default implementation with `object(forKey)` so that we can differentiate a `nil` value from 47 | // a 0 value. 48 | ubiquitousStore.object(forKey: ubiquitousStoreKey) as? Self 49 | } 50 | 51 | @available(watchOS 9.0, *) 52 | @inlinable 53 | public static func set( 54 | _ ubiquitousStoreKey: String, 55 | to value: Self, 56 | in ubiquitousStore: NSUbiquitousKeyValueStore 57 | ) { 58 | ubiquitousStore.set(value, forKey: ubiquitousStoreKey) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Key-Value Persistible/Extension/Standard Library/Int+KeyValuePersistible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Int+KeyValuePersistible.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/13/24. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - KeyValuePersistible Extension 11 | 12 | extension Int: KeyValuePersistible { 13 | // MARK: Public Static Interface 14 | 15 | @inlinable 16 | public static var persistentKeyValueRepresentation: some PersistentKeyValueRepresentation { 17 | PrimitivePersistentKeyValueRepresentation() 18 | } 19 | } 20 | 21 | // MARK: - PrimitiveKeyValuePersistible Extension 22 | 23 | extension Int: PrimitiveKeyValuePersistible { 24 | // MARK: Interfacing with User Defaults 25 | 26 | @inlinable 27 | public static func get(_ userDefaultsKey: String, from userDefaults: UserDefaults) -> Self? { 28 | // We use the default implementation with `object(forKey)` so that we can differentiate a `nil` value from 29 | // a 0 value. 30 | userDefaults.object(forKey: userDefaultsKey) as? Self 31 | } 32 | 33 | @inlinable 34 | public static func set(_ userDefaultsKey: String, to value: Self, in userDefaults: UserDefaults) { 35 | userDefaults.set(value, forKey: userDefaultsKey) 36 | } 37 | 38 | // MARK: Interfacing with Ubiquitous Key-Value Store 39 | 40 | @available(watchOS 9.0, *) 41 | @inlinable 42 | public static func get( 43 | _ ubiquitousStoreKey: String, 44 | from ubiquitousStore: NSUbiquitousKeyValueStore 45 | ) -> Self? { 46 | // We use the default implementation with `object(forKey)` so that we can differentiate a `nil` value from 47 | // a 0 value. 48 | ubiquitousStore.object(forKey: ubiquitousStoreKey) as? Self 49 | } 50 | 51 | @available(watchOS 9.0, *) 52 | @inlinable 53 | public static func set( 54 | _ ubiquitousStoreKey: String, 55 | to value: Self, 56 | in ubiquitousStore: NSUbiquitousKeyValueStore 57 | ) { 58 | ubiquitousStore.set(value, forKey: ubiquitousStoreKey) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Key-Value Persistible/Extension/Standard Library/Optional+KeyValuePersistible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Optional+KeyValuePersistible.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/13/24. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - KeyValuePersistible Extension 11 | 12 | extension Optional: KeyValuePersistible where Wrapped: KeyValuePersistible { 13 | // MARK: Public Static Interface 14 | 15 | @inlinable 16 | public static var persistentKeyValueRepresentation: some PersistentKeyValueRepresentation { 17 | Wrapped.persistentKeyValueRepresentation.optionalRepresentation 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Key-Value Persistible/Extension/Standard Library/String+KeyValuePersistible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+KeyValuePersistible.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/13/24. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - KeyValuePersistible Extension 11 | 12 | extension String: KeyValuePersistible { 13 | // MARK: Public Static Interface 14 | 15 | @inlinable 16 | public static var persistentKeyValueRepresentation: some PersistentKeyValueRepresentation { 17 | PrimitivePersistentKeyValueRepresentation() 18 | } 19 | } 20 | 21 | // MARK: - PrimitiveKeyValuePersistible Extension 22 | 23 | extension String: PrimitiveKeyValuePersistible { 24 | // MARK: Interfacing with User Defaults 25 | 26 | @inlinable 27 | public static func get(_ userDefaultsKey: String, from userDefaults: UserDefaults) -> Self? { 28 | userDefaults.string(forKey: userDefaultsKey) 29 | } 30 | 31 | @inlinable 32 | public static func set(_ userDefaultsKey: String, to value: Self, in userDefaults: UserDefaults) { 33 | userDefaults.set(value, forKey: userDefaultsKey) 34 | } 35 | 36 | // MARK: Interfacing with Ubiquitous Key-Value Store 37 | 38 | @available(watchOS 9.0, *) 39 | @inlinable 40 | public static func get( 41 | _ ubiquitousStoreKey: String, 42 | from ubiquitousStore: NSUbiquitousKeyValueStore 43 | ) -> Self? { 44 | ubiquitousStore.string(forKey: ubiquitousStoreKey) 45 | } 46 | 47 | @available(watchOS 9.0, *) 48 | @inlinable 49 | public static func set( 50 | _ ubiquitousStoreKey: String, 51 | to value: Self, 52 | in ubiquitousStore: NSUbiquitousKeyValueStore 53 | ) { 54 | ubiquitousStore.set(value, forKey: ubiquitousStoreKey) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Key-Value Persistible/KeyValuePersistible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyValuePersistible.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/13/24. 6 | // 7 | 8 | /// A type that can be a value in a ``PersistentKeyValueStore``. 9 | public protocol KeyValuePersistible { 10 | // MARK: Associated Types 11 | 12 | /// The type of the representation of the key-value pair in the ``PersistentKeyValueStore``. 13 | associatedtype PersistentKeyValueRepresentation: PersistentKeyValueKit.PersistentKeyValueRepresentation 14 | 15 | // MARK: Static Interface 16 | 17 | /// The representation of the key-value pair in the ``PersistentKeyValueStore``. 18 | static var persistentKeyValueRepresentation: Self.PersistentKeyValueRepresentation { get } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Key-Value Persistible/PrimitiveKeyValuePersistible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrimitiveKeyValuePersistible.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/13/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A protocol for a representation of a value in a ``PersistentKeyValueStore`` that is itself a persistible type and 11 | /// defines explicitly how to interface with `UserDefaults` and `NSUbiquitousKeyValueStore`. 12 | /// 13 | /// This protocol is intended for use with types that have explicit interfaces defined on `UserDefaults` and/or 14 | /// `NSUbiquitousKeyValueStore`. All of those types already implement this protocol. Other types should prefer 15 | /// proxy representations, like ``CodablePersistentKeyValueRepresentation`` or 16 | /// ``ProxyPersistentKeyValueRepresentation``, which simplify the requirements and make the best performance decisions. 17 | /// However, it is possible to use this type to create very specific or complex representations of a custom type within 18 | /// a store, such as utilizing `[String: Any]` storage. 19 | /// 20 | /// The built-in primitive types are: 21 | /// - `Array where Element: KeyValuePersistible` 22 | /// - `Bool` 23 | /// - `Data` 24 | /// - `Dictionary where Value: KeyValuePersistible` 25 | /// - `Double` 26 | /// - `Float` 27 | /// - `Int` 28 | /// - `Optional where Wrapped: KeyValuePersistible` 29 | /// - `String` 30 | /// - `URL` 31 | /// 32 | /// It is intended, but not required, that conforming types' representations are of type 33 | /// ``PrimitivePersistentKeyValueRepresentation``. 34 | public protocol PrimitiveKeyValuePersistible: KeyValuePersistible { 35 | // MARK: Interfacing with Property List Array 36 | 37 | /// Get the values from the given property-list-compatible array, if they exist and if they can all be represented 38 | /// as `Self`. 39 | /// 40 | /// - Parameter propertyListArray: The array to get the values from. 41 | /// - Returns: The values from the array, or `nil` if any value cannot be represented as `Self`. Note that this 42 | /// function will return `nil` if the array contains values of different types, as it enforces type homogeneity 43 | /// across all elements. 44 | /// - Important: This function will return `nil` if the array contains values of different types, as it enforces 45 | /// type homogeneity across all elements. 46 | static func get(from propertyListArray: [Any]) -> [Self]? 47 | 48 | /// Set the values in the given property-list-compatible array. 49 | /// 50 | /// - Parameter values: The values to set in the array. All values must be of the same type. 51 | /// - Parameter propertyListArray: The array to set the values in. 52 | static func set(_ values: [Self], to propertyListArray: inout [Any]) 53 | 54 | // MARK: Interfacing with Property List Dictionary 55 | 56 | /// Get the value at the given key from the given property-list-compatible dictionary, if it exists and if it can 57 | /// be represented as `Self`. 58 | /// 59 | /// - Parameter dictionaryKey: The key to get the value from. 60 | /// - Parameter propertyListDictionary: The dictionary to get the value from at `dictionaryKey`. 61 | /// - Returns: The value at `dictionaryKey` in `propertyListDictionary`, or `nil` if it does not exist or cannot be 62 | /// represented as `Self`. 63 | static func get(_ dictionaryKey: String, from propertyListDictionary: [String: Any]) -> Self? 64 | 65 | /// Set the value at the given key in the given property-list-compatible dictionary. 66 | /// 67 | /// - Parameter dictionaryKey: The key to set the value at. 68 | /// - Parameter propertyListDictionary: The dictionary to set the value in at `dictionaryKey`. 69 | static func set(_ dictionaryKey: String, to value: Self, in propertyListDictionary: inout [String: Any]) 70 | 71 | // MARK: Interfacing with User Defaults 72 | 73 | /// Get the value at the given key from the given `UserDefaults`, if it exists and if it can be represented as 74 | /// `Self`. 75 | /// 76 | /// - Parameter userDefaultsKey: The key to get the value from. 77 | /// - Parameter userDefaults: The `UserDefaults` to get the value from at `userDefaultsKey`. 78 | /// - Returns: The value at `userDefaultsKey` in `userDefaults`, or `nil` if it does not exist or cannot be 79 | /// represented as `Self`. 80 | static func get(_ userDefaultsKey: String, from userDefaults: UserDefaults) -> Self? 81 | 82 | /// Set the value at the given key in the given `UserDefaults`. 83 | /// 84 | /// - Parameter userDefaultsKey: The key to set the value at. 85 | /// - Parameter userDefaults: The `UserDefaults` to set the value in at `userDefaultsKey`. 86 | static func set(_ userDefaultsKey: String, to value: Self, in userDefaults: UserDefaults) 87 | 88 | // MARK: Interfacing with Ubiquitous Key-Value Store 89 | 90 | /// Get the value at the given key from the given `NSUbiquitousKeyValueStore`, if it exists and if it can be 91 | /// represented as `Self`. 92 | /// 93 | /// - Parameter ubiquitousStoreKey: The key to get the value from. 94 | /// - Parameter ubiquitousStore: The `NSUbiquitousKeyValueStore` to get the value from at 95 | /// `ubiquitousStoreKey`. 96 | /// - Returns: The value at `ubiquitousStoreKey` in `ubiquitousStore`, or `nil` if it does not exist or cannot be 97 | /// represented as `Self`. 98 | @available(watchOS 9.0, *) 99 | static func get(_ ubiquitousStoreKey: String, from ubiquitousStore: NSUbiquitousKeyValueStore) -> Self? 100 | 101 | /// Set the value at the given key in the given `NSUbiquitousKeyValueStore`. 102 | /// 103 | /// - Parameter ubiquitousStoreKey: The key to set the value at. 104 | /// - Parameter ubiquitousStore: The `NSUbiquitousKeyValueStore` to set the value in at 105 | /// `ubiquitousStoreKey`. 106 | @available(watchOS 9.0, *) 107 | static func set(_ ubiquitousStoreKey: String, to value: Self, in ubiquitousStore: NSUbiquitousKeyValueStore) 108 | } 109 | 110 | // MARK: - Default Implementation 111 | 112 | extension PrimitiveKeyValuePersistible { 113 | // MARK: Interfacing with Property List Array 114 | 115 | @inlinable 116 | public static func get(from propertyListArray: [Any]) -> [Self]? { 117 | propertyListArray as? [Self] 118 | } 119 | 120 | @inlinable 121 | public static func set(_ value: [Self], to propertyListArray: inout [Any]) { 122 | propertyListArray = value 123 | } 124 | 125 | // MARK: Interfacing with Property List Dictionary 126 | 127 | @inlinable 128 | public static func get(_ dictionaryKey: String, from propertyListDictionary: [String: Any]) -> Self? { 129 | propertyListDictionary[dictionaryKey] as? Self 130 | } 131 | 132 | @inlinable 133 | public static func set(_ dictionaryKey: String, to value: Self, in propertyListDictionary: inout [String: Any]) { 134 | propertyListDictionary[dictionaryKey] = value 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Persistent Key-Value Representation/Implementations/CodablePersistentKeyValueRepresentation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodablePersistentKeyValueRepresentation.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/15/24. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | /// A representation of a value in a ``PersistentKeyValueStore`` for types that participate in Swift's protocols for 12 | /// encoding and decoding. 13 | /// 14 | /// This is a proxy representation. The ``Value`` type will be persisted as the `Input`/`Output` of the given 15 | /// ``Encoder`` and ``Decoder``, which must itself be persistible. 16 | /// 17 | /// Errors are not reported. Failure to encode the value with the given ``Encoder`` will result in no value being 18 | /// stored. Failure to decode the value with the given ``Decoder`` will return `nil`. 19 | public struct CodablePersistentKeyValueRepresentation 20 | where 21 | Value: Decodable & Encodable, 22 | Encoder: TopLevelEncoder & Sendable, 23 | Decoder: TopLevelDecoder & Sendable, 24 | Encoder.Output: KeyValuePersistible, 25 | Encoder.Output == Decoder.Input 26 | { 27 | /// The decoder used to deserialize values from a ``PersistentKeyValueStore``. 28 | public let decoder: Decoder 29 | 30 | /// The encoder used to serialize values for a ``PersistentKeyValueStore``. 31 | public let encoder: Encoder 32 | 33 | // MARK: Public Initialization 34 | 35 | /// Creates a new representation with the given encoder and decoder. 36 | /// 37 | /// - Parameter encoder: The encoder used to serialize values for a ``PersistentKeyValueStore``. 38 | /// - Parameter decoder: The decoder used to deserialize values from a ``PersistentKeyValueStore``. 39 | @inlinable 40 | public init(encoder: Encoder, decoder: Decoder) { 41 | self.encoder = encoder 42 | self.decoder = decoder 43 | } 44 | } 45 | 46 | // MARK: - PersistentKeyValueRepresentation Extension 47 | 48 | extension CodablePersistentKeyValueRepresentation: ProxyPersistentKeyValueRepresentationProtocol { 49 | // MARK: Public Typealiases 50 | 51 | public typealias Proxy = Encoder.Output 52 | 53 | // MARK: Public Instance Interface 54 | 55 | @inlinable 56 | public func from(_ proxy: Proxy) -> Value? { 57 | try? decoder.decode(Value.self, from: proxy) 58 | } 59 | 60 | @inlinable 61 | public func to(_ value: Value) -> Proxy? { 62 | try? encoder.encode(value) 63 | } 64 | } 65 | 66 | // MARK: - Extension for Data Coders 67 | 68 | @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) 69 | extension CodablePersistentKeyValueRepresentation where Encoder == JSONEncoder, Decoder == JSONDecoder { 70 | // MARK: Public Initialization 71 | 72 | /// Creates a new representation with the default `JSONEncoder` and `JSONDecoder`. 73 | @inlinable 74 | public init() { 75 | self.init(encoder: JSONEncoder(), decoder: JSONDecoder()) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Persistent Key-Value Representation/Implementations/LosslessStringConvertiblePersistentKeyValueRepresentation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LosslessStringConvertiblePersistentKeyValueRepresentation.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 10/15/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A representation of a value in a ``PersistentKeyValueStore`` for types that can be converted to and from a `String` 11 | /// without losing information. 12 | /// 13 | /// This is a proxy representation. The ``Value`` type will be persisted as a `String`, which is itself persistible. 14 | public struct LosslessStringConvertiblePersistentKeyValueRepresentation 15 | where 16 | Value: LosslessStringConvertible 17 | { 18 | // MARK: Public Initialization 19 | 20 | /// Creates a new representation. 21 | @inlinable 22 | public init() {} 23 | } 24 | 25 | // MARK: - ProxyPersistentKeyValueRepresentationProtocol Extension 26 | 27 | extension LosslessStringConvertiblePersistentKeyValueRepresentation: ProxyPersistentKeyValueRepresentationProtocol { 28 | // MARK: Public Typealiases 29 | 30 | public typealias Proxy = String 31 | 32 | // MARK: Public Instance Interface 33 | 34 | @inlinable 35 | public func from(_ proxy: Proxy) -> Value? { 36 | Value(proxy) 37 | } 38 | 39 | @inlinable 40 | public func to(_ value: Value) -> Proxy? { 41 | String(value) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Persistent Key-Value Representation/Implementations/OptionalPersistentKeyValueRepresentation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionalPersistentKeyValueRepresentation.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 10/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A persistent key-value representation that can represent the `Optional` variant of another representation's 11 | /// ``Value``. All non-optional operations are deferred to the base representation. 12 | /// 13 | /// - Important: Consumers of this library should not have to interact with this type directly in happy-path scenarios. 14 | public struct OptionalPersistentKeyValueRepresentation 15 | where 16 | Base: PersistentKeyValueRepresentation 17 | { 18 | public let base: Base 19 | 20 | // MARK: Public Initialization 21 | 22 | /// Creates a new optional persistent key-value representation. 23 | /// 24 | /// - Parameter base: The base persistent key-value representation that is deferred to for non-optional operations. 25 | @inlinable 26 | public init(_ base: Base) { 27 | self.base = base 28 | } 29 | } 30 | 31 | // MARK: - PersistentKeyValueRepresentation Extension 32 | 33 | extension OptionalPersistentKeyValueRepresentation: PersistentKeyValueRepresentation { 34 | // MARK: Public Typealiases 35 | 36 | public typealias Value = Optional 37 | 38 | // MARK: Interfacing with Property List Array 39 | 40 | @inlinable 41 | public func get(from propertyListArray: [Any]) -> [Value]? { 42 | guard let originalArray = base.get(from: propertyListArray) else { 43 | return .none 44 | } 45 | 46 | return originalArray 47 | } 48 | 49 | public func set(_ values: [Value], to propertyListArray: inout [Any]) { 50 | var nonOptionalValues: [Base.Value] = [] 51 | 52 | nonOptionalValues.reserveCapacity(values.capacity) 53 | 54 | for value in values { 55 | guard let value else { 56 | continue 57 | } 58 | 59 | nonOptionalValues.append(value) 60 | } 61 | 62 | base.set(nonOptionalValues, to: &propertyListArray) 63 | } 64 | 65 | // MARK: Interfacing with Property List Dictionary 66 | 67 | @inlinable 68 | public func get(_ dictionaryKey: String, from propertyListDictionary: [String: Any]) -> Value? { 69 | guard let value = base.get(dictionaryKey, from: propertyListDictionary) else { 70 | return .none 71 | } 72 | 73 | return value 74 | } 75 | 76 | @inlinable 77 | public func set(_ dictionaryKey: String, to value: Value, in propertyListDictionary: inout [String: Any]) { 78 | guard let value else { 79 | propertyListDictionary.removeValue(forKey: dictionaryKey) 80 | return 81 | } 82 | 83 | base.set(dictionaryKey, to: value, in: &propertyListDictionary) 84 | } 85 | 86 | // MARK: Interfacing with User Defaults 87 | 88 | @inlinable 89 | public func get(_ userDefaultsKey: String, from userDefaults: UserDefaults) -> Value? { 90 | guard let value = base.get(userDefaultsKey, from: userDefaults) else { 91 | return .none 92 | } 93 | 94 | return value 95 | } 96 | 97 | @inlinable 98 | public func set(_ userDefaultsKey: String, to value: Value, in userDefaults: UserDefaults) { 99 | guard let value else { 100 | userDefaults.removeObject(forKey: userDefaultsKey) 101 | return 102 | } 103 | 104 | base.set(userDefaultsKey, to: value, in: userDefaults) 105 | } 106 | 107 | // MARK: Interfacing with Ubiquitous Key-Value Store 108 | 109 | @available(watchOS 9.0, *) 110 | @inlinable 111 | public func get( 112 | _ ubiquitousStoreKey: String, 113 | from ubiquitousStore: NSUbiquitousKeyValueStore 114 | ) -> Value? { 115 | guard let value = base.get(ubiquitousStoreKey, from: ubiquitousStore) else { 116 | return .none 117 | } 118 | 119 | return value 120 | } 121 | 122 | @available(watchOS 9.0, *) 123 | @inlinable 124 | public func set(_ ubiquitousStoreKey: String, to value: Value, in ubiquitousStore: NSUbiquitousKeyValueStore) { 125 | guard let value else { 126 | ubiquitousStore.removeObject(forKey: ubiquitousStoreKey) 127 | return 128 | } 129 | 130 | base.set(ubiquitousStoreKey, to: value, in: ubiquitousStore) 131 | } 132 | } 133 | 134 | // MARK: - Protocol Conveniences 135 | 136 | extension PersistentKeyValueRepresentation { 137 | // MARK: Public Static Interface 138 | 139 | /// Returns a wrapped representation of the receiver that can represent ``Value?``. 140 | @inlinable 141 | public var optionalRepresentation: OptionalPersistentKeyValueRepresentation { 142 | OptionalPersistentKeyValueRepresentation(self) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Persistent Key-Value Representation/Implementations/PrimitivePersistentKeyValueRepresentation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrimitivePersistentKeyValueRepresentation.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/13/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A representation of a value in a ``PersistentKeyValueStore`` that is itself a persistible type and defines 11 | /// explicitly how to interface with `UserDefaults` and `NSUbiquitousKeyValueStore`. 12 | /// 13 | /// ``Value`` is required to conform to ``PrimitiveKeyValuePersistible``. All applicable standard library & Foundation 14 | /// types already conform. It is unlikely that you need to use this representation, unless you have built an 15 | /// especially-custom representation for your custom type and made it conform to ``PrimitiveKeyValuePersistible``. 16 | /// 17 | /// - SeeAlso: ``PrimitiveKeyValuePersistible`` 18 | public struct PrimitivePersistentKeyValueRepresentation 19 | where 20 | Value: PrimitiveKeyValuePersistible 21 | { 22 | // MARK: Public Initialization 23 | 24 | /// Creates a new representation. 25 | @inlinable 26 | public init() {} 27 | } 28 | 29 | // MARK: - PersistentKeyValueRepresentation Extension 30 | 31 | extension PrimitivePersistentKeyValueRepresentation: PersistentKeyValueRepresentation { 32 | // MARK: Interfacing with Property List Array 33 | 34 | @inlinable 35 | public func get(from propertyListArray: [Any]) -> [Value]? { 36 | Value.get(from: propertyListArray) 37 | } 38 | 39 | @inlinable 40 | public func set(_ values: [Value], to propertyListArray: inout [Any]) { 41 | Value.set(values, to: &propertyListArray) 42 | } 43 | 44 | // MARK: Interfacing with Property List Dictionary 45 | 46 | @inlinable 47 | public func get(_ dictionaryKey: String, from propertyListDictionary: [String: Any]) -> Value? { 48 | Value.get(dictionaryKey, from: propertyListDictionary) 49 | } 50 | 51 | @inlinable 52 | public func set(_ dictionaryKey: String, to value: Value, in propertyListDictionary: inout [String: Any]) { 53 | Value.set(dictionaryKey, to: value, in: &propertyListDictionary) 54 | } 55 | 56 | // MARK: Interfacing with User Defaults 57 | 58 | @inlinable 59 | public func get(_ userDefaultsKey: String, from userDefaults: UserDefaults) -> Value? { 60 | Value.get(userDefaultsKey, from: userDefaults) 61 | } 62 | 63 | @inlinable 64 | public func set(_ userDefaultsKey: String, to value: Value, in userDefaults: UserDefaults) { 65 | Value.set(userDefaultsKey, to: value, in: userDefaults) 66 | } 67 | 68 | // MARK: Interfacing with Ubiquitous Key-Value Store 69 | 70 | @available(watchOS 9.0, *) 71 | @inlinable 72 | public func get(_ ubiquitousStoreKey: String, from ubiquitousStore: NSUbiquitousKeyValueStore) -> Value? { 73 | Value.get(ubiquitousStoreKey, from: ubiquitousStore) 74 | } 75 | 76 | @available(watchOS 9.0, *) 77 | @inlinable 78 | public func set(_ ubiquitousStoreKey: String, to value: Value, in ubiquitousStore: NSUbiquitousKeyValueStore) { 79 | Value.set(ubiquitousStoreKey, to: value, in: ubiquitousStore) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Persistent Key-Value Representation/Implementations/ProxyPersistentKeyValueRepresentation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProxyPersistentKeyValueRepresentation.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/20/24. 6 | // 7 | 8 | /// A representation of a value in a ``PersistentKeyValueStore`` that is proxied through another persistible type. 9 | /// 10 | /// This is a concrete implementation of ``ProxyPersistentKeyValueRepresentationProtocol``. 11 | public struct ProxyPersistentKeyValueRepresentation where Proxy: KeyValuePersistible { 12 | /// The closure used to deserialize a value from its proxy. 13 | public let from: @Sendable (Proxy) -> Value? 14 | 15 | /// The closure used to serialize a value into its proxy. 16 | public let to: @Sendable (Value) -> Proxy? 17 | 18 | // MARK: Public Initialization 19 | 20 | /// Creates a new representation that proxies a value through another explicit representation. 21 | /// 22 | /// This is useful for composing representations together. The value of ``Proxy`` is inferred from the 23 | /// proxied representation, not vice versa. 24 | /// 25 | /// - Parameter other: The other representation to proxy through. 26 | /// - Parameter to: The closure used to serialize a value into the other representation's value. 27 | /// - Parameter from: The closure used to deserialize a value from the other representation's value. 28 | public init( 29 | other: Other, 30 | to: @Sendable @escaping (Value) -> Other.Value, 31 | from: @Sendable @escaping (Other.Value) -> Value 32 | ) where Other: ProxyPersistentKeyValueRepresentationProtocol, Other.Proxy == Proxy { 33 | self.to = { @Sendable in 34 | other.to(to($0)) 35 | } 36 | 37 | self.from = { @Sendable in 38 | guard let otherDeserialization = other.from($0) else { 39 | return nil 40 | } 41 | 42 | return from(otherDeserialization) 43 | } 44 | } 45 | 46 | /// Creates a new representation that proxies a value though another persistible type. 47 | /// 48 | /// - Parameter to: The closure used to serialize a value into its proxy. 49 | /// - Parameter from: The closure used to deserialize a value from its proxy. 50 | @inlinable 51 | public init( 52 | to: @Sendable @escaping (Value) -> Proxy, 53 | from: @Sendable @escaping (Proxy) -> Value 54 | ) { 55 | self.to = to 56 | self.from = from 57 | } 58 | 59 | /// Creates a new representation that proxies a value though another persistible type. 60 | /// 61 | /// This specialized initializer is useful for callers using `KeyPath` shorthand with `Optional` values. 62 | /// 63 | /// - Parameter to: The closure used to serialize a value into its proxy. 64 | /// - Parameter from: The closure used to deserialize a value from its proxy. 65 | @inlinable 66 | public init( 67 | to: @Sendable @escaping (Value) -> Proxy?, 68 | from: @Sendable @escaping (Proxy) -> Value 69 | ) { 70 | self.to = to 71 | self.from = from 72 | } 73 | 74 | /// Creates a new representation that proxies a value though another persistible type. 75 | /// 76 | /// This specialized initializer is useful for callers using `KeyPath` shorthand with `Optional` values. 77 | /// 78 | /// - Parameter to: The closure used to serialize a value into its proxy. 79 | /// - Parameter from: The closure used to deserialize a value from its proxy. 80 | @inlinable 81 | public init( 82 | to: @Sendable @escaping (Value) -> Proxy, 83 | from: @Sendable @escaping (Proxy) -> Value? 84 | ) { 85 | self.to = to 86 | self.from = from 87 | } 88 | 89 | /// Creates a new representation that proxies a value though another persistible type. 90 | /// 91 | /// This specialized initializer is useful for callers using `KeyPath` shorthand with `Optional` values. 92 | /// 93 | /// - Parameter to: The closure used to serialize a value into its proxy. 94 | /// - Parameter from: The closure used to deserialize a value from its proxy. 95 | @inlinable 96 | public init( 97 | to: @Sendable @escaping (Value) -> Proxy?, 98 | from: @Sendable @escaping (Proxy) -> Value? 99 | ) { 100 | self.to = to 101 | self.from = from 102 | } 103 | } 104 | 105 | // MARK: - ProxyPersistentKeyValueRepresentationProtocol Extension 106 | 107 | extension ProxyPersistentKeyValueRepresentation: ProxyPersistentKeyValueRepresentationProtocol { 108 | // MARK: Public Instance Interface 109 | 110 | @inlinable 111 | public func from(_ proxy: Proxy) -> Value? { 112 | from(proxy) 113 | } 114 | 115 | @inlinable 116 | public func to(_ value: Value) -> Proxy? { 117 | to(value) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Persistent Key-Value Representation/Implementations/ProxyPersistentKeyValueRepresentationProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProxyPersistentKeyValueRepresentationProtocol.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/13/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A protocol for a representation of a value in a ``PersistentKeyValueStore`` that is proxied through another 11 | /// persistible type. 12 | /// 13 | /// This protocol is useful to build other representations that proxy a type in a specific way, like 14 | /// ``CodablePersistentKeyValueRepresentation`` which proxies a type through its `Codable` implementation, or 15 | /// ``RawRepresentablePersistentKeyValueRepresentation`` which proxies a type through its `RawRepresentable` 16 | /// implementation. 17 | /// 18 | /// To create a one-off representation to conform a type to ``KeyValuePersistible``, one should use 19 | /// ``ProxyPersistentKeyValueRepresentation``, which is a concrete implementation of this protocol. 20 | public protocol ProxyPersistentKeyValueRepresentationProtocol: PersistentKeyValueRepresentation { 21 | // MARK: Associated Types 22 | 23 | /// The persistible type used as proxy. 24 | associatedtype Proxy: KeyValuePersistible 25 | 26 | /// The type of value that is represented in a ``PersistentKeyValueStore``. 27 | associatedtype Value 28 | 29 | // MARK: Instance Interface 30 | 31 | /// Deserializes a value from its proxy. 32 | func from(_ proxy: Proxy) -> Value? 33 | 34 | /// Serializes a value into its proxy. 35 | func to(_ value: Value) -> Proxy? 36 | } 37 | 38 | // MARK: - PersistentKeyValueRepresentation Extension 39 | 40 | extension ProxyPersistentKeyValueRepresentationProtocol { 41 | // MARK: Interfacing with Property List Array 42 | 43 | public func get(from propertyListArray: [Any]) -> [Value]? { 44 | guard let proxyValues = Proxy.persistentKeyValueRepresentation.get(from: propertyListArray) else { 45 | return nil 46 | } 47 | 48 | var originalArray: [Value] = [] 49 | 50 | originalArray.reserveCapacity(proxyValues.capacity) 51 | 52 | for proxyValue in proxyValues { 53 | guard let originalValue = from(proxyValue) else { 54 | return nil 55 | } 56 | 57 | originalArray.append(originalValue) 58 | } 59 | 60 | return originalArray 61 | } 62 | 63 | public func set(_ values: [Value], to propertyListArray: inout [Any]) { 64 | var proxyValues: [Proxy] = [] 65 | 66 | proxyValues.reserveCapacity(values.capacity) 67 | 68 | for value in values { 69 | guard let serialization = to(value) else { 70 | return 71 | } 72 | 73 | proxyValues.append(serialization) 74 | } 75 | 76 | Proxy.persistentKeyValueRepresentation.set(proxyValues, to: &propertyListArray) 77 | } 78 | 79 | // MARK: Interfacing with Property List Dictionary 80 | 81 | @inlinable 82 | public func get(_ dictionaryKey: String, from propertyListDictionary: [String: Any]) -> Value? { 83 | guard 84 | let proxyValue = Proxy.persistentKeyValueRepresentation.get(dictionaryKey, from: propertyListDictionary) 85 | else { 86 | return nil 87 | } 88 | 89 | return from(proxyValue) 90 | } 91 | 92 | @inlinable 93 | public func set(_ dictionaryKey: String, to value: Value, in propertyListDictionary: inout [String: Any]) { 94 | guard let serialization = to(value) else { 95 | return 96 | } 97 | 98 | Proxy.persistentKeyValueRepresentation.set(dictionaryKey, to: serialization, in: &propertyListDictionary) 99 | } 100 | 101 | // MARK: Interface with User Defaults 102 | 103 | @inlinable 104 | public func get(_ userDefaultsKey: String, from userDefaults: UserDefaults) -> Value? { 105 | guard let proxyValue = Proxy.persistentKeyValueRepresentation.get(userDefaultsKey, from: userDefaults) else { 106 | return nil 107 | } 108 | 109 | return from(proxyValue) 110 | } 111 | 112 | @inlinable 113 | public func set(_ userDefaultsKey: String, to value: Value, in userDefaults: UserDefaults) { 114 | guard let serialization = to(value) else { 115 | return 116 | } 117 | 118 | Proxy.persistentKeyValueRepresentation.set(userDefaultsKey, to: serialization, in: userDefaults) 119 | } 120 | 121 | // MARK: Interface with Ubiquitous Key-Value Store 122 | 123 | @available(watchOS 9.0, *) 124 | @inlinable 125 | public func get(_ ubiquitousStoreKey: String, from ubiquitousStore: NSUbiquitousKeyValueStore) -> Value? { 126 | guard 127 | let proxyValue = Proxy.persistentKeyValueRepresentation.get(ubiquitousStoreKey, from: ubiquitousStore) 128 | else { 129 | return nil 130 | } 131 | 132 | return from(proxyValue) 133 | } 134 | 135 | @available(watchOS 9.0, *) 136 | @inlinable 137 | public func set(_ ubiquitousStoreKey: String, to value: Value, in ubiquitousStore: NSUbiquitousKeyValueStore) { 138 | guard let serialization = to(value) else { 139 | return 140 | } 141 | 142 | Proxy.persistentKeyValueRepresentation.set(ubiquitousStoreKey, to: serialization, in: ubiquitousStore) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Persistent Key-Value Representation/Implementations/RawRepresentablePersistentKeyValueRepresentation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RawRepresentablePersistentKeyValueRepresentation.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/15/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A representation of a value in a ``PersistentKeyValueStore`` for types that can be associated as raw values. 11 | /// 12 | /// This is a proxy representation. The ``Value`` type will be persisted as its `RawValue`, which must itself be 13 | /// persistible. 14 | public struct RawRepresentablePersistentKeyValueRepresentation 15 | where 16 | Value: RawRepresentable, 17 | Value.RawValue: KeyValuePersistible 18 | { 19 | // MARK: Public Initialization 20 | 21 | /// Creates a new representation. 22 | @inlinable 23 | public init() {} 24 | } 25 | 26 | // MARK: - ProxyPersistentKeyValueRepresentationProtocol Extension 27 | 28 | extension RawRepresentablePersistentKeyValueRepresentation: ProxyPersistentKeyValueRepresentationProtocol { 29 | // MARK: Public Typealiases 30 | 31 | public typealias Proxy = Value.RawValue 32 | 33 | // MARK: Public Instance Interface 34 | 35 | @inlinable 36 | public func from(_ proxy: Proxy) -> Value? { 37 | Value(rawValue: proxy) 38 | } 39 | 40 | @inlinable 41 | public func to(_ value: Value) -> Proxy? { 42 | value.rawValue 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Persistent Key-Value Representation/PersistentKeyValueRepresentation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersistentKeyValueRepresentation.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/13/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A representation of a value in a ``PersistentKeyValueStore``. 11 | public protocol PersistentKeyValueRepresentation: Sendable { 12 | // MARK: Associated Types 13 | 14 | /// The type of value that is represented in a ``PersistentKeyValueStore``. 15 | associatedtype Value 16 | 17 | // MARK: Interfacing with Property List Array 18 | 19 | /// Get the values from the given property-list-compatible array, if they exist and if they can all be represented 20 | /// as ``Value``. 21 | /// 22 | /// - Parameter propertyListArray: The array to get the values from. 23 | /// - Returns: The values from the array, or `nil` if any value cannot be represented as ``Value``. Note that this 24 | /// function will return `nil` if the array contains values of different types, as it enforces type homogeneity 25 | /// across all elements. 26 | /// - Important: This function will return `nil` if the array contains values of different types, as it enforces 27 | /// type homogeneity across all elements. 28 | func get(from propertyListArray: [Any]) -> [Value]? 29 | 30 | /// Set the values in the given property-list-compatible array. 31 | /// 32 | /// - Parameter values: The values to set in the array. All values must be of the same type. 33 | /// - Parameter propertyListArray: The array to set the values in. 34 | func set(_ values: [Value], to propertyListArray: inout [Any]) 35 | 36 | // MARK: Interfacing with Property List Dictionary 37 | 38 | /// Get the value at the given key from the given property-list-compatible dictionary, if it exists and if it can 39 | /// be represented as ``Value``. 40 | /// 41 | /// - Parameter dictionaryKey: The key to get the value from. 42 | /// - Parameter propertyListDictionary: The dictionary to get the value from at `dictionaryKey`. 43 | /// - Returns: The value at `dictionaryKey` in `dictionary`, or `nil` if it does not exist or cannot be represented 44 | /// as ``Value``. 45 | func get(_ dictionaryKey: String, from propertyListDictionary: [String: Any]) -> Value? 46 | 47 | /// Set the value at the given key in the given property-list-compatible dictionary. 48 | /// 49 | /// - Parameter dictionaryKey: The key to set the value at. 50 | /// - Parameter value: The value to set the key to. 51 | /// - Parameter propertyListDictionary: The dictionary to set the value in at `dictionaryKey`. 52 | func set(_ dictionaryKey: String, to value: Value, in propertyListDictionary: inout [String: Any]) 53 | 54 | // MARK: Interfacing with User Defaults 55 | 56 | /// Get the value at the given key from the given `UserDefaults`, if it exists and if it can be represented as 57 | /// ``Value``. 58 | /// 59 | /// - Parameter userDefaultsKey: The key to get the value from. 60 | /// - Parameter userDefaults: The `UserDefaults` to get the value from at `userDefaultsKey`. 61 | /// - Returns: The value at `userDefaultsKey` in `userDefaults`, or `nil` if it does not exist or cannot be 62 | /// represented as ``Value``. 63 | func get(_ userDefaultsKey: String, from userDefaults: UserDefaults) -> Value? 64 | 65 | /// Set the value at the given key in the given `UserDefaults`. 66 | /// 67 | /// - Parameter userDefaultsKey: The key to set the value at. 68 | /// - Parameter value: The value to set the key to. 69 | /// - Parameter userDefaults: The `UserDefaults` to set the value in at `userDefaultsKey`. 70 | func set(_ userDefaultsKey: String, to value: Value, in userDefaults: UserDefaults) 71 | 72 | // MARK: Interfacing with Ubiquitous Key-Value Store 73 | 74 | /// Get the value at the given key from the given `NSUbiquitousKeyValueStore`, if it exists and if it can be 75 | /// represented as ``Value``. 76 | /// 77 | /// - Parameter ubiquitousStoreKey: The key to get the value from. 78 | /// - Parameter ubiquitousStore: The `NSUbiquitousKeyValueStore` to get the value from at `ubiquitousStoreKey`. 79 | /// - Returns: The value at `ubiquitousStoreKey` in `ubiquitousStore`, or `nil` if it does not exist or cannot be 80 | /// represented as ``Value``. 81 | @available(watchOS 9.0, *) 82 | func get(_ ubiquitousStoreKey: String, from ubiquitousStore: NSUbiquitousKeyValueStore) -> Value? 83 | 84 | /// Set the value at the given key in the given `NSUbiquitousKeyValueStore`. 85 | /// 86 | /// - Parameter ubiquitousStoreKey: The key to set the value at. 87 | /// - Parameter value: The value to set the key to. 88 | /// - Parameter ubiquitousStore: The `NSUbiquitousKeyValueStore` to set the value in at `ubiquitousStoreKey`. 89 | @available(watchOS 9.0, *) 90 | func set(_ ubiquitousStoreKey: String, to value: Value, in ubiquitousStore: NSUbiquitousKeyValueStore) 91 | } 92 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Persistent Key-Value Store/Extensions/Foundation/NSUbiquitousKeyValueStore+PersistentKeyValueStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSUbiquitousKeyValueStore+PersistentKeyValueStore.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 6/11/22. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - PersistentKeyValueStore Extension 11 | 12 | @available(watchOS 9.0, *) 13 | extension NSUbiquitousKeyValueStore: PersistentKeyValueStore { 14 | /// The notification that is posted when the value of a key changes internally. 15 | public static let didChangeInternallyNotification = NSNotification.Name( 16 | "NSUbiquitousKeyValueStore.DidChangeInternally" 17 | ) 18 | 19 | // MARK: Getting Values 20 | 21 | @inlinable 22 | public func get(_ key: Key) -> Key.Value where Key: PersistentKeyProtocol { 23 | key.get(from: self) 24 | } 25 | 26 | // MARK: Setting Values 27 | 28 | @inlinable 29 | public func set(_ key: Key, to value: Key.Value) where Key: PersistentKeyProtocol { 30 | key.set(to: value, in: self) 31 | 32 | Self.postInternalChangeNotification(for: key, from: self) 33 | } 34 | 35 | // MARK: Removing Values 36 | 37 | @inlinable 38 | public func remove(_ key: Key) where Key: PersistentKeyProtocol { 39 | key.remove(from: self) 40 | 41 | Self.postInternalChangeNotification(for: key, from: self) 42 | } 43 | 44 | // MARK: Observing Keys 45 | 46 | @inlinable 47 | public func deregister( 48 | _ observer: NSObject, 49 | for key: Key, 50 | context: UnsafeMutableRawPointer? 51 | ) where Key : PersistentKeyProtocol { 52 | // We use selector-based notification APIs that do not need to perform manual observation deregistration. 53 | } 54 | 55 | @inlinable 56 | public func register( 57 | observer target: NSObject, 58 | for key: Key, 59 | with context: UnsafeMutableRawPointer?, 60 | and selector: Selector 61 | ) where Key: PersistentKeyProtocol { 62 | NotificationCenter.default.addObserver( 63 | target, 64 | selector: selector, 65 | name: NSUbiquitousKeyValueStore.didChangeExternallyNotification, 66 | object: self 67 | ) 68 | 69 | NotificationCenter.default.addObserver( 70 | target, 71 | selector: selector, 72 | name: NSUbiquitousKeyValueStore.didChangeInternallyNotification, 73 | object: self 74 | ) 75 | } 76 | 77 | // MARK: Internal Static Interface 78 | 79 | @usableFromInline 80 | internal static var changedKeysKey: AnyHashable { 81 | AnyHashable(NSUbiquitousKeyValueStoreChangedKeysKey) 82 | } 83 | 84 | @usableFromInline 85 | internal static func postInternalChangeNotification( 86 | for key: Key, 87 | from ubiquitousKeyValueStore: NSUbiquitousKeyValueStore 88 | ) where Key: PersistentKeyProtocol { 89 | postInternalChangeNotification(for: key.id, from: ubiquitousKeyValueStore) 90 | } 91 | 92 | @usableFromInline 93 | internal static func postInternalChangeNotification( 94 | for keyID: String, 95 | from ubiquitousKeyValueStore: NSUbiquitousKeyValueStore 96 | ) { 97 | NotificationCenter.default.post( 98 | name: didChangeInternallyNotification, 99 | object: ubiquitousKeyValueStore, 100 | userInfo: [ 101 | changedKeysKey: [ 102 | keyID, 103 | ], 104 | ] 105 | ) 106 | } 107 | } 108 | 109 | // MARK: - Static Accessors 110 | 111 | @available(watchOS 9.0, *) 112 | extension PersistentKeyValueStore where Self == NSUbiquitousKeyValueStore { 113 | // MARK: Public Static Interface 114 | 115 | /// A convenient way to access `NSUbiquitousKeyValueStore.default`. 116 | @inlinable 117 | public static var ubiquitous: Self { 118 | .default 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Persistent Key-Value Store/Extensions/Foundation/UserDefaults+PersistentKeyValueStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults+PersistentKeyValueStore.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 3/21/21. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - PersistentKeyValueStore Extension 11 | 12 | extension UserDefaults: PersistentKeyValueStore { 13 | // MARK: Getting Values 14 | 15 | @inlinable 16 | public func get(_ key: Key) -> Key.Value where Key: PersistentKeyProtocol { 17 | key.get(from: self) 18 | } 19 | 20 | // MARK: Setting Values 21 | 22 | @inlinable 23 | public func set(_ key: Key, to value: Key.Value) where Key: PersistentKeyProtocol { 24 | key.set(to: value, in: self) 25 | } 26 | 27 | // MARK: Removing Values 28 | 29 | @inlinable 30 | public func remove(_ key: Key) where Key: PersistentKeyProtocol { 31 | key.remove(from: self) 32 | } 33 | 34 | // MARK: Observing Keys 35 | 36 | @inlinable 37 | public func deregister( 38 | _ observer: NSObject, 39 | for key: Key, 40 | context: UnsafeMutableRawPointer? 41 | ) where Key : PersistentKeyProtocol { 42 | removeObserver(observer, forKeyPath: key.id, context: context) 43 | } 44 | 45 | @inlinable 46 | public func register( 47 | observer target: NSObject, 48 | for key: Key, 49 | with context: UnsafeMutableRawPointer?, 50 | and selector: Selector 51 | ) where Key: PersistentKeyProtocol { 52 | addObserver(target, forKeyPath: key.id, context: context) 53 | } 54 | } 55 | 56 | // MARK: - Bespoke Implementation 57 | 58 | extension UserDefaults { 59 | // MARK: Register Default Values 60 | 61 | /// Registers the given keys with their default values. 62 | /// 63 | /// - Parameter keys: The keys to register. 64 | @inlinable 65 | public func register(_ keys: any PersistentKeyProtocol...) { 66 | register(keys) 67 | } 68 | 69 | /// Registers the given keys with their default values. 70 | /// 71 | /// - Parameter keys: The keys to register. 72 | public func register(_ keys: [any PersistentKeyProtocol]) { 73 | var defaults: [String: Any] = Dictionary(minimumCapacity: keys.capacity) 74 | 75 | for key in keys { 76 | key.registerDefault(in: &defaults) 77 | } 78 | 79 | register(defaults: defaults) 80 | } 81 | } 82 | 83 | // MARK: - Static Accessors 84 | 85 | extension PersistentKeyValueStore where Self == UserDefaults { 86 | // MARK: Public Static Interface 87 | 88 | /// A convenient way to access `UserDefaults.standard`. 89 | /// 90 | /// If you construct your own `UserDefaults` for an app group, we encourage you to make your own static accessor 91 | /// for your convenience. 92 | @inlinable 93 | public static var local: Self { 94 | .standard 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Persistent Key-Value Store/Implementations/InMemoryPersistentKeyValueStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InMemoryPersistentKeyValueStore.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 3/23/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A key-value store that persists data in memory. 11 | /// 12 | /// This store is useful for testing and development purposes, although `UserDefaults.standard` often works just as 13 | /// well. 14 | /// 15 | /// There is nothing that prevents it from being used in production, but it may be a sign that you are using the library 16 | /// unnecessarily. The library is intended to be used exclusively as a general interface on top of the two persistent 17 | /// key-value stores provided by Apple platforms: `UserDefaults` and `NSUbiquitousKeyValueStore`. 18 | public final class InMemoryPersistentKeyValueStore { 19 | private var storage: [String: Any] 20 | 21 | // MARK: Public Initialization 22 | 23 | /// Creates a new in-memory key-value store. 24 | public init() { 25 | storage = [:] 26 | } 27 | 28 | // MARK: Private Instance Interface 29 | 30 | private subscript(key: String) -> Any? { 31 | get { 32 | storage[key] 33 | } 34 | set { 35 | storage[key] = newValue 36 | } 37 | } 38 | } 39 | 40 | // MARK: - PersistentKeyValueStore Extension 41 | 42 | extension InMemoryPersistentKeyValueStore: PersistentKeyValueStore { 43 | // MARK: Getting Values 44 | 45 | public func get(_ key: Key) -> Key.Value where Key: PersistentKeyProtocol { 46 | self[key.id] as? Key.Value ?? key.defaultValue 47 | } 48 | 49 | // MARK: Setting Values 50 | 51 | public func set(_ key: Key, to value: Key.Value) where Key: PersistentKeyProtocol { 52 | // We loosely try to replicate property list behavior of not representing `nil` values in storage. 53 | if let array = value as? [Any?] { 54 | self[key.id] = array.compactMap(\.self) 55 | } else if let dict = value as? [String: Any?] { 56 | self[key.id] = dict.compactMapValues(\.self) 57 | } else { 58 | self[key.id] = value 59 | } 60 | } 61 | 62 | // MARK: Removing Values 63 | 64 | public func remove(_ key: Key) where Key: PersistentKeyProtocol { 65 | storage.removeValue(forKey: key.id) 66 | } 67 | 68 | // MARK: Observing Keys 69 | 70 | @inlinable 71 | public func deregister( 72 | _ observer: NSObject, 73 | for key: Key, 74 | context: UnsafeMutableRawPointer? 75 | ) where Key : PersistentKeyProtocol { 76 | // NO-OP 77 | } 78 | 79 | public func register( 80 | observer target: NSObject, 81 | for key: Key, 82 | with context: UnsafeMutableRawPointer?, 83 | and selector: Selector 84 | ) where Key: PersistentKeyProtocol { 85 | // NO-OP 86 | } 87 | } 88 | 89 | // MARK: - Static Accessors 90 | 91 | extension PersistentKeyValueStore where Self == InMemoryPersistentKeyValueStore { 92 | // MARK: Public Static Interface 93 | 94 | /// A store suitable for use in development, e.g. SwiftUI previews. 95 | @inlinable 96 | public static var ephemeral: Self { 97 | InMemoryPersistentKeyValueStore() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Persistent Key-Value Store/PersistentKeyValueStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersistentKeyValueStore.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 3/21/21. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A protocol for a key-value store that persists data across app launches. 11 | /// 12 | /// This interface is molded to the two persistent key-value stores provided by Apple platforms: `UserDefaults` and 13 | /// `NSUbiquitousKeyValueStore`. It takes necessary liberties by being fully aware of these implementations. It is not 14 | /// intended to support a generic key-value store implementation. 15 | public protocol PersistentKeyValueStore { 16 | // MARK: Getting Values 17 | 18 | /// Gets the value for the given key. 19 | /// 20 | /// The default value is returned if the key has not been set. 21 | /// 22 | /// - Parameter key: The key to get the value for. 23 | /// - Returns: The value for the given key, or the default value if the key has not been set. 24 | func get(_ key: Key) -> Key.Value where Key: PersistentKeyProtocol 25 | 26 | // MARK: Setting Values 27 | 28 | /// Sets the value for the given key. 29 | /// 30 | /// - Parameter key: The key to set the value for. 31 | /// - Parameter value: The new value for the key. 32 | func set(_ key: Key, to value: Key.Value) where Key: PersistentKeyProtocol 33 | 34 | // MARK: Removing Values 35 | 36 | /// Removes the value for the given key. 37 | /// 38 | /// - Parameter key: The key to remove the value for. 39 | func remove(_ key: Key) where Key: PersistentKeyProtocol 40 | 41 | // MARK: Observing Keys 42 | 43 | /// Removes an observer for the given key. 44 | /// 45 | /// - Important: This is a `NO-OP`, and not a concern, for `NSUbiquitousKeyValueStore`. 46 | /// - Parameter observer: The object to remove as an observer. 47 | /// - Parameter key: The key to stop observing. 48 | /// - Parameter context: Arbitrary data that more specifically identifies the observer to be removed. 49 | func deregister( 50 | _ observer: NSObject, 51 | for key: Key, 52 | context: UnsafeMutableRawPointer? 53 | ) where Key: PersistentKeyProtocol 54 | 55 | /// Registers an observer for the given key. 56 | /// 57 | /// This is used to orchestrate the SwiftUI property wrapper. This is not intended to provide a general way to 58 | /// observe changes to keys: its ergonomics are specifically designed for implementing 59 | /// ``PersistentKeyUIObservableObject``, knowing that the implementation is either `UserDefaults` or 60 | /// `NSUbiquitousKeyValueStore`. 61 | /// 62 | /// If `Self` is `UserDefaults` then key-value observation, notifying `target` directly, will be used. If `Self` is 63 | /// `NSUbiquitousKeyValueStore` then notifications will be used and `target` will be notified through the 64 | /// `selector`. 65 | /// 66 | /// For `UserDefaults`, neither the object receiving this message, nor observer, are retained. An object that calls 67 | /// this method must also eventually call either the ``deregister(_:forKeyPath:context:)`` method to unregister the 68 | /// observer. 69 | /// 70 | /// - Parameter observer: The observer to register. 71 | /// - Parameter key: The key to observe. 72 | /// - Parameter context: The context to use for `UserDefaults` key-value observation. This is a `NO-OP` for 73 | /// `NSUbiquitousKeyValueStore`. 74 | /// - Parameter selector: The selector to use for `NSUbiquitousKeyValueStore` notification observation. This is a 75 | /// `NO-OP` for `UserDefaults`. 76 | func register( 77 | observer: NSObject, 78 | for key: Key, 79 | with context: UnsafeMutableRawPointer?, 80 | and selector: Selector 81 | ) where Key: PersistentKeyProtocol 82 | } 83 | 84 | // MARK: Bespoke Implementation 85 | 86 | extension PersistentKeyValueStore { 87 | // MARK: Public Subscripts 88 | 89 | /// Gets the value for the given key. 90 | /// 91 | /// The default value is returned if the key has not been set. 92 | /// 93 | /// - Parameter key: The key to get the value for. 94 | /// - Returns: The value for the given key, or the default value if the key has not been set. 95 | @inlinable 96 | public subscript(_ key: some PersistentKeyProtocol) -> Value { 97 | self.get(key) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Persistent Key/PersistentDebugKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersistentDebugKey.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/9/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A key for a value in a ``PersistentKeyValueStore`` that is only mutable in debug builds. 11 | /// 12 | /// In production builds, the default value is always returned and setting the value has no effect. 13 | /// 14 | /// It is recommended to use the static accessor pattern to define and access keys. This pattern allows you to define 15 | /// keys in common locations and access them anywhere in a type-safe manner. The APIs are designed to be as ergonomic 16 | /// as possible for this pattern. 17 | /// 18 | /// e.g. 19 | /// 20 | /// ```swift 21 | /// extension PersistentKeyProtocol where Self == PersistentDebugKey { 22 | /// static var isAppStoreRatingEnabled: Self { 23 | /// Self( 24 | /// "IsAppStoreRatingEnabled", 25 | /// debugDefaultValue: false, 26 | /// releaseDefaultValue: true 27 | /// ) 28 | /// } 29 | /// } 30 | /// … 31 | /// userDefaults.set(.isAppStoreRatingEnabled, to: false) 32 | /// … 33 | /// userDefaults.get(.isAppStoreRatingEnabled) // false in Debug, true in Release 34 | /// ``` 35 | /// 36 | /// - Warning: Debug keys will only work if compiling this framework from source (e.g. as a SwiftPM dependency). If 37 | /// using a pre-built binary then the `DEBUG` code paths will likely not be included and default values will always 38 | /// be used. 39 | public struct PersistentDebugKey: 40 | Identifiable, 41 | PersistentKeyProtocol 42 | where 43 | Value: Sendable 44 | { 45 | /// The default value for the key. 46 | /// 47 | /// In debug builds, this value is used when the key has not been set. In production builds, this value is always 48 | /// used. 49 | public let defaultValue: Value 50 | 51 | /// The unique identifier for the key. 52 | /// 53 | /// This value must be unique across all keys in a given ``PersistentKeyValueStore``. 54 | public let id: String 55 | 56 | /// The representation of the key-value pair in the ``PersistentKeyValueStore``. 57 | public let representation: any PersistentKeyValueRepresentation 58 | 59 | // MARK: Public Initialization 60 | 61 | /// Creates a new key with the given identifier and default value. 62 | /// 63 | /// The key-value pair is represented by the default representation for ``Value``. 64 | /// 65 | /// - Parameter id: The unique identifier for the key. 66 | /// - Parameter defaultValue: The default value for the key. 67 | @inlinable 68 | public init( 69 | _ id: String, 70 | defaultValue: Value 71 | ) where Value: KeyValuePersistible { 72 | self.id = id 73 | self.defaultValue = defaultValue 74 | 75 | representation = Value.persistentKeyValueRepresentation 76 | } 77 | 78 | /// Creates a new key with the given identifier and separate default values for debug and release builds. 79 | /// 80 | /// The key-value pair is represented by the default representation for ``Value``. 81 | /// 82 | /// - Parameter id: The unique identifier for the key. 83 | /// - Parameter debugDefaultValue: The default value to use in debug builds. 84 | /// - Parameter releaseDefaultValue: The default value to use in release builds. 85 | @inlinable 86 | public init( 87 | _ id: String, 88 | debugDefaultValue: Value, 89 | releaseDefaultValue: Value 90 | ) where Value: KeyValuePersistible { 91 | self.id = id 92 | #if DEBUG 93 | self.defaultValue = debugDefaultValue 94 | #else 95 | self.defaultValue = releaseDefaultValue 96 | #endif 97 | 98 | representation = Value.persistentKeyValueRepresentation 99 | } 100 | 101 | /// Creates a new key with the given identifier, default value, and representation. 102 | /// 103 | /// Use this initializer to supply a custom representation for this specific key instead of using the default 104 | /// representation for ``Value``. 105 | /// 106 | /// - Parameter id: The unique identifier for the key. 107 | /// - Parameter defaultValue: The default value for the key. 108 | /// - Parameter representation: The representation used to persist the value. 109 | @inlinable 110 | public init( 111 | _ id: String, 112 | defaultValue: Value, 113 | representation: some PersistentKeyValueRepresentation 114 | ) { 115 | self.id = id 116 | self.defaultValue = defaultValue 117 | self.representation = representation 118 | } 119 | 120 | /// Creates a new key with the given identifier, separate default values for debug and release builds, and 121 | /// representation. 122 | /// 123 | /// Use this initializer to supply a custom representation for this specific key instead of using the default 124 | /// representation for ``Value``. 125 | /// 126 | /// - Parameter id: The unique identifier for the key. 127 | /// - Parameter debugDefaultValue: The default value to use in debug builds. 128 | /// - Parameter releaseDefaultValue: The default value to use in release builds. 129 | /// - Parameter representation: The representation used to persist the value. 130 | @inlinable 131 | public init( 132 | _ id: String, 133 | debugDefaultValue: Value, 134 | releaseDefaultValue: Value, 135 | representation: some PersistentKeyValueRepresentation 136 | ) { 137 | self.id = id 138 | #if DEBUG 139 | self.defaultValue = debugDefaultValue 140 | #else 141 | self.defaultValue = releaseDefaultValue 142 | #endif 143 | self.representation = representation 144 | } 145 | 146 | /// Creates a new key with the given identifier, default value, and representation. 147 | /// 148 | /// Use this initializer to supply a custom representation for this specific key instead of using the default 149 | /// representation for ``Value?``. 150 | /// 151 | /// - Parameter id: The unique identifier for the key. 152 | /// - Parameter defaultValue: The default value for the key. 153 | /// - Parameter representation: The representation used to persist the value. 154 | @inlinable 155 | public init( 156 | _ id: String, 157 | defaultValue: Value, 158 | representation: some PersistentKeyValueRepresentation 159 | ) where Value == Optional { 160 | self.id = id 161 | self.defaultValue = defaultValue 162 | self.representation = representation.optionalRepresentation 163 | } 164 | 165 | /// Creates a new key with the given identifier, separate default values for debug and release builds, and 166 | /// representation. 167 | /// 168 | /// Use this initializer to supply a custom representation for this specific key instead of using the default 169 | /// representation for ``Value?``. 170 | /// 171 | /// - Parameter id: The unique identifier for the key. 172 | /// - Parameter debugDefaultValue: The default value to use in debug builds. 173 | /// - Parameter releaseDefaultValue: The default value to use in release builds. 174 | /// - Parameter representation: The representation used to persist the value. 175 | @inlinable 176 | public init( 177 | _ id: String, 178 | debugDefaultValue: Value, 179 | releaseDefaultValue: Value, 180 | representation: some PersistentKeyValueRepresentation 181 | ) where Value == Optional { 182 | self.id = id 183 | #if DEBUG 184 | self.defaultValue = debugDefaultValue 185 | #else 186 | self.defaultValue = releaseDefaultValue 187 | #endif 188 | self.representation = representation.optionalRepresentation 189 | } 190 | } 191 | 192 | // MARK: - PersistentKeyProtocol Extension 193 | 194 | extension PersistentDebugKey { 195 | // MARK: Interfacing with User Defaults 196 | 197 | /// In debug builds, gets the value of the key from the given `UserDefaults`. 198 | /// 199 | /// In debug builds, the default value is returned if the key has not been set. In production builds, the default 200 | /// value is always returned. 201 | /// 202 | /// - Parameter userDefaults: The `UserDefaults` to get the value from. 203 | /// - Returns: In debug builds, the value of the key in the given `UserDefaults`, or the default value if the key 204 | /// has not been set. In production builds, the default value. 205 | @inlinable 206 | public func get(from userDefaults: UserDefaults) -> Value { 207 | #if DEBUG 208 | representation.get(id, from: userDefaults) ?? defaultValue 209 | #else 210 | defaultValue 211 | #endif 212 | } 213 | 214 | /// In debug builds, removes the value of the key from the given `UserDefaults`. 215 | /// 216 | /// In production builds, this method has no effect. 217 | /// 218 | /// - Parameter userDefaults: The `UserDefaults` to remove the value from. 219 | @inlinable 220 | public func remove(from userDefaults: UserDefaults) { 221 | #if DEBUG 222 | userDefaults.removeObject(forKey: id) 223 | #else 224 | // NO-OP 225 | #endif 226 | } 227 | 228 | /// In debug builds, sets the value of the key to the given value in the given `UserDefaults`. 229 | /// 230 | /// In production builds, this method has no effect. 231 | /// 232 | /// - Parameter newValue: The new value for the key. 233 | @inlinable 234 | public func set(to newValue: Value, in userDefaults: UserDefaults) { 235 | #if DEBUG 236 | representation.set(id, to: newValue, in: userDefaults) 237 | #else 238 | // NO-OP 239 | #endif 240 | } 241 | 242 | // MARK: Interfacing with Ubiquitous Key-Value Store 243 | 244 | /// In debug builds, gets the value of the key from the given `NSUbiquitousKeyValueStore`. The default value is 245 | /// returned if the key has not been set. 246 | /// 247 | /// In production builds, the default value is always returned. 248 | /// 249 | /// - Parameter ubiquitousStore: The `NSUbiquitousKeyValueStore` to get the value from. 250 | /// - Returns: In debug builds, the value of the key in the given `NSUbiquitousKeyValueStore`, or the default value 251 | /// if the key has not been set. In production builds, the default value. 252 | @available(watchOS 9.0, *) 253 | @inlinable 254 | public func get(from ubiquitousStore: NSUbiquitousKeyValueStore) -> Value { 255 | #if DEBUG 256 | representation.get(id, from: ubiquitousStore) ?? defaultValue 257 | #else 258 | defaultValue 259 | #endif 260 | } 261 | 262 | /// In debug builds, removes the value of the key from the given `NSUbiquitousKeyValueStore`. 263 | /// 264 | /// In production builds, this method has no effect. 265 | /// 266 | /// - Parameter ubiquitousStore: The `NSUbiquitousKeyValueStore` to remove the value from. 267 | @available(watchOS 9.0, *) 268 | @inlinable 269 | public func remove(from ubiquitousStore: NSUbiquitousKeyValueStore) { 270 | #if DEBUG 271 | ubiquitousStore.removeObject(forKey: id) 272 | #else 273 | // NO-OP 274 | #endif 275 | } 276 | 277 | /// In debug builds, sets the value of the key to the given value in the given `NSUbiquitousKeyValueStore`. 278 | /// 279 | /// In production builds, this method has no effect. 280 | /// 281 | /// - Parameter newValue: The new value for the key. 282 | /// - Parameter ubiquitousStore: The `NSUbiquitousKeyValueStore` to set the value in. 283 | @available(watchOS 9.0, *) 284 | @inlinable 285 | public func set(to newValue: Value, in ubiquitousStore: NSUbiquitousKeyValueStore) { 286 | #if DEBUG 287 | representation.set(id, to: newValue, in: ubiquitousStore) 288 | #else 289 | // NO-OP 290 | #endif 291 | } 292 | } 293 | 294 | // MARK: - Conditional Equatable Extension 295 | 296 | extension PersistentDebugKey: Equatable where Value: Equatable { 297 | // MARK: Public Static Interface 298 | 299 | @inlinable 300 | public static func == (lhs: PersistentDebugKey, rhs: PersistentDebugKey) -> Bool { 301 | lhs.defaultValue == rhs.defaultValue && lhs.id == rhs.id 302 | } 303 | } 304 | 305 | // MARK: - Conditional Hashable Extension 306 | 307 | extension PersistentDebugKey: Hashable where Value: Hashable { 308 | // MARK: Public Instance Interface 309 | 310 | @inlinable 311 | public func hash(into hasher: inout Hasher) { 312 | hasher.combine(defaultValue) 313 | hasher.combine(id) 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Persistent Key/PersistentKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersistentKey.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 3/28/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A key for a value in a ``PersistentKeyValueStore``. 11 | /// 12 | /// It is recommended to use the static accessor pattern to define and access keys. This pattern allows you to define 13 | /// keys in common locations and access them anywhere in a type-safe manner. The APIs are designed to be as ergonomic 14 | /// as possible for this pattern. 15 | /// 16 | /// e.g. 17 | /// 18 | /// ```swift 19 | /// extension PersistentKeyProtocol where Self == PersistentKey { 20 | /// static var mostRecentLaunchDate: Self { 21 | /// Self("MostRecentLaunchDate", defaultValue: .distantPast) 22 | /// } 23 | /// } 24 | /// … 25 | /// userDefaults.set(.mostRecentLaunchDate, to: .now) 26 | /// … 27 | /// guard userDefaults.get(.mostRecentLaunchDate) < firstOfMonth else … 28 | /// ``` 29 | public struct PersistentKey: Identifiable where Value: Sendable { 30 | /// The default value for the key. 31 | /// 32 | /// This value is returned when the key is not present. 33 | public let defaultValue: Value 34 | 35 | /// The unique identifier for the key. 36 | /// 37 | /// This value must be unique across all keys in a given ``PersistentKeyValueStore``. 38 | public let id: String 39 | 40 | /// The representation of the key-value pair in the ``PersistentKeyValueStore``. 41 | public let representation: any PersistentKeyValueRepresentation 42 | 43 | // MARK: Public Initialization 44 | 45 | /// Creates a new key with the given identifier and default value. 46 | /// 47 | /// The key-value pair is represented by the default representation for ``Value``. 48 | /// 49 | /// - Parameter id: The unique identifier for the key. 50 | /// - Parameter defaultValue: The default value for the key. 51 | @inlinable 52 | public init( 53 | _ id: String, 54 | defaultValue: Value 55 | ) where Value: KeyValuePersistible { 56 | self.id = id 57 | self.defaultValue = defaultValue 58 | 59 | representation = Value.persistentKeyValueRepresentation 60 | } 61 | 62 | /// Creates a new key with the given identifier, default value, and representation. 63 | /// 64 | /// Use this initializer to supply a custom representation for this specific key instead of using the default 65 | /// representation for ``Value``. 66 | /// 67 | /// - Parameter id: The unique identifier for the key. 68 | /// - Parameter defaultValue: The default value for the key. 69 | /// - Parameter representation: The representation used to persist the value. 70 | @inlinable 71 | public init( 72 | _ id: String, 73 | defaultValue: Value, 74 | representation: some PersistentKeyValueRepresentation 75 | ) { 76 | self.id = id 77 | self.defaultValue = defaultValue 78 | self.representation = representation 79 | } 80 | 81 | /// Creates a new key with the given identifier, default value, and representation. 82 | /// 83 | /// Use this initializer to supply a custom representation for this specific key instead of using the default 84 | /// representation for ``Value?``. 85 | /// 86 | /// - Parameter id: The unique identifier for the key. 87 | /// - Parameter defaultValue: The default value for the key. 88 | /// - Parameter representation: The representation used to persist the value. 89 | @inlinable 90 | public init( 91 | _ id: String, 92 | defaultValue: Value, 93 | representation: some PersistentKeyValueRepresentation 94 | ) where Value == Optional { 95 | self.id = id 96 | self.defaultValue = defaultValue 97 | self.representation = representation.optionalRepresentation 98 | } 99 | } 100 | 101 | // MARK: - PersistentKeyProtocol Extension 102 | 103 | extension PersistentKey: PersistentKeyProtocol { 104 | // MARK: Interfacing with User Defaults 105 | 106 | @inlinable 107 | public func get(from userDefaults: UserDefaults) -> Value { 108 | representation.get(id, from: userDefaults) ?? defaultValue 109 | } 110 | 111 | @inlinable 112 | public func remove(from userDefaults: UserDefaults) { 113 | userDefaults.removeObject(forKey: id) 114 | } 115 | 116 | @inlinable 117 | public func set(to newValue: Value, in userDefaults: UserDefaults) { 118 | representation.set(id, to: newValue, in: userDefaults) 119 | } 120 | 121 | // MARK: Interfacing with Ubiquitous Key-Value Store 122 | 123 | @available(watchOS 9.0, *) 124 | @inlinable 125 | public func get(from ubiquitousStore: NSUbiquitousKeyValueStore) -> Value { 126 | representation.get(id, from: ubiquitousStore) ?? defaultValue 127 | } 128 | 129 | @available(watchOS 9.0, *) 130 | @inlinable 131 | public func remove(from ubiquitousStore: NSUbiquitousKeyValueStore) { 132 | ubiquitousStore.removeObject(forKey: id) 133 | } 134 | 135 | @available(watchOS 9.0, *) 136 | @inlinable 137 | public func set(to newValue: Value, in ubiquitousStore: NSUbiquitousKeyValueStore) { 138 | representation.set(id, to: newValue, in: ubiquitousStore) 139 | } 140 | } 141 | 142 | // MARK: - Conditional Equatable Extension 143 | 144 | extension PersistentKey: Equatable where Value: Equatable { 145 | // MARK: Public Static Interface 146 | 147 | @inlinable 148 | public static func == (lhs: PersistentKey, rhs: PersistentKey) -> Bool { 149 | lhs.defaultValue == rhs.defaultValue && lhs.id == rhs.id 150 | } 151 | } 152 | 153 | // MARK: - Conditional Hashable Extension 154 | 155 | extension PersistentKey: Hashable where Value: Hashable { 156 | // MARK: Public Instance Interface 157 | 158 | @inlinable 159 | public func hash(into hasher: inout Hasher) { 160 | hasher.combine(defaultValue) 161 | hasher.combine(id) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Persistent Key/PersistentKeyProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersistentKeyProtocol.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/17/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A protocol for a key for a value in a ``PersistentKeyValueStore``. 11 | /// 12 | /// This interface is designed around the two persistent key-value stores provided by Apple platforms: `UserDefaults` 13 | /// and `NSUbiquitousKeyValueStore`. It is not intended to support a generic key-value store implementation. 14 | public protocol PersistentKeyProtocol: Identifiable, Sendable where ID == String { 15 | // MARK: Associated Types 16 | 17 | /// The type of the value for the key. 18 | associatedtype Value 19 | 20 | // MARK: Instance Interface 21 | 22 | /// The default value for the key. 23 | /// 24 | /// This value is used when the key has not been set. 25 | var defaultValue: Value { get } 26 | 27 | /// The unique identifier for the key. 28 | /// 29 | /// This is value used as the key in the underlying stores. 30 | /// 31 | /// - Important: This value must be unique across all keys in a given ``PersistentKeyValueStore``. 32 | var id: ID { get } 33 | 34 | /// The representation of the key-value pair in the ``PersistentKeyValueStore``. 35 | var representation: any PersistentKeyValueRepresentation { get } 36 | 37 | // MARK: Interfacing with User Defaults 38 | 39 | /// Gets the value of the key from the given `UserDefaults`. 40 | /// 41 | /// The default value is returned if the key has not been set. 42 | /// 43 | /// - Parameter userDefaults: The `UserDefaults` to get the value from. 44 | /// - Returns: The value of the key in the given `UserDefaults`, or the default value if the key has not been set. 45 | func get(from userDefaults: UserDefaults) -> Value 46 | 47 | /// Removes the value of the key from the given `UserDefaults`. 48 | /// 49 | /// - Parameter userDefaults: The `UserDefaults` to remove the value from. 50 | func remove(from userDefaults: UserDefaults) 51 | 52 | /// Sets the value of the key to the given value in the given `UserDefaults`. 53 | /// 54 | /// - Parameter newValue: The new value for the key. 55 | /// - Parameter userDefaults: The `UserDefaults` to set the value in. 56 | func set(to newValue: Value, in userDefaults: UserDefaults) 57 | 58 | // MARK: Interfacing with Ubiquitous Key-Value Store 59 | 60 | /// Gets the value of the key from the given `NSUbiquitousKeyValueStore`. 61 | /// 62 | /// The default value is returned if the key has not been set. 63 | /// 64 | /// - Parameter ubiquitousStore: The `NSUbiquitousKeyValueStore` to get the value from. 65 | /// - Returns: The value of the key in the given `NSUbiquitousKeyValueStore`, or the default value if the key has 66 | /// not been set. 67 | @available(watchOS 9.0, *) 68 | func get(from ubiquitousStore: NSUbiquitousKeyValueStore) -> Value 69 | 70 | /// Removes the value of the key from the given `NSUbiquitousKeyValueStore`. 71 | /// 72 | /// - Parameter ubiquitousStore: The `NSUbiquitousKeyValueStore` to remove the value from. 73 | @available(watchOS 9.0, *) 74 | func remove(from ubiquitousStore: NSUbiquitousKeyValueStore) 75 | 76 | /// Sets the value of the key to the given value in the given `NSUbiquitousKeyValueStore`. 77 | /// 78 | /// - Parameter newValue: The new value for the key. 79 | /// - Parameter ubiquitousStore: The `NSUbiquitousKeyValueStore` to set the value in. 80 | @available(watchOS 9.0, *) 81 | func set(to newValue: Value, in ubiquitousStore: NSUbiquitousKeyValueStore) 82 | } 83 | 84 | // MARK: - Bespoke Implementation 85 | 86 | extension PersistentKeyProtocol { 87 | // MARK: Public Instance Interface 88 | 89 | /// Registers the default value for the key in the given property-list-compatible dictionary. 90 | /// 91 | /// This is intended for use by ``UserDefaults/register(_:)`` to register the default values for the keys. 92 | /// 93 | /// - Parameter propertyListDictionary: The dictionary to register the default value in. 94 | @inlinable 95 | public func registerDefault(in propertyListDictionary: inout [String: Any]) { 96 | representation.set(id, to: defaultValue, in: &propertyListDictionary) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Property Wrapper/DefaultPersistentKeyValueStoreViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultPersistentKeyValueStoreViewModifier.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 7/1/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A view modifier that sets the `defaultPersistentKeyValueStore` environment value to the given store. 11 | /// 12 | /// This modifier allows you to specify a custom `PersistentKeyValueStore` to be used as the default store for all 13 | /// ``PersistentValue`` instances within the modified view hierarchy. 14 | /// 15 | /// Using this view modifier is equivalent to: 16 | /// 17 | /// ```swift 18 | /// environment(\.defaultPersistentKeyValueStore, store) 19 | /// ``` 20 | /// 21 | /// - Important: This modifier only affects ``PersistentValue`` instances that don't have a custom store specified. 22 | /// ``PersistentValue`` instances with a custom store will continue to use their specified store. 23 | public struct DefaultPersistentKeyValueStoreViewModifier { 24 | /// The store that will be set as the default in the environment where the view modifier is applied. 25 | public let store: any PersistentKeyValueStore 26 | 27 | // MARK: Public Initialization 28 | 29 | /// Creates a new view modifier with the specified store. 30 | /// 31 | /// - Parameter store: The `PersistentKeyValueStore` to be used as the default store in the modified view hierarchy. 32 | @inlinable 33 | public init(store: any PersistentKeyValueStore) { 34 | self.store = store 35 | } 36 | } 37 | 38 | // MARK: - ViewModifier Extension 39 | 40 | extension DefaultPersistentKeyValueStoreViewModifier: ViewModifier { 41 | // MARK: Modifier Body 42 | 43 | @inlinable 44 | public func body(content: Content) -> some View { 45 | content 46 | .environment(\.defaultPersistentKeyValueStore, store) 47 | } 48 | } 49 | 50 | // MARK: - Extension for View 51 | 52 | extension View { 53 | // MARK: Public Instance Interface 54 | 55 | /// Sets the default persistent key-value store for ``PersistentValue`` instances within this view. 56 | /// 57 | /// Use this modifier to specify a custom ``PersistentKeyValueStore`` to be used as the default store for all 58 | /// ``PersistentValue`` instances within the modified view hierarchy that don't have a custom store specified. 59 | /// 60 | /// Using this view modifier is equivalent to: 61 | /// 62 | /// ```swift 63 | /// environment(\.defaultPersistentKeyValueStore, store) 64 | /// ``` 65 | /// 66 | /// - Important: This modifier only affects ``PersistentValue`` instances that don't have a custom store specified. 67 | /// ``PersistentValue`` instances with a custom store will continue to use their specified store. 68 | /// - Parameter store: The `PersistentKeyValueStore` to use as the default store for ``PersistentValue`` instances. 69 | /// - Returns: A view with the default persistent key-value store set to the specified store. 70 | @inlinable 71 | public func defaultPersistentKeyValueStore( 72 | _ store: any PersistentKeyValueStore 73 | ) -> ModifiedContent { 74 | modifier(DefaultPersistentKeyValueStoreViewModifier(store: store)) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Property Wrapper/PersistentKeyUIObservableObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersistentKeyUIObservableObject.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 6/13/22. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | /// A `MainActor`-isolated observer for a key in a ``PersistentKeyValueStore``. 12 | /// 13 | /// This class, and the ``PersistentKeyValueStore`` functions it relies upon, implement the two known ways to observe 14 | /// `UserDefaults` and `NSUbiquitousKeyValueStore`. It is likely that other ``PersistentKeyValueStore`` implementations 15 | /// can be written under the same interface but that is not what it is optimized for. This lets us use 16 | /// ``PersistentValue`` with any type of ``PersistentKeyValueStore`` and simplifies many callsites that previously had 17 | /// to be duplicated between those two known ``PersistentKeyValueStore`` implementations. 18 | /// 19 | /// - Important: Using this class with `NSUbiquitousKeyValueStore` requires all local (i.e. in-app) mutations to be 20 | /// performed through the ``PersistentKeyValueStore`` APIs, or ``PersistentValue``. There is no way to observe local 21 | /// changes performed through the system APIs for `NSUbiquitousKeyValueStore`; notifications only exist for external 22 | /// changes, and key-value observation does not work. We emit a custom notification for internal changes to emulate 23 | /// the external system behavior. 24 | @MainActor 25 | public final class PersistentKeyUIObservableObject: ObservableObject where Key: PersistentKeyProtocol { 26 | /// The key being observed. 27 | public let key: Key 28 | 29 | /// The store that the key is being observed in. 30 | private var _store: (any PersistentKeyValueStore)? 31 | 32 | /// The object used for key-value and `NotificationCenter` observation. 33 | private var observer: Observer! 34 | 35 | // MARK: Public Initialization 36 | 37 | /// Creates a new observer for the given key in the given store. 38 | /// 39 | /// - Parameter store: The store to observe the key in. 40 | /// - Parameter key: The key to observe. 41 | public init(store: (any PersistentKeyValueStore)?, key: Key) { 42 | self.key = key 43 | 44 | _store = store 45 | 46 | observer = Observer(keyID: key.id) { [weak self] in 47 | self?.objectWillChange.send() 48 | } deregister: { @MainActor [weak self] in 49 | self?.deregisterObserver(on: store) 50 | } 51 | 52 | registerObserver(on: store) 53 | } 54 | 55 | // MARK: Public Instance Interface 56 | 57 | /// The store that the key is being observed in. 58 | public var store: (any PersistentKeyValueStore)? { 59 | get { 60 | _store 61 | } 62 | set { 63 | deregisterObserver(on: _store) 64 | 65 | _store = newValue 66 | 67 | observer.deregister = { @MainActor [weak self] in 68 | self?.deregisterObserver(on: newValue) 69 | } 70 | 71 | registerObserver(on: newValue) 72 | } 73 | } 74 | 75 | /// The value of the key in the store. 76 | public var value: Key.Value { 77 | get { 78 | assertStoreExists(_store) 79 | 80 | return _store?.get(key) ?? key.defaultValue 81 | } 82 | set { 83 | assertStoreExists(_store) 84 | 85 | objectWillChange.send() 86 | 87 | _store?.set(key, to: newValue) 88 | } 89 | } 90 | 91 | // MARK: Private Instance Interface 92 | 93 | private func assertStoreExists(_ store: (any PersistentKeyValueStore)?) { 94 | assert(store != nil, "Store should always be set on initialization or immediately after.") 95 | } 96 | 97 | private func deregisterObserver(on store: (any PersistentKeyValueStore)?) { 98 | store?.deregister(observer, for: key, context: nil) 99 | } 100 | 101 | private func registerObserver(on store: (any PersistentKeyValueStore)?) { 102 | store?.register( 103 | observer: observer, 104 | for: key, 105 | with: nil, 106 | and: #selector(Observer.didReceive(_:)) 107 | ) 108 | } 109 | } 110 | 111 | // MARK: - Observer Definition 112 | 113 | private final class Observer: NSObject, ObservableObject where Key: PersistentKeyProtocol { 114 | internal let keyID: String 115 | internal let objectWillChange: @MainActor () -> Void 116 | 117 | internal var deregister: @MainActor () -> Void 118 | 119 | // MARK: Internal Initialization 120 | 121 | internal init( 122 | keyID: String, 123 | objectWillChange: @escaping @MainActor () -> Void, 124 | deregister: @escaping @MainActor () -> Void 125 | ) { 126 | self.keyID = keyID 127 | self.objectWillChange = objectWillChange 128 | self.deregister = deregister 129 | } 130 | 131 | // MARK: Deinitialization 132 | 133 | deinit { 134 | performOnMainIfNecessary(deregister) 135 | } 136 | 137 | // MARK: NSObject Implementation 138 | 139 | /// - SeeAlso: https://forums.swift.org/t/crash-when-running-in-swift-6-language-mode/72431/2 140 | internal nonisolated override func observeValue( 141 | forKeyPath keyPath: String?, 142 | of object: Any?, 143 | change: [NSKeyValueChangeKey: Any]?, 144 | context: UnsafeMutableRawPointer? 145 | ) { 146 | performOnMainIfNecessary(objectWillChange) 147 | } 148 | 149 | // MARK: Internal Instance Interface 150 | 151 | /// - SeeAlso: https://forums.swift.org/t/crash-when-running-in-swift-6-language-mode/72431/2 152 | @objc 153 | internal nonisolated func didReceive(_ notification: Notification) { 154 | guard 155 | #available(watchOS 9.0, *), 156 | let changedKeyIDs = notification.userInfo?[NSUbiquitousKeyValueStore.changedKeysKey] as? [String] 157 | else { 158 | return 159 | } 160 | 161 | let changedKeyIDsSet = Set(changedKeyIDs) 162 | 163 | guard changedKeyIDsSet.contains(keyID) else { 164 | return 165 | } 166 | 167 | performOnMainIfNecessary(objectWillChange) 168 | } 169 | 170 | // MARK: Private Instance Interface 171 | 172 | private func performOnMainIfNecessary(_ action: @escaping @MainActor () -> Void) { 173 | if Thread.isMainThread { 174 | MainActor.assumeIsolated(action) 175 | } else { 176 | DispatchQueue.main.async(execute: action) 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Property Wrapper/PersistentValue+EnvironmentValues.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersistentValue+EnvironmentValues.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 7/1/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - Extension for EnvironmentValues 11 | 12 | extension EnvironmentValues { 13 | /// The default persistent key-value store for the environment. 14 | /// 15 | /// This property allows you to read and write to the default ``PersistentKeyValueStore`` used by 16 | /// ``PersistentValue`` property wrappers in the current environment. 17 | /// 18 | /// You can set a custom default store for a view hierarchy using the `defaultPersistentKeyValueStore(_:)` modifier: 19 | /// 20 | /// ```swift 21 | /// ContentView() 22 | /// .defaultPersistentKeyValueStore(.ubiquitous) 23 | /// ``` 24 | /// 25 | /// If you don't set a custom default store, this value is set to `UserDefaults.standard`. 26 | /// 27 | /// - SeeAlso: ``PersistentKeyValueStore`` 28 | /// - SeeAlso: ``PersistentValue`` 29 | public var defaultPersistentKeyValueStore: any PersistentKeyValueStore { 30 | get { self[DefaultPersistentKeyValueStoreKey.self] } 31 | set { self[DefaultPersistentKeyValueStoreKey.self] = newValue } 32 | } 33 | } 34 | 35 | // MARK: - EnvironmentKey Definition 36 | 37 | private struct DefaultPersistentKeyValueStoreKey: EnvironmentKey { 38 | // MARK: Internal Static Interface 39 | 40 | @usableFromInline 41 | internal static var defaultValue: any PersistentKeyValueStore { 42 | UserDefaults.standard 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/PersistentKeyValueKit/Property Wrapper/PersistentValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersistentValue.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/21/22. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import SwiftUI 11 | 12 | /// A property wrapper type that can read and write to a value in a ``PersistentKeyValueStore``. 13 | /// 14 | /// Use `PersistentValue` to create a property that can read and write to a value in a ``PersistentKeyValueStore``. 15 | /// The value is automatically synchronized with the store, ensuring that changes in the store are reflected in the 16 | /// property and vice versa. 17 | /// 18 | /// You can use `PersistentValue` in a SwiftUI view like this: 19 | /// 20 | /// ```swift 21 | /// struct ContentView: View { 22 | /// @PersistentValue(.isAppStoreRatingEnabled, store: userDefaults) 23 | /// var isAppStoreRatingEnabled: Bool 24 | /// 25 | /// var body: some View { 26 | /// Toggle("Enable App Store Rating", isOn: $isAppStoreRatingEnabled) 27 | /// } 28 | /// } 29 | /// ``` 30 | /// 31 | /// If you don't specify a store, `PersistentValue` uses the `defaultPersistentKeyValueStore` from the environment. If 32 | /// you don't specify a default value, `UserDefaults.standard` is used. 33 | /// 34 | /// - Important: Using this property wrapper with `NSUbiquitousKeyValueStore` requires all local (i.e. in-app) mutations 35 | /// to be performed through the ``PersistentKeyValueStore`` APIs, or this property wrapper. There is no way to observe 36 | /// local changes performed through the system APIs for `NSUbiquitousKeyValueStore`; notifications only exist for 37 | /// external changes, and key-value observation does not work. We emit a custom notification for internal changes to 38 | /// emulate the external system behavior. 39 | /// - Attention: We use `@preconcurrency` to conform to `DynamicProperty` because otherwise `update()` is forced to 40 | /// be nonisolated in order to meet the conformance. Apple uses `@preconcurrency` for their `@MainActor`-isolated 41 | /// property wrappers as of 2024-10-22. 42 | @propertyWrapper 43 | @MainActor 44 | public struct PersistentValue where Key: PersistentKeyProtocol { 45 | @Environment(\.defaultPersistentKeyValueStore) private var defaultStore 46 | 47 | @StateObject private var observer: PersistentKeyUIObservableObject 48 | 49 | // MARK: Public Initialization 50 | 51 | /// Creates a property that can read and write to a value in a ``PersistentKeyValueStore``. 52 | /// 53 | /// - Parameter key: The key to read and write to in the persistent store. 54 | /// - Parameter store: The ``PersistentKeyValueStore`` to read and write to. If `nil`, `PersistentValue` uses the 55 | /// `defaultPersistentKeyValueStore` from the environment. 56 | public init(_ key: Key, store: (any PersistentKeyValueStore)? = nil) { 57 | _observer = StateObject(wrappedValue: PersistentKeyUIObservableObject(store: store, key: key)) 58 | } 59 | 60 | // MARK: Property Wrapper Implementation 61 | 62 | /// A binding to the underlying value referenced by the persistent value property. 63 | /// 64 | /// This property allows SwiftUI to automatically update the value when used in controls like `TextField`. 65 | /// To get the `projectedValue`, use the `$` operator on the property variable. 66 | @inlinable 67 | public var projectedValue: Binding { 68 | Binding { 69 | wrappedValue 70 | } set: { 71 | wrappedValue = $0 72 | } 73 | } 74 | 75 | /// The underlying value referenced by the persistent key. 76 | /// 77 | /// This property provides primary access to the value. When you use the `@PersistentValue` property wrapper, 78 | /// the wrapped value is this property. 79 | /// 80 | /// Accessing this property reads and writes the value from the underlying ``PersistentKeyValueStore``. 81 | public var wrappedValue: Key.Value { 82 | get { 83 | observer.value 84 | } 85 | nonmutating set { 86 | observer.value = newValue 87 | } 88 | } 89 | } 90 | 91 | // MARK: - DynamicProperty Extension 92 | 93 | extension PersistentValue: @preconcurrency DynamicProperty { 94 | // MARK: Updating the Value 95 | 96 | public func update() { 97 | guard observer.store == nil else { 98 | return 99 | } 100 | 101 | observer.store = defaultStore 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Scaffolding/Custom Persistent Key-Value Representations/ReferenceProxyPersistentKeyValueRepresentation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReferenceProxyPersistentKeyValueRepresentation.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 10/1/24. 6 | // 7 | 8 | import PersistentKeyValueKit 9 | 10 | public final class ReferenceProxyPersistentKeyValueRepresentation { 11 | private let deserializing: @Sendable (Proxy) -> Value? 12 | private let serializing: @Sendable (Value) -> Proxy? 13 | 14 | // MARK: Public Initialization 15 | 16 | public init( 17 | serializing: @Sendable @escaping (Value) -> Proxy?, 18 | deserializing: @Sendable @escaping (Proxy) -> Value? 19 | ) { 20 | self.serializing = serializing 21 | self.deserializing = deserializing 22 | } 23 | } 24 | 25 | // MARK: - ProxyPersistentKeyValueRepresentationProtocol Extension 26 | 27 | extension ReferenceProxyPersistentKeyValueRepresentation: ProxyPersistentKeyValueRepresentationProtocol { 28 | // MARK: Public Instance Interface 29 | 30 | public func from(_ proxy: Proxy) -> Value? { 31 | deserializing(proxy) 32 | } 33 | 34 | public func to(_ value: Value) -> Proxy? { 35 | serializing(value) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Scaffolding/Custom Persistible Types/CustomPersistibleType+Codable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomPersistibleType+Codable.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/17/22. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | 11 | extension CustomPersistibleType { 12 | public struct Codable: Swift.Codable, Equatable, Sendable { 13 | public let int: Int 14 | public let string: String 15 | 16 | // MARK: Public Initialization 17 | 18 | @inlinable 19 | public init(int: Int, string: String) { 20 | self.int = int 21 | self.string = string 22 | } 23 | } 24 | } 25 | 26 | // MARK: - KeyValuePersistible Extension 27 | 28 | @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) 29 | extension CustomPersistibleType.Codable: KeyValuePersistible { 30 | // MARK: Public Static Interface 31 | 32 | @inlinable 33 | public static var persistentKeyValueRepresentation: some PersistentKeyValueRepresentation { 34 | CodablePersistentKeyValueRepresentation(encoder: encoder, decoder: decoder) 35 | } 36 | } 37 | 38 | // MARK: - Constants 39 | 40 | extension CustomPersistibleType.Codable { 41 | public static let large = Self(int: .max, string: String(Array(repeating: "A", count: 1_000))) 42 | public static let small = Self(int: .min, string: "") 43 | } 44 | 45 | // MARK: - Coders 46 | 47 | extension CustomPersistibleType.Codable { 48 | public static let decoder = JSONDecoder() 49 | 50 | public static let encoder: JSONEncoder = { 51 | let encoder = JSONEncoder() 52 | encoder.outputFormatting = .sortedKeys 53 | return encoder 54 | }() 55 | } 56 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Scaffolding/Custom Persistible Types/CustomPersistibleType+Comprehensive.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomPersistibleType+Comprehensive.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 10/12/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | 11 | extension CustomPersistibleType { 12 | /// A type that contains one of every persistible type. 13 | /// 14 | /// We purposefully do not conform this to ``KeyValuePersistible`` so as to encourage testing it with multiple 15 | /// proxy representations (i.e. ``StringlyKeyedDictionaryPersistentKeyValueRepresentation`` and 16 | /// ``CodablePersistentKeyValueRepresentation``). It also serves to gurantee that complex types that do not conform 17 | /// can still be persisted with a representation that is passed to a ``PersistentKeyProtocol`` implementation. 18 | /// 19 | /// This is a (minor) superset of ``CustomPersistibleType.StringlyKeyedDictionary``. 20 | public struct Comprehensive: Swift.Codable, Sendable { 21 | public let arrayOfPrimitives: [Int] 22 | public let arrayOfProxies: [CustomPersistibleType.Proxy] 23 | public let bool: Bool 24 | public let codable: CustomPersistibleType.Codable 25 | public let data: Data 26 | public let double: Double 27 | public let float: Float 28 | public let int: Int 29 | public let losslessStringConvertible: CustomPersistibleType.LosslessStringConvertible 30 | public let proxy: CustomPersistibleType.Proxy 31 | public let rawRepresentable: CustomPersistibleType.RawRepresentable 32 | public let string: String 33 | public let stringOptional: String? 34 | public let url: URL 35 | } 36 | } 37 | 38 | // MARK: - Constants 39 | 40 | extension CustomPersistibleType.Comprehensive { 41 | public static let large = Self( 42 | arrayOfPrimitives: [1, 2, 3, 4, 5], 43 | arrayOfProxies: [.distantPast, .distantFuture], 44 | bool: true, 45 | codable: .large, 46 | data: Data([0xDE, 0xAD, 0xBE, 0xEF]), 47 | double: .greatestFiniteMagnitude, 48 | float: .greatestFiniteMagnitude, 49 | int: .max, 50 | losslessStringConvertible: "🙂", 51 | proxy: .distantFuture, 52 | rawRepresentable: .caseOne, 53 | string: String(Array(repeating: "A", count: 1_000)), 54 | stringOptional: nil, 55 | url: URL(string: "https://kylehugh.es")! 56 | ) 57 | 58 | public static let small = Self( 59 | arrayOfPrimitives: [], 60 | arrayOfProxies: [], 61 | bool: false, 62 | codable: .small, 63 | data: Data([0x12, 0x34, 0x56, 0x78]), 64 | double: .leastNormalMagnitude, 65 | float: .leastNormalMagnitude, 66 | int: .min, 67 | losslessStringConvertible: "☹️", 68 | proxy: .distantPast, 69 | rawRepresentable: .caseTwo, 70 | string: "", 71 | stringOptional: nil, 72 | url: URL(string: "https://kylehugh.es")! 73 | ) 74 | } 75 | 76 | // MARK: - Representations 77 | 78 | extension CustomPersistibleType.Comprehensive { 79 | @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) 80 | public static let codableRepresentation = CodablePersistentKeyValueRepresentation< 81 | CustomPersistibleType.Comprehensive, 82 | JSONEncoder, 83 | JSONDecoder 84 | >() 85 | } 86 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Scaffolding/Custom Persistible Types/CustomPersistibleType+LosslessStringConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomPersistibleType+LosslessStringConvertible.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 10/30/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | 11 | extension CustomPersistibleType { 12 | public typealias LosslessStringConvertible = Character 13 | } 14 | 15 | // MARK: - Decodable Extension 16 | 17 | extension Character: @retroactive Decodable { 18 | // MARK: Public Initialization 19 | 20 | public init(from decoder: Decoder) throws { 21 | let container = try decoder.singleValueContainer() 22 | let string = try container.decode(String.self) 23 | 24 | guard let character = string.first, string.count == 1 else { 25 | throw DecodingError.dataCorruptedError( 26 | in: container, 27 | debugDescription: "Invalid character: \(string). Expected single character string." 28 | ) 29 | } 30 | 31 | self = character 32 | } 33 | } 34 | 35 | // MARK: - Encodable Extension 36 | 37 | extension Character: @retroactive Encodable { 38 | // MARK: Public Instance Interface 39 | 40 | public func encode(to encoder: Encoder) throws { 41 | var container = encoder.singleValueContainer() 42 | 43 | try container.encode(String(self)) 44 | } 45 | } 46 | 47 | // MARK: - KeyValuePersistible Extension 48 | 49 | extension CustomPersistibleType.LosslessStringConvertible: KeyValuePersistible { 50 | // MARK: Public Static Interface 51 | 52 | public static var persistentKeyValueRepresentation: some PersistentKeyValueRepresentation { 53 | LosslessStringConvertiblePersistentKeyValueRepresentation() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Scaffolding/Custom Persistible Types/CustomPersistibleType+Proxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomPersistibleType+Proxy.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 10/30/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | 11 | extension CustomPersistibleType { 12 | public typealias Proxy = Date 13 | } 14 | 15 | // MARK: - KeyValuePersistible Extension 16 | 17 | extension CustomPersistibleType.Proxy: KeyValuePersistible { 18 | // MARK: Public Static Interface 19 | 20 | @inlinable 21 | public static var persistentKeyValueRepresentation: some PersistentKeyValueRepresentation { 22 | ProxyPersistentKeyValueRepresentation( 23 | to: \.timeIntervalSinceReferenceDate, 24 | from: Date.init(timeIntervalSinceReferenceDate:) 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Scaffolding/Custom Persistible Types/CustomPersistibleType+RawRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomPersistibleType.RawRepresentable.swift 3 | // PersistentKeyValueKitTests 4 | // 5 | // Created by Kyle Hughes on 4/17/22. 6 | // 7 | 8 | import PersistentKeyValueKit 9 | 10 | extension CustomPersistibleType { 11 | public enum RawRepresentable: String, Swift.Codable, Equatable, Sendable { 12 | case caseOne 13 | case caseTwo = "CASE_TWO" 14 | } 15 | } 16 | 17 | // MARK: - KeyValuePersistible Extension 18 | 19 | extension CustomPersistibleType.RawRepresentable: KeyValuePersistible { 20 | // MARK: Public Static Interface 21 | 22 | @inlinable 23 | public static var persistentKeyValueRepresentation: some PersistentKeyValueRepresentation { 24 | RawRepresentablePersistentKeyValueRepresentation() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Scaffolding/Custom Persistible Types/CustomPersistibleType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomPersistibleType.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 10/27/24. 6 | // 7 | 8 | public enum CustomPersistibleType { 9 | // NO-OP 10 | } 11 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Scaffolding/Mocks/MockNSUbiquitousKeyValueStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockNSUbiquitousKeyValueStore.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/28/24. 6 | // 7 | 8 | import Foundation 9 | 10 | @testable import PersistentKeyValueKit 11 | 12 | /// A subclass of `NSUbiquitousKeyValueStore` that overrides the implementations of all initializers, functions, 13 | /// and symbols that are used by this framework. 14 | /// 15 | /// We prefer to test actual implementations but `NSUbiquitousKeyValueStore` requires iCloud entitlements and we 16 | /// cannot instrument that for unit tests. We make a best-effort attempt to reflect the important details of the 17 | /// underlying implementation. 18 | @available(watchOS 9.0, *) 19 | public class MockUbiquitousKeyValueStore: NSUbiquitousKeyValueStore { 20 | public var storage: [String: Any] 21 | public var synchronizeReturnValue: Bool 22 | 23 | // MARK: Public Initialization 24 | 25 | override public init() { 26 | storage = [:] 27 | synchronizeReturnValue = true 28 | } 29 | 30 | // MARK: Constants 31 | 32 | nonisolated(unsafe) public static let mockDefault = MockUbiquitousKeyValueStore() 33 | 34 | public override class var `default`: NSUbiquitousKeyValueStore { 35 | mockDefault 36 | } 37 | 38 | // MARK: Getting Values 39 | 40 | public override func object(forKey aKey: String) -> Any? { 41 | storage[aKey] 42 | } 43 | 44 | public override func string(forKey aKey: String) -> String? { 45 | storage[aKey] as? String 46 | } 47 | 48 | public override func array(forKey aKey: String) -> [Any]? { 49 | storage[aKey] as? [Any] 50 | } 51 | 52 | public override func dictionary(forKey aKey: String) -> [String: Any]? { 53 | storage[aKey] as? [String: Any] 54 | } 55 | 56 | public override func data(forKey aKey: String) -> Data? { 57 | storage[aKey] as? Data 58 | } 59 | 60 | public override func longLong(forKey aKey: String) -> Int64 { 61 | storage[aKey] as? Int64 ?? 0 62 | } 63 | 64 | public override func double(forKey aKey: String) -> Double { 65 | storage[aKey] as? Double ?? 0.0 66 | } 67 | 68 | public override func bool(forKey aKey: String) -> Bool { 69 | storage[aKey] as? Bool ?? false 70 | } 71 | 72 | // MARK: Setting Values 73 | 74 | public override func set(_ anObject: Any?, forKey aKey: String) { 75 | storage[aKey] = anObject 76 | 77 | Self.postInternalChangeNotification(for: aKey, from: self) 78 | } 79 | 80 | public override func set(_ aString: String?, forKey aKey: String) { 81 | storage[aKey] = aString 82 | 83 | Self.postInternalChangeNotification(for: aKey, from: self) 84 | } 85 | 86 | public override func set(_ aData: Data?, forKey aKey: String) { 87 | storage[aKey] = aData 88 | 89 | Self.postInternalChangeNotification(for: aKey, from: self) 90 | } 91 | 92 | public override func set(_ anArray: [Any]?, forKey aKey: String) { 93 | storage[aKey] = anArray 94 | 95 | Self.postInternalChangeNotification(for: aKey, from: self) 96 | } 97 | 98 | public override func set(_ aDictionary: [String: Any]?, forKey aKey: String) { 99 | storage[aKey] = aDictionary 100 | 101 | Self.postInternalChangeNotification(for: aKey, from: self) 102 | } 103 | 104 | public override func set(_ value: Int64, forKey aKey: String) { 105 | storage[aKey] = value 106 | 107 | Self.postInternalChangeNotification(for: aKey, from: self) 108 | } 109 | 110 | public override func set(_ value: Double, forKey aKey: String) { 111 | storage[aKey] = value 112 | 113 | Self.postInternalChangeNotification(for: aKey, from: self) 114 | } 115 | 116 | public override func set(_ value: Bool, forKey aKey: String) { 117 | storage[aKey] = value 118 | 119 | Self.postInternalChangeNotification(for: aKey, from: self) 120 | } 121 | 122 | // MARK: Explicitly Synchronizing In-Memory Key-Value Data to Disk 123 | 124 | public override func synchronize() -> Bool { 125 | synchronizeReturnValue 126 | } 127 | 128 | // MARK: Removing Keys 129 | 130 | public override func removeObject(forKey aKey: String) { 131 | storage.removeValue(forKey: aKey) 132 | 133 | Self.postInternalChangeNotification(for: aKey, from: self) 134 | } 135 | 136 | // MARK: Retrieving the Current Keys and Values 137 | 138 | public override var dictionaryRepresentation: [String: Any] { 139 | storage 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Key-Value Persistible/AbstractPrimitiveKeyValuePersistibleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AbstractPrimitiveKeyValuePersistibleTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 9/30/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | import XCTest 11 | 12 | public class AbstractPrimitiveKeyValuePersistibleTests: 13 | AbstractKeyValuePersistibleTests 14 | where 15 | Target: Equatable & KeyValuePersistible 16 | { 17 | // MARK: Public Abstract Interface 18 | 19 | public var targets: [Target] { 20 | fatalError("`targets` needs to be implemented in a concrete subclass.") 21 | } 22 | 23 | // MARK: AbstractKeyValuePersistibleTests Implementation 24 | 25 | override public var expectations: [Expectation] { 26 | targets.map { Expectation($0) } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Key-Value Persistible/Extensions/Foundation/DataKeyValuePersistibleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataKeyValuePersistibleTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 9/30/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | import XCTest 11 | 12 | final class DataKeyValuePersistibleTests: AbstractPrimitiveKeyValuePersistibleTests { 13 | // MARK: AbstractPrimitiveKeyValuePersistibleTests Implementation 14 | 15 | override var targets: [Data] { 16 | [ 17 | Data(), 18 | "Hello, World!".data(using: .utf8)!, 19 | Data([0, 1, 2, 3, 4, 5]), 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Key-Value Persistible/Extensions/Foundation/DateKeyValuePersistibleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateKeyValuePersistibleTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 9/30/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | import XCTest 11 | 12 | final class DateKeyValuePersistibleTests: AbstractKeyValuePersistibleTests< 13 | Date, 14 | TimeInterval, 15 | TimeInterval, 16 | TimeInterval 17 | > { 18 | // MARK: AbstractKeyValuePersistibleTests Implementation 19 | 20 | override var expectations: [Expectation] { 21 | [ 22 | Expectation( 23 | target: Date(timeIntervalSinceReferenceDate: 1_000_000), 24 | propertyListRepresentation: \.timeIntervalSinceReferenceDate, 25 | ubiquitousStoreRepresentation: \.timeIntervalSinceReferenceDate, 26 | userDefaultsRepresentation: \.timeIntervalSinceReferenceDate 27 | ) 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Key-Value Persistible/Extensions/Foundation/URLKeyValuePersistibleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLKeyValuePersistibleTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 9/30/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | import XCTest 11 | 12 | final class URLKeyValuePersistibleTests: AbstractKeyValuePersistibleTests { 13 | // MARK: AbstractKeyValuePersistibleTests Implementation 14 | 15 | override public var expectations: [Expectation] { 16 | targets.map { 17 | Expectation( 18 | target: $0, 19 | propertyListRepresentation: \.absoluteString, 20 | ubiquitousStoreRepresentation: \.self, 21 | userDefaultsRepresentation: \.self 22 | ) 23 | } 24 | } 25 | 26 | // MARK: Instance Implementation 27 | 28 | var targets: [URL] { 29 | [ 30 | URL(string: "https://kylehugh.es")! 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Key-Value Persistible/Extensions/Standard Library/ArrayOfPrimitivesKeyValuePersistibleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayOfPrimitivesKeyValuePersistibleTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/27/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | import XCTest 11 | 12 | final class ArrayOfPrimitivesKeyValuePersistibleTests: 13 | AbstractPrimitiveKeyValuePersistibleTests> 14 | { 15 | // MARK: AbstractPrimitiveKeyValuePersistibleTests Implementation 16 | 17 | override var targets: [[Bool]] { 18 | [ 19 | [ 20 | false, 21 | true, 22 | ] 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Key-Value Persistible/Extensions/Standard Library/ArrayOfProxiesKeyValuePersistibleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayOfProxiesKeyValuePersistibleTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/27/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | import XCTest 11 | 12 | final class ArrayOfProxiesKeyValuePersistibleTests: 13 | AbstractKeyValuePersistibleTests< 14 | Array, 15 | Array, 16 | Array, 17 | Array 18 | > 19 | { 20 | // MARK: AbstractKeyValuePersistibleTests Implementation 21 | 22 | override var expectations: [Expectation] { 23 | [ 24 | [ 25 | .distantFuture, 26 | .distantPast, 27 | ] 28 | ].map { 29 | Expectation( 30 | target: $0, 31 | propertyListRepresentation: { 32 | $0.map(\.timeIntervalSinceReferenceDate) 33 | }, 34 | ubiquitousStoreRepresentation: { 35 | $0.map(\.timeIntervalSinceReferenceDate) 36 | }, 37 | userDefaultsRepresentation: { 38 | $0.map(\.timeIntervalSinceReferenceDate) 39 | } 40 | ) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Key-Value Persistible/Extensions/Standard Library/BoolKeyValuePersistibleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoolKeyValuePersistibleTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 5/11/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | import XCTest 11 | 12 | final class BoolKeyValuePersistibleTests: AbstractPrimitiveKeyValuePersistibleTests { 13 | // MARK: AbstractPrimitiveKeyValuePersistibleTests Implementation 14 | 15 | override var targets: [Bool] { 16 | [ 17 | false, 18 | true, 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Key-Value Persistible/Extensions/Standard Library/DictionaryOfPrimitivesKeyValuePersistibleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DictionaryOfPrimitivesKeyValuePersistibleTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 5/11/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | import XCTest 11 | 12 | final class DictionaryOfPrimitivesKeyValuePersistibleTests: 13 | AbstractPrimitiveKeyValuePersistibleTests> 14 | { 15 | // MARK: AbstractPrimitiveKeyValuePersistibleTests Implementation 16 | 17 | override var targets: [[String: String]] { 18 | [ 19 | [ 20 | "distantFuture": "A long time from now…", 21 | "distantPast": "A long time ago…", 22 | ] 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Key-Value Persistible/Extensions/Standard Library/DictionaryOfProxiesKeyValuePersistibleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DictionaryOfProxiesKeyValuePersistibleTests 2.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/27/24. 6 | // 7 | 8 | 9 | import Foundation 10 | import PersistentKeyValueKit 11 | import XCTest 12 | 13 | final class DictionaryOfProxiesKeyValuePersistibleTests: 14 | AbstractKeyValuePersistibleTests< 15 | Dictionary, 16 | Dictionary, 17 | Dictionary, 18 | Dictionary 19 | > 20 | { 21 | // MARK: AbstractKeyValuePersistibleTests Implementation 22 | 23 | override var expectations: [Expectation] { 24 | [ 25 | [ 26 | "distantFuture": .distantFuture, 27 | "distantPast": .distantPast, 28 | ] 29 | ].map { 30 | Expectation( 31 | target: $0, 32 | propertyListRepresentation: { 33 | $0.mapValues(\.timeIntervalSinceReferenceDate) 34 | }, 35 | ubiquitousStoreRepresentation: { 36 | $0.mapValues(\.timeIntervalSinceReferenceDate) 37 | }, 38 | userDefaultsRepresentation: { 39 | $0.mapValues(\.timeIntervalSinceReferenceDate) 40 | } 41 | ) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Key-Value Persistible/Extensions/Standard Library/DoubleKeyValuePersistibleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DoubleKeyValuePersistibleTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/29/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | import XCTest 11 | 12 | final class DoubleKeyValuePersistibleTests: AbstractPrimitiveKeyValuePersistibleTests { 13 | // MARK: AbstractPrimitiveKeyValuePersistibleTests Implementation 14 | 15 | override var targets: [Double] { 16 | [ 17 | .greatestFiniteMagnitude, 18 | .leastNonzeroMagnitude, 19 | .infinity, 20 | .zero, 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Key-Value Persistible/Extensions/Standard Library/FloatKeyValuePersistibleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FloatKeyValuePersistibleTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/29/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | import XCTest 11 | 12 | final class FloatKeyValuePersistibleTests: AbstractPrimitiveKeyValuePersistibleTests { 13 | // MARK: AbstractPrimitiveKeyValuePersistibleTests Implementation 14 | 15 | override var targets: [Float] { 16 | [ 17 | .greatestFiniteMagnitude, 18 | .leastNonzeroMagnitude, 19 | .infinity, 20 | .zero, 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Key-Value Persistible/Extensions/Standard Library/IntKeyValuePersistibleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntKeyValuePersistibleTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/29/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | import XCTest 11 | 12 | final class IntKeyValuePersistibleTests: AbstractPrimitiveKeyValuePersistibleTests { 13 | // MARK: AbstractPrimitiveKeyValuePersistibleTests Implementation 14 | 15 | override var targets: [Int] { 16 | [ 17 | .max, 18 | .min, 19 | .zero, 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Key-Value Persistible/Extensions/Standard Library/OptionalKeyValuePersistibleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionalKeyValuePersistibleTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 9/30/24. 6 | // 7 | 8 | // NO-OP: `Optional+KeyValuePersistible` is exhaustively tested for every type in the abstract test class. 9 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Key-Value Persistible/Extensions/Standard Library/StringKeyValuePersistibleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringKeyValuePersistibleTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 5/11/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | import XCTest 11 | 12 | final class StringKeyValuePersistibleTests: AbstractPrimitiveKeyValuePersistibleTests { 13 | // MARK: AbstractPrimitiveKeyValuePersistibleTests Implementation 14 | 15 | override var targets: [String] { 16 | [ 17 | String(), 18 | "Created by Kyle Hughes on 5/11/24.", 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Representation/Implementations/LosslessStringConvertiblePersistentKeyValueRepresentationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LosslessStringConvertiblePersistentKeyValueRepresentationTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 10/15/24. 6 | // 7 | 8 | import PersistentKeyValueKit 9 | import XCTest 10 | 11 | final class LosslessStringConvertiblePersistentKeyValueRepresentationTests: XCTestCase {} 12 | 13 | // MARK: - UserDefaults Tests 14 | 15 | extension LosslessStringConvertiblePersistentKeyValueRepresentationTests { 16 | // MARK: Tests 17 | 18 | func test_from() { 19 | let value = "1337" 20 | let representation = LosslessStringConvertiblePersistentKeyValueRepresentation() 21 | 22 | XCTAssertEqual(1337, representation.from(value)) 23 | } 24 | 25 | func test_to() { 26 | let value = 1337 27 | let representation = LosslessStringConvertiblePersistentKeyValueRepresentation() 28 | 29 | XCTAssertEqual("1337", representation.to(value)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Representation/Implementations/ProxyPersistentKeyValueRepresentationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProxyPersistentKeyValueRepresentationTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 10/4/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | import XCTest 11 | 12 | final class ProxyPersistentKeyValueRepresentationTests: XCTestCase {} 13 | 14 | // MARK: Initialization Tests 15 | 16 | extension ProxyPersistentKeyValueRepresentationTests { 17 | // MARK: Tests 18 | 19 | @MainActor 20 | func test_init() { 21 | let representation = ProxyPersistentKeyValueRepresentation( 22 | to: { String($0) }, 23 | from: { Int($0) ?? 0 } 24 | ) 25 | 26 | XCTAssertEqual(representation.to(42), "42") 27 | XCTAssertEqual(representation.from("42"), 42) 28 | XCTAssertEqual(representation.from("invalid"), 0) 29 | } 30 | 31 | @MainActor 32 | func test_init_otherRepresentation() { 33 | // October 4, 2021, 00:00:00 UTC 34 | let timeInterval: TimeInterval = 1633305600 35 | let date = Date(timeIntervalSince1970: timeInterval) 36 | 37 | let representation = ProxyPersistentKeyValueRepresentation( 38 | other: ProxyPersistentKeyValueRepresentation( 39 | to: { String($0) }, 40 | from: { TimeInterval($0) ?? 0 } 41 | ), 42 | to: \.timeIntervalSince1970, 43 | from: Date.init(timeIntervalSince1970:) 44 | ) 45 | 46 | XCTAssertEqual(representation.from("invalid"), Date(timeIntervalSince1970: 0)) 47 | XCTAssertEqual(representation.to(date), "1633305600.0") 48 | XCTAssertEqual( 49 | representation.from("1633305600.0")?.timeIntervalSince1970 ?? 0, 50 | date.timeIntervalSince1970, 51 | accuracy: 0.001 52 | ) 53 | } 54 | 55 | @MainActor 56 | func test_init_optional_to() { 57 | let representation = ProxyPersistentKeyValueRepresentation( 58 | to: { $0 > 0 ? String($0) : nil }, 59 | from: { Int($0) ?? 0 } 60 | ) 61 | 62 | XCTAssertEqual(representation.to(42), "42") 63 | XCTAssertNil(representation.to(-5)) 64 | XCTAssertEqual(representation.from("42"), 42) 65 | XCTAssertEqual(representation.from("invalid"), 0) 66 | } 67 | 68 | @MainActor 69 | func test_init_optional_from() { 70 | let representation = ProxyPersistentKeyValueRepresentation( 71 | to: { String($0) }, 72 | from: { Int($0) } 73 | ) 74 | 75 | XCTAssertEqual(representation.to(42), "42") 76 | XCTAssertEqual(representation.from("42"), 42) 77 | XCTAssertNil(representation.from("invalid")) 78 | } 79 | 80 | @MainActor 81 | func test_init_optional_both() { 82 | let representation = ProxyPersistentKeyValueRepresentation( 83 | to: { $0 > 0 ? String($0) : nil }, 84 | from: { Int($0) } 85 | ) 86 | 87 | XCTAssertEqual(representation.to(42), "42") 88 | XCTAssertNil(representation.to(-5)) 89 | XCTAssertEqual(representation.from("42"), 42) 90 | XCTAssertNil(representation.from("invalid")) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/AbstractPersistentKeyValueStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AbstractPersistentKeyValueStoreTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/28/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | import XCTest 11 | 12 | #if canImport(UIKit) 13 | import UIKit 14 | #endif 15 | 16 | public class AbstractPersistentKeyValueStoreTests: XCTestCase where Target: PersistentKeyValueStore { 17 | // MARK: Public Abstract Interface 18 | 19 | public var target: Target { 20 | fatalError("`target` needs to be implemented in a concrete subclass.") 21 | } 22 | 23 | // MARK: Public Class Interface 24 | 25 | public class var isAbstractTestCase: Bool { 26 | self == AbstractPersistentKeyValueStoreTests.self 27 | } 28 | 29 | // MARK: XCTestCase Implementation 30 | 31 | override class public var defaultTestSuite: XCTestSuite { 32 | guard isAbstractTestCase else { 33 | return super.defaultTestSuite 34 | } 35 | 36 | return XCTestSuite(name: "Empty Suite for \(Self.self)") 37 | } 38 | 39 | // MARK: Tests for Subscripts 40 | 41 | @MainActor 42 | func test_subscript_defaultValue() { 43 | let defaultValue = "defaultValue" 44 | let key = PersistentKey("key", defaultValue: defaultValue) 45 | 46 | XCTAssertEqual(target[key], defaultValue) 47 | } 48 | 49 | @MainActor 50 | func test_subscript_storedValue() { 51 | let defaultValue = "defaultValue" 52 | let storedValue = "storedValue" 53 | let key = PersistentKey("key", defaultValue: defaultValue) 54 | 55 | target.set(key, to: storedValue) 56 | 57 | XCTAssertEqual(target[key], storedValue) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/InMemoryPersistentKeyValueStore/InMemoryPersistentKeyValueStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InMemoryPersistentKeyValueStoreTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/28/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | import XCTest 11 | 12 | final class InMemoryPersistentKeyValueStoreTests: AbstractPersistentKeyValueStoreTests { 13 | private let storage = InMemoryPersistentKeyValueStore() 14 | 15 | // MARK: AbstractPersistentKeyValueStoreTests Implementation 16 | 17 | override var target: InMemoryPersistentKeyValueStore { 18 | storage 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/NSUbiquitousKeyValueStore/AbstractNSUbiquitousKeyValueStoreTypeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AbstractNSUbiquitousKeyValueStoreTypeTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/17/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | import XCTest 11 | 12 | @available(watchOS 9.0, *) 13 | public class AbstractNSUbiquitousKeyValueStoreTypeTests: 14 | AbstractPersistentKeyValueStoreTypeTests 15 | where 16 | Target: Equatable & KeyValuePersistible & Sendable 17 | { 18 | private let _store = MockUbiquitousKeyValueStore() 19 | 20 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 21 | 22 | override public var store: NSUbiquitousKeyValueStore { 23 | _store 24 | } 25 | 26 | // MARK: XCTestCase Implementation 27 | 28 | override public func tearDown() { 29 | store.dictionaryRepresentation.keys.forEach(store.removeObject) 30 | } 31 | 32 | // MARK: Public Class Interface 33 | 34 | override public class var isAbstractTestCase: Bool { 35 | self == AbstractNSUbiquitousKeyValueStoreTypeTests.self 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/NSUbiquitousKeyValueStore/NSUbiquitousKeyValueStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSUbiquitousKeyValueStoreTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 4/28/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | import XCTest 11 | 12 | @available(watchOS 9.0, *) 13 | final class NSUbiquitousKeyValueStoreTests: AbstractPersistentKeyValueStoreTests { 14 | private let storage: NSUbiquitousKeyValueStore = MockUbiquitousKeyValueStore() 15 | 16 | // MARK: AbstractPersistentKeyValueStoreTests Implementation 17 | 18 | override var target: NSUbiquitousKeyValueStore { 19 | storage 20 | } 21 | 22 | // MARK: XCTestCase Implementation 23 | 24 | override public func tearDown() { 25 | storage.dictionaryRepresentation.keys.forEach(storage.removeObject) 26 | } 27 | 28 | // MARK: Static Accessor Tests 29 | 30 | @MainActor 31 | func test_staticAccessor() { 32 | XCTAssert(NSUbiquitousKeyValueStore.default === NSUbiquitousKeyValueStore.ubiquitous) 33 | } 34 | 35 | // MARK: PersistentKeyValueStore Tests 36 | 37 | @MainActor 38 | func test_persistentKeyValueStore_remove() { 39 | let keyID = "key" 40 | let defaultValue = "defaultValue" 41 | let storedValue = "storedValue" 42 | 43 | let key = PersistentKey(keyID, defaultValue: defaultValue) 44 | 45 | storage.set(storedValue, forKey: key.id) 46 | storage.remove(key) 47 | 48 | XCTAssertNil(storage.dictionaryRepresentation[keyID]) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/NSUbiquitousKeyValueStore/Type Tests/Custom/CodableNSUbiquitousKeyValueStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodableNSUbiquitousKeyValueStoreTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/3/24. 6 | // 7 | 8 | @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) 9 | final class CodableNSUbiquitousKeyValueStoreTests: 10 | AbstractNSUbiquitousKeyValueStoreTypeTests 11 | { 12 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 13 | 14 | override var testValues: [CustomPersistibleType.Codable] { 15 | [ 16 | .large, 17 | .small, 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/NSUbiquitousKeyValueStore/Type Tests/Custom/LosslessStringConvertibleNSUbiquitousKeyValueStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LosslessStringConvertibleNSUbiquitousKeyValueStoreTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/3/24. 6 | // 7 | 8 | final class LosslessStringConvertibleNSUbiquitousKeyValueStoreTests: 9 | AbstractUserDefaultsTypeTests 10 | { 11 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 12 | 13 | override var testValues: [CustomPersistibleType.LosslessStringConvertible] { 14 | [ 15 | "C", 16 | "😂", 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/NSUbiquitousKeyValueStore/Type Tests/Custom/ProxyNSUbiquitousKeyValueStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProxyNSUbiquitousKeyValueStoreTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/3/24. 6 | // 7 | 8 | @available(watchOS 9.0, *) 9 | final class ProxyNSUbiquitousKeyValueStoreTests: 10 | AbstractNSUbiquitousKeyValueStoreTypeTests 11 | { 12 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 13 | 14 | override var testValues: [CustomPersistibleType.Proxy] { 15 | [ 16 | .distantFuture, 17 | .distantPast, 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/NSUbiquitousKeyValueStore/Type Tests/Custom/RawRepresentableNSUbiquitousKeyValueStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RawRepresentableNSUbiquitousKeyValueStoreTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/3/24. 6 | // 7 | 8 | final class RawRepresentableNSUbiquitousKeyValueStoreTests: 9 | AbstractUserDefaultsTypeTests 10 | { 11 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 12 | 13 | override var testValues: [CustomPersistibleType.RawRepresentable] { 14 | [ 15 | .caseOne, 16 | .caseTwo, 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/NSUbiquitousKeyValueStore/Type Tests/Foundation/DataNSUbiquitousKeyValueStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataNSUbiquitousKeyValueStoreTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/24/24. 6 | // 7 | 8 | import Foundation 9 | 10 | @available(watchOS 9.0, *) 11 | final class DataNSUbiquitousKeyValueStoreTests: AbstractNSUbiquitousKeyValueStoreTypeTests { 12 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 13 | 14 | override var testValues: [Data] { 15 | [ 16 | Data(), 17 | Data([0xDE, 0xAD, 0xBE, 0xEF]), 18 | Data(repeating: 0xFF, count: 1024), 19 | "Hello, World!".data(using: .utf8)!, 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/NSUbiquitousKeyValueStore/Type Tests/Foundation/URLNSUbiquitousKeyValueStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLNSUbiquitousKeyValueStoreTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/24/24. 6 | // 7 | 8 | import Foundation 9 | 10 | @available(watchOS 9.0, *) 11 | final class URLNSUbiquitousKeyValueStoreTests: AbstractNSUbiquitousKeyValueStoreTypeTests { 12 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 13 | 14 | override var testValues: [URL] { 15 | [ 16 | URL(string: "https://kylehugh.es")!, 17 | URL(string: "https://example.com/path?query=value#fragment")!, 18 | URL(string: "file:///path/to/file.txt")!, 19 | URL(string: "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==")!, 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/NSUbiquitousKeyValueStore/Type Tests/Standard Library/ArrayNSUbiquitousKeyValueStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayNSUbiquitousKeyValueStoreTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/17/24. 6 | // 7 | 8 | /// - Important: While each type is exhaustively tested in arrays in the abstract test class, it is still useful to 9 | /// test nested arrays. Here, we do not go for complete exhaustion – every type as nested arrays – because we could 10 | /// then justify an infinite number of array-of-array-of… tests. 11 | @available(watchOS 9.0, *) 12 | final class ArrayNSUbiquitousKeyValueStoreTests: AbstractNSUbiquitousKeyValueStoreTypeTests> { 13 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 14 | 15 | override var testValues: [[Float]] { 16 | [ 17 | [ 18 | .greatestFiniteMagnitude, 19 | .leastNonzeroMagnitude, 20 | .infinity, 21 | .zero, 22 | ] 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/NSUbiquitousKeyValueStore/Type Tests/Standard Library/BoolNSUbiquitousKeyValueStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoolNSUbiquitousKeyValueStoreTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/17/24. 6 | // 7 | 8 | @available(watchOS 9.0, *) 9 | final class BoolNSUbiquitousKeyValueStoreTests: AbstractNSUbiquitousKeyValueStoreTypeTests { 10 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 11 | 12 | override var testValues: [Bool] { 13 | [ 14 | true, 15 | false 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/NSUbiquitousKeyValueStore/Type Tests/Standard Library/DictionaryNSUbiquitousKeyValueStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DictionaryNSUbiquitousKeyValueStoreTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/17/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | 11 | /// - Important: While each type is exhaustively tested in dictionaries in the abstract test class, it is still useful 12 | /// to test nested dictionaries. Here, we do not go for complete exhaustion – every type as nested arrays – because we 13 | /// could then justify an infinite number of dictionary-of-dictionary-of… tests. 14 | @available(watchOS 9.0, *) 15 | final class DictionaryNSUbiquitousKeyValueStoreTests: 16 | AbstractNSUbiquitousKeyValueStoreTypeTests> 17 | { 18 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 19 | 20 | override var testValues: [[String: CustomPersistibleType.Proxy]] { 21 | [ 22 | [ 23 | "distantFuture": .distantFuture, 24 | "distantPast": .distantPast, 25 | "referenceDate": Date(timeIntervalSinceReferenceDate: 0), 26 | ] 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/NSUbiquitousKeyValueStore/Type Tests/Standard Library/DoubleNSUbiquitousKeyValueStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DoubleNSUbiquitousKeyValueStoreTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/17/24. 6 | // 7 | 8 | @available(watchOS 9.0, *) 9 | final class DoubleNSUbiquitousKeyValueStoreTests: AbstractNSUbiquitousKeyValueStoreTypeTests { 10 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 11 | 12 | override var testValues: [Double] { 13 | [ 14 | .greatestFiniteMagnitude, 15 | .leastNonzeroMagnitude, 16 | .infinity, 17 | .zero, 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/NSUbiquitousKeyValueStore/Type Tests/Standard Library/FloatNSUbiquitousKeyValueStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FloatNSUbiquitousKeyValueStoreTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/17/24. 6 | // 7 | 8 | @available(watchOS 9.0, *) 9 | final class FloatNSUbiquitousKeyValueStoreTests: AbstractNSUbiquitousKeyValueStoreTypeTests { 10 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 11 | 12 | override var testValues: [Float] { 13 | [ 14 | .greatestFiniteMagnitude, 15 | .leastNonzeroMagnitude, 16 | .infinity, 17 | .zero, 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/NSUbiquitousKeyValueStore/Type Tests/Standard Library/IntNSUbiquitousKeyValueStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntNSUbiquitousKeyValueStoreTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/17/24. 6 | // 7 | 8 | @available(watchOS 9.0, *) 9 | final class IntNSUbiquitousKeyValueStoreTests: AbstractNSUbiquitousKeyValueStoreTypeTests { 10 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 11 | 12 | override var testValues: [Int] { 13 | [ 14 | .max, 15 | .min, 16 | .zero, 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/NSUbiquitousKeyValueStore/Type Tests/Standard Library/OptionalNSUbiquitousKeyValueStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionalNSUbiquitousKeyValueStoreTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/17/24. 6 | // 7 | 8 | // NO-OP: The optional variant is exhaustively tested for every type in the abstract test class. 9 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/NSUbiquitousKeyValueStore/Type Tests/Standard Library/StringNSUbiquitousKeyValueStoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringNSUbiquitousKeyValueStoreTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/17/24. 6 | // 7 | 8 | @available(watchOS 9.0, *) 9 | final class StringNSUbiquitousKeyValueStoreTests: AbstractNSUbiquitousKeyValueStoreTypeTests { 10 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 11 | 12 | override var testValues: [String] { 13 | [ 14 | String(), 15 | "Created by Kyle Hughes on 5/11/24.", 16 | String(repeating: "A", count: 1_000), 17 | "Hello, 世界! 🌍", 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/UserDefaults/AbstractUserDefaultsTypeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AbstractUserDefaultsTypeTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/3/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | import XCTest 11 | 12 | public class AbstractUserDefaultsTypeTests: AbstractPersistentKeyValueStoreTypeTests 13 | where 14 | Target: Equatable & KeyValuePersistible & Sendable 15 | { 16 | private let _store = UserDefaults() 17 | 18 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 19 | 20 | override public var store: UserDefaults { 21 | _store 22 | } 23 | 24 | // MARK: XCTestCase Implementation 25 | 26 | override public func tearDown() { 27 | store.dictionaryRepresentation().keys.forEach(store.removeObject) 28 | store.setVolatileDomain([:], forName: UserDefaults.registrationDomain) 29 | } 30 | 31 | // MARK: Public Class Interface 32 | 33 | override public class var isAbstractTestCase: Bool { 34 | self == AbstractUserDefaultsTypeTests.self 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/UserDefaults/Type Tests/Custom/CodableUserDefaultsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodableUserDefaultsTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/3/24. 6 | // 7 | 8 | @available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) 9 | final class CodableUserDefaultsTests: AbstractUserDefaultsTypeTests { 10 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 11 | 12 | override var testValues: [CustomPersistibleType.Codable] { 13 | [ 14 | .large, 15 | .small, 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/UserDefaults/Type Tests/Custom/LosslessStringConvertibleUserDefaultsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LosslessStringConvertibleUserDefaultsTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/3/24. 6 | // 7 | 8 | final class LosslessStringConvertibleUserDefaultsTests: 9 | AbstractUserDefaultsTypeTests 10 | { 11 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 12 | 13 | override var testValues: [CustomPersistibleType.LosslessStringConvertible] { 14 | [ 15 | "C", 16 | "😂", 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/UserDefaults/Type Tests/Custom/ProxyUserDefaultsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProxyUserDefaultsTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/3/24. 6 | // 7 | 8 | final class ProxyUserDefaultsTests: AbstractUserDefaultsTypeTests { 9 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 10 | 11 | override var testValues: [CustomPersistibleType.Proxy] { 12 | [ 13 | .distantFuture, 14 | .distantPast, 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/UserDefaults/Type Tests/Custom/RawRepresentableUserDefaultsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RawRepresentableUserDefaultsTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/3/24. 6 | // 7 | 8 | final class RawRepresentableUserDefaultsTests: 9 | AbstractUserDefaultsTypeTests 10 | { 11 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 12 | 13 | override var testValues: [CustomPersistibleType.RawRepresentable] { 14 | [ 15 | .caseOne, 16 | .caseTwo, 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/UserDefaults/Type Tests/Foundation/DataUserDefaultsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataUserDefaultsTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/3/24. 6 | // 7 | 8 | import Foundation 9 | 10 | final class DataUserDefaultsTests: AbstractUserDefaultsTypeTests { 11 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 12 | 13 | override var testValues: [Data] { 14 | [ 15 | Data(), 16 | Data([0xDE, 0xAD, 0xBE, 0xEF]), 17 | Data(repeating: 0xFF, count: 1024), 18 | "Hello, World!".data(using: .utf8)!, 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/UserDefaults/Type Tests/Foundation/URLUserDefaultsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLUserDefaultsTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/3/24. 6 | // 7 | 8 | import Foundation 9 | 10 | final class URLUserDefaultsTests: AbstractUserDefaultsTypeTests { 11 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 12 | 13 | override var testValues: [URL] { 14 | [ 15 | URL(string: "https://kylehugh.es")!, 16 | URL(string: "https://example.com/path?query=value#fragment")!, 17 | URL(string: "file:///path/to/file.txt")!, 18 | URL(string: "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==")!, 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/UserDefaults/Type Tests/Standard Library/ArrayUserDefaultsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayUserDefaultsTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/3/24. 6 | // 7 | 8 | /// - Important: While each type is exhaustively tested in arrays in the abstract test class, it is still useful to 9 | /// test nested arrays. Here, we do not go for complete exhaustion – every type as nested arrays – because we could 10 | /// then justify an infinite number of array-of-array-of… tests. 11 | final class ArrayUserDefaultsTests: AbstractUserDefaultsTypeTests> { 12 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 13 | 14 | override var testValues: [[Float]] { 15 | [ 16 | [ 17 | .greatestFiniteMagnitude, 18 | .leastNonzeroMagnitude, 19 | .infinity, 20 | .zero, 21 | ] 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/UserDefaults/Type Tests/Standard Library/BoolUserDefaultsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoolUserDefaultsTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/3/24. 6 | // 7 | 8 | final class BoolUserDefaultsTests: AbstractUserDefaultsTypeTests { 9 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 10 | 11 | override var testValues: [Bool] { 12 | [ 13 | true, 14 | false 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/UserDefaults/Type Tests/Standard Library/DictionaryUserDefaultsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DictionaryUserDefaultsTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/3/24. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | 11 | /// - Important: While each type is exhaustively tested in dictionaries in the abstract test class, it is still useful 12 | /// to test nested dictionaries. Here, we do not go for complete exhaustion – every type as nested arrays – because we 13 | /// could then justify an infinite number of dictionary-of-dictionary-of… tests. 14 | final class DictionaryUserDefaultsTests: 15 | AbstractUserDefaultsTypeTests> 16 | { 17 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 18 | 19 | override var testValues: [[String: CustomPersistibleType.Proxy]] { 20 | [ 21 | [ 22 | "distantFuture": .distantFuture, 23 | "distantPast": .distantPast, 24 | "referenceDate": Date(timeIntervalSinceReferenceDate: 0), 25 | ] 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/UserDefaults/Type Tests/Standard Library/DoubleUserDefaultsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DoubleUserDefaultsTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/3/24. 6 | // 7 | 8 | final class DoubleUserDefaultsTests: AbstractUserDefaultsTypeTests { 9 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 10 | 11 | override var testValues: [Double] { 12 | [ 13 | .greatestFiniteMagnitude, 14 | .leastNonzeroMagnitude, 15 | .infinity, 16 | .zero, 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/UserDefaults/Type Tests/Standard Library/FloatUserDefaultsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FloatUserDefaultsTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/3/24. 6 | // 7 | 8 | final class FloatUserDefaultsTests: AbstractUserDefaultsTypeTests { 9 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 10 | 11 | override var testValues: [Float] { 12 | [ 13 | .greatestFiniteMagnitude, 14 | .leastNonzeroMagnitude, 15 | .infinity, 16 | .zero, 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/UserDefaults/Type Tests/Standard Library/IntUserDefaultsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntUserDefaultsTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/3/24. 6 | // 7 | 8 | final class IntUserDefaultsTests: AbstractUserDefaultsTypeTests { 9 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 10 | 11 | override var testValues: [Int] { 12 | [ 13 | .max, 14 | .min, 15 | .zero, 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/UserDefaults/Type Tests/Standard Library/OptionalUserDefaultsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionalUserDefaultsTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/3/24. 6 | // 7 | 8 | // NO-OP: The optional variant is exhaustively tested for every type in the abstract test class. 9 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/UserDefaults/Type Tests/Standard Library/StringUserDefaultsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringUserDefaultsTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 11/3/24. 6 | // 7 | 8 | final class StringUserDefaultsTests: AbstractUserDefaultsTypeTests { 9 | // MARK: AbstractPersistentKeyValueStoreTypeTests Implementation 10 | 11 | override var testValues: [String] { 12 | [ 13 | String(), 14 | "Created by Kyle Hughes on 5/11/24.", 15 | String(repeating: "A", count: 1_000), 16 | "Hello, 世界! 🌍", 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key-Value Store/UserDefaults/UserDefaultsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 3/28/22. 6 | // 7 | 8 | import Foundation 9 | import PersistentKeyValueKit 10 | import XCTest 11 | 12 | final class UserDefaultsTests: AbstractPersistentKeyValueStoreTests { 13 | private let storage = UserDefaults.standard 14 | 15 | // MARK: AbstractPersistentKeyValueStoreTests Implementation 16 | 17 | override var target: UserDefaults { 18 | storage 19 | } 20 | 21 | // MARK: XCTestCase Implementation 22 | 23 | override public func tearDown() { 24 | storage.dictionaryRepresentation().keys.forEach(storage.removeObject) 25 | storage.setVolatileDomain([:], forName: UserDefaults.registrationDomain) 26 | } 27 | 28 | // MARK: Static Accessor Tests 29 | 30 | @MainActor 31 | func test_staticAccessor() { 32 | XCTAssert(UserDefaults.standard === UserDefaults.local) 33 | } 34 | 35 | // MARK: PersistentKeyValueStore Tests 36 | 37 | @MainActor 38 | func test_persistentKeyValueStore_registerDefaults_existingKeys() { 39 | let keyID = "stringKey" 40 | let defaultValue = "defaultString" 41 | let existingValue = "existingString" 42 | 43 | let key = PersistentKey(keyID, defaultValue: defaultValue) 44 | 45 | storage.set(existingValue, forKey: keyID) 46 | storage.register([key]) 47 | 48 | XCTAssertEqual(storage.string(forKey: keyID), existingValue) 49 | } 50 | 51 | @MainActor 52 | func test_persistentKeyValueStore_registerDefaults_nonexistingKeys() { 53 | let stringKeyID = "stringKey" 54 | let intKeyID = "intKey" 55 | let boolKeyID = "boolKey" 56 | 57 | let stringDefaultValue = "defaultString" 58 | let intDefaultValue = 42 59 | let boolDefaultValue = true 60 | 61 | let stringKey = PersistentKey(stringKeyID, defaultValue: stringDefaultValue) 62 | let intKey = PersistentKey(intKeyID, defaultValue: intDefaultValue) 63 | let boolKey = PersistentKey(boolKeyID, defaultValue: boolDefaultValue) 64 | 65 | storage.register([stringKey, intKey, boolKey]) 66 | 67 | XCTAssertEqual(storage.string(forKey: stringKeyID), stringDefaultValue) 68 | XCTAssertEqual(storage.integer(forKey: intKeyID), intDefaultValue) 69 | XCTAssertEqual(storage.bool(forKey: boolKeyID), boolDefaultValue) 70 | } 71 | 72 | @MainActor 73 | func test_persistentKeyValueStore_registerDefaults_unregistered() { 74 | let unregisteredKeyID = "unregisteredKey" 75 | 76 | XCTAssertNil(storage.string(forKey: unregisteredKeyID)) 77 | } 78 | 79 | @MainActor 80 | func test_persistentKeyValueStore_registerDefaults_variadic() { 81 | let stringKeyID = "stringKey" 82 | let intKeyID = "intKey" 83 | let boolKeyID = "boolKey" 84 | 85 | let stringDefaultValue = "defaultString" 86 | let intDefaultValue = 42 87 | let boolDefaultValue = true 88 | 89 | let stringKey = PersistentKey(stringKeyID, defaultValue: stringDefaultValue) 90 | let intKey = PersistentKey(intKeyID, defaultValue: intDefaultValue) 91 | let boolKey = PersistentKey(boolKeyID, defaultValue: boolDefaultValue) 92 | 93 | storage.register(stringKey, intKey, boolKey) 94 | 95 | XCTAssertEqual(storage.string(forKey: stringKeyID), stringDefaultValue) 96 | XCTAssertEqual(storage.integer(forKey: intKeyID), intDefaultValue) 97 | XCTAssertEqual(storage.bool(forKey: boolKeyID), boolDefaultValue) 98 | } 99 | 100 | @MainActor 101 | func test_persistentKeyValueStore_remove() { 102 | let keyID = "key" 103 | let defaultValue = "defaultValue" 104 | let storedValue = "storedValue" 105 | 106 | let key = PersistentKey(keyID, defaultValue: defaultValue) 107 | 108 | storage.set(storedValue, forKey: key.id) 109 | storage.remove(key) 110 | 111 | XCTAssertNil(storage.dictionaryRepresentation()[keyID]) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key/AbstractPersistentKeyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AbstractPersistentKeyTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 10/4/24. 6 | // 7 | 8 | import PersistentKeyValueKit 9 | import XCTest 10 | 11 | public class AbstractPersistentKeyTests: XCTestCase where Key: PersistentKeyProtocol, Key.Value: Equatable { 12 | public let userDefaults = UserDefaults() 13 | 14 | // MARK: Public Abstract Interface 15 | 16 | public var defaultValue: Key.Value { 17 | fatalError("`defaultValue` needs to be implemented in a concrete subclass.") 18 | } 19 | 20 | public var id: String { 21 | fatalError("`id` needs to be implemented in a concrete subclass.") 22 | } 23 | 24 | public var storedValue: Key.Value { 25 | fatalError("`storedValue` needs to be implemented in a concrete subclass.") 26 | } 27 | 28 | public var target: Key { 29 | fatalError("`target` needs to be implemented in a concrete subclass.") 30 | } 31 | 32 | // MARK: Public Class Interface 33 | 34 | public class var isAbstractTestCase: Bool { 35 | Self.self == AbstractPersistentKeyTests.self 36 | } 37 | 38 | // MARK: XCTestCase Implementation 39 | 40 | override public class var defaultTestSuite: XCTestSuite { 41 | guard isAbstractTestCase else { 42 | return super.defaultTestSuite 43 | } 44 | 45 | return XCTestSuite(name: "Empty Suite for \(Self.self)") 46 | } 47 | 48 | override public func tearDown() { 49 | userDefaults.dictionaryRepresentation().keys.forEach(userDefaults.removeObject) 50 | userDefaults.setVolatileDomain([:], forName: UserDefaults.registrationDomain) 51 | } 52 | 53 | // MARK: Initialization Tests 54 | 55 | @MainActor 56 | func test_init() { 57 | XCTAssertEqual(target.id, id) 58 | XCTAssertEqual(target.defaultValue, defaultValue) 59 | } 60 | 61 | // MARK: PersistentKeyProtocol Tests 62 | 63 | @available(watchOS 9.0, *) 64 | @MainActor 65 | func test_persistentKey_ubiquitousKeyValueStore_get_defaultValue() { 66 | let ubiquitousKeyValueStore: NSUbiquitousKeyValueStore = MockUbiquitousKeyValueStore() 67 | 68 | XCTAssertEqual(target.get(from: ubiquitousKeyValueStore), defaultValue) 69 | } 70 | 71 | @available(watchOS 9.0, *) 72 | @MainActor 73 | func test_persistentKey_ubiquitousKeyValueStore_get_storedValue() { 74 | let ubiquitousKeyValueStore: NSUbiquitousKeyValueStore = MockUbiquitousKeyValueStore() 75 | 76 | ubiquitousKeyValueStore.set(storedValue, forKey: id) 77 | 78 | XCTAssertEqual(target.get(from: ubiquitousKeyValueStore), storedValue) 79 | } 80 | 81 | @available(watchOS 9.0, *) 82 | @MainActor 83 | func test_persistentKey_ubiquitousKeyValueStore_remove_noValue() { 84 | let ubiquitousKeyValueStore: NSUbiquitousKeyValueStore = MockUbiquitousKeyValueStore() 85 | 86 | target.remove(from: ubiquitousKeyValueStore) 87 | 88 | XCTAssertNil(ubiquitousKeyValueStore.dictionaryRepresentation[id]) 89 | } 90 | 91 | @available(watchOS 9.0, *) 92 | @MainActor 93 | func test_persistentKey_ubiquitousKeyValueStore_remove_storedValue() { 94 | let ubiquitousKeyValueStore: NSUbiquitousKeyValueStore = MockUbiquitousKeyValueStore() 95 | 96 | ubiquitousKeyValueStore.set(storedValue, forKey: id) 97 | 98 | target.remove(from: ubiquitousKeyValueStore) 99 | 100 | XCTAssertNil(ubiquitousKeyValueStore.dictionaryRepresentation[id]) 101 | } 102 | 103 | @available(watchOS 9.0, *) 104 | @MainActor 105 | func test_persistentKey_ubiquitousKeyValueStore_set() { 106 | let ubiquitousKeyValueStore: NSUbiquitousKeyValueStore = MockUbiquitousKeyValueStore() 107 | 108 | target.set(to: storedValue, in: ubiquitousKeyValueStore) 109 | 110 | XCTAssertEqual(ubiquitousKeyValueStore.dictionaryRepresentation[id] as? Key.Value, storedValue) 111 | } 112 | 113 | @MainActor 114 | func test_persistentKey_userDefaults_get_defaultValue() { 115 | XCTAssertEqual(target.get(from: userDefaults), defaultValue) 116 | } 117 | 118 | @MainActor 119 | func test_persistentKey_userDefaults_get_storedValue() { 120 | userDefaults.set(storedValue, forKey: id) 121 | 122 | XCTAssertEqual(target.get(from: userDefaults), storedValue) 123 | } 124 | 125 | @MainActor 126 | func test_persistentKey_userDefaults_remove_noValue() { 127 | target.remove(from: userDefaults) 128 | 129 | XCTAssertNil(userDefaults.dictionaryRepresentation()[id]) 130 | } 131 | 132 | @MainActor 133 | func test_persistentKey_userDefaults_remove_storedValue() { 134 | userDefaults.set(storedValue, forKey: id) 135 | target.remove(from: userDefaults) 136 | 137 | XCTAssertNil(userDefaults.dictionaryRepresentation()[id]) 138 | } 139 | 140 | @MainActor 141 | func test_persistentKey_userDefaults_set() { 142 | target.set(to: storedValue, in: userDefaults) 143 | 144 | XCTAssertEqual(userDefaults.dictionaryRepresentation()[id] as? Key.Value, storedValue) 145 | } 146 | 147 | // MARK: Internal Instance Interface 148 | 149 | /// Skip the test if the environment is not iOS 16, or later, or equivalent. 150 | /// 151 | /// There is an issue with the implementation of tryCast across the Objective-C bridge when testing on 152 | /// iOS 15 from Xcode 16. Additionally, marking test cases as available <= iOS 16 is not working in the 153 | /// same environment. 154 | func skipIfNotiOS16OrLaterOrEquivalent() throws { 155 | guard #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) else { 156 | throw XCTSkip(">= iOS 16 is required for this test.") 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key/Implementations/PersistentDebugKeyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersistentDebugKeyTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 10/4/24. 6 | // 7 | 8 | import PersistentKeyValueKit 9 | import XCTest 10 | 11 | final class PersistentDebugKeyTests: AbstractPersistentKeyTests> { 12 | // MARK: AbstractPersistenKeyTests Implementation 13 | 14 | override var defaultValue: String { 15 | "defaultValue" 16 | } 17 | 18 | override var id: String { 19 | "key" 20 | } 21 | 22 | override var storedValue: String { 23 | "storedValue" 24 | } 25 | 26 | override var target: PersistentDebugKey { 27 | PersistentDebugKey(id, defaultValue: defaultValue) 28 | } 29 | 30 | // MARK: Initialization Tests 31 | 32 | @MainActor 33 | func test_init_otherRepresentation() throws { 34 | try skipIfNotiOS16OrLaterOrEquivalent() 35 | 36 | let representation = ReferenceProxyPersistentKeyValueRepresentation( 37 | serializing: \.self, 38 | deserializing: \.self 39 | ) 40 | 41 | let key = PersistentDebugKey(id, defaultValue: defaultValue, representation: representation) 42 | 43 | XCTAssertEqual(key.id, id) 44 | XCTAssertEqual(key.defaultValue, defaultValue) 45 | XCTAssert( 46 | key.representation as? ReferenceProxyPersistentKeyValueRepresentation 47 | === representation 48 | ) 49 | } 50 | 51 | @MainActor 52 | func test_init_otherRepresentation_optional() throws { 53 | try skipIfNotiOS16OrLaterOrEquivalent() 54 | 55 | let representation = ReferenceProxyPersistentKeyValueRepresentation( 56 | serializing: \.self, 57 | deserializing: \.self 58 | ) 59 | 60 | let key = PersistentDebugKey(id, defaultValue: nil, representation: representation) 61 | 62 | XCTAssertEqual(key.id, id) 63 | XCTAssertEqual(key.defaultValue, nil) 64 | 65 | let baseRepresentation = try XCTUnwrap( 66 | key.representation as? OptionalPersistentKeyValueRepresentation< 67 | ReferenceProxyPersistentKeyValueRepresentation 68 | > 69 | ) 70 | 71 | XCTAssert(baseRepresentation.base === representation) 72 | } 73 | 74 | // MARK: Equatable Tests 75 | 76 | @MainActor 77 | func test_persistentKey_equatable() { 78 | let other = target 79 | 80 | XCTAssertEqual(target, other) 81 | } 82 | 83 | // MARK: Hashable Tests 84 | 85 | @MainActor 86 | func test_persistentKey_hashable() { 87 | let other = target 88 | 89 | XCTAssertEqual(target.hashValue, other.hashValue) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key/Implementations/PersistentKeyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersistentKeyTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 10/4/24. 6 | // 7 | 8 | import PersistentKeyValueKit 9 | import XCTest 10 | 11 | final class PersistentKeyTests: AbstractPersistentKeyTests> { 12 | // MARK: AbstractPersistenKeyTests Implementation 13 | 14 | override var defaultValue: String { 15 | "defaultValue" 16 | } 17 | 18 | override var id: String { 19 | "key" 20 | } 21 | 22 | override var storedValue: String { 23 | "storedValue" 24 | } 25 | 26 | override var target: PersistentKey { 27 | PersistentKey(id, defaultValue: defaultValue) 28 | } 29 | 30 | // MARK: Initialization Tests 31 | 32 | @MainActor 33 | func test_init_otherRepresentation() throws { 34 | try skipIfNotiOS16OrLaterOrEquivalent() 35 | 36 | let representation = ReferenceProxyPersistentKeyValueRepresentation( 37 | serializing: \.self, 38 | deserializing: \.self 39 | ) 40 | 41 | let key = PersistentKey(id, defaultValue: defaultValue, representation: representation) 42 | 43 | XCTAssertEqual(key.id, id) 44 | XCTAssertEqual(key.defaultValue, defaultValue) 45 | XCTAssert( 46 | key.representation as? ReferenceProxyPersistentKeyValueRepresentation 47 | === representation 48 | ) 49 | } 50 | 51 | @MainActor 52 | func test_init_otherRepresentation_optional() throws { 53 | try skipIfNotiOS16OrLaterOrEquivalent() 54 | 55 | let representation = ReferenceProxyPersistentKeyValueRepresentation( 56 | serializing: \.self, 57 | deserializing: \.self 58 | ) 59 | 60 | let key = PersistentKey(id, defaultValue: nil, representation: representation) 61 | 62 | XCTAssertEqual(key.id, id) 63 | XCTAssertEqual(key.defaultValue, nil) 64 | 65 | let baseRepresentation = try XCTUnwrap( 66 | key.representation as? OptionalPersistentKeyValueRepresentation< 67 | ReferenceProxyPersistentKeyValueRepresentation 68 | > 69 | ) 70 | 71 | XCTAssert(baseRepresentation.base === representation) 72 | } 73 | 74 | // MARK: Equatable Tests 75 | 76 | @MainActor 77 | func test_persistentKey_equatable() { 78 | let other = target 79 | 80 | XCTAssertEqual(target, other) 81 | } 82 | 83 | // MARK: Hashable Tests 84 | 85 | @MainActor 86 | func test_persistentKey_hashable() { 87 | let other = target 88 | 89 | XCTAssertEqual(target.hashValue, other.hashValue) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key/PersistentKeyObserverTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersistentKeyObserverTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 10/4/24. 6 | // 7 | 8 | import PersistentKeyValueKit 9 | import XCTest 10 | 11 | final class PersistentKeyObserverTests: XCTestCase { 12 | private let userDefaults: UserDefaults = .standard 13 | 14 | // MARK: XCTestCase Implementation 15 | 16 | override func tearDown() { 17 | userDefaults.dictionaryRepresentation().keys.forEach(userDefaults.removeObject) 18 | userDefaults.setVolatileDomain([:], forName: UserDefaults.registrationDomain) 19 | } 20 | } 21 | 22 | // MARK: - Initialization Tests 23 | 24 | extension PersistentKeyObserverTests { 25 | // MARK: Tests 26 | 27 | @MainActor 28 | func test_init_userDefaults() { 29 | let key = PersistentKey("testKey", defaultValue: "defaultValue") 30 | let observer = PersistentKeyUIObservableObject(store: userDefaults, key: key) 31 | 32 | XCTAssertNotNil(observer) 33 | XCTAssertEqual(observer.key.id, "testKey") 34 | XCTAssertEqual(observer.value, "defaultValue") 35 | } 36 | 37 | @available(watchOS 9.0, *) 38 | @MainActor 39 | func test_init_ubiquitousKeyValueStore() { 40 | let ubiquitousKeyValueStore: NSUbiquitousKeyValueStore = MockUbiquitousKeyValueStore() 41 | let key = PersistentKey("testKey", defaultValue: "defaultValue") 42 | let observer = PersistentKeyUIObservableObject(store: ubiquitousKeyValueStore, key: key) 43 | 44 | XCTAssertNotNil(observer) 45 | XCTAssertEqual(observer.key.id, "testKey") 46 | XCTAssertEqual(observer.value, "defaultValue") 47 | } 48 | } 49 | 50 | // MARK: - Value Tests 51 | 52 | extension PersistentKeyObserverTests { 53 | // MARK: Tests 54 | 55 | @MainActor 56 | func test_value_get() { 57 | let key = PersistentKey("testKey", defaultValue: "defaultValue") 58 | let observer = PersistentKeyUIObservableObject(store: userDefaults, key: key) 59 | 60 | XCTAssertEqual(observer.value, "defaultValue") 61 | 62 | userDefaults.set("newValue", forKey: "testKey") 63 | XCTAssertEqual(observer.value, "newValue") 64 | } 65 | 66 | @MainActor 67 | func test_value_set() { 68 | let key = PersistentKey("testKey", defaultValue: "defaultValue") 69 | let observer = PersistentKeyUIObservableObject(store: userDefaults, key: key) 70 | 71 | observer.value = "newValue" 72 | XCTAssertEqual(observer.value, "newValue") 73 | XCTAssertEqual(userDefaults.string(forKey: "testKey"), "newValue") 74 | } 75 | } 76 | 77 | // MARK: - Observation Tests 78 | 79 | extension PersistentKeyObserverTests { 80 | // MARK: Tests 81 | 82 | @MainActor 83 | func test_observation_userDefaultsChangesObserved() { 84 | let expectation = self.expectation(description: "Value change observed") 85 | expectation.expectedFulfillmentCount = 2 86 | let key = PersistentKey("testKey", defaultValue: "defaultValue") 87 | let observer = PersistentKeyUIObservableObject(store: userDefaults, key: key) 88 | 89 | let cancellable = observer.objectWillChange.sink { 90 | expectation.fulfill() 91 | } 92 | 93 | userDefaults.set("newValue1", forKey: "testKey") 94 | userDefaults.set("newValue2", forKey: "testKey") 95 | 96 | waitForExpectations(timeout: 1.0) 97 | cancellable.cancel() 98 | } 99 | } 100 | 101 | // MARK: - Store Change Tests 102 | 103 | extension PersistentKeyObserverTests { 104 | // MARK: Tests 105 | 106 | @available(watchOS 9.0, *) 107 | @MainActor 108 | func test_store_changeUpdatesValueAndObservation() { 109 | let ubiquitousKeyValueStore: NSUbiquitousKeyValueStore = MockUbiquitousKeyValueStore() 110 | let key = PersistentKey("testKey", defaultValue: "defaultValue") 111 | let observer = PersistentKeyUIObservableObject(store: userDefaults, key: key) 112 | 113 | userDefaults.set("userDefaultsValue", forKey: "testKey") 114 | XCTAssertEqual(observer.value, "userDefaultsValue") 115 | 116 | observer.store = ubiquitousKeyValueStore 117 | XCTAssertEqual(observer.value, "defaultValue") 118 | 119 | ubiquitousKeyValueStore.set("ubiquitousValue", forKey: "testKey") 120 | XCTAssertEqual(observer.value, "ubiquitousValue") 121 | } 122 | } 123 | 124 | // MARK: - Deinitialization Tests 125 | 126 | extension PersistentKeyObserverTests { 127 | // MARK: Tests 128 | 129 | @MainActor 130 | func test_deinitialization_observerRemoved() { 131 | let key = PersistentKey("testKey", defaultValue: "defaultValue") 132 | var observer: PersistentKeyUIObservableObject? = PersistentKeyUIObservableObject(store: userDefaults, key: key) 133 | 134 | weak var weakObserver = observer 135 | observer = nil 136 | 137 | XCTAssertNil(weakObserver, "Observer should be deallocated") 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Persistent Key/PersistentKeyProtocolTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersistentKeyProtocolTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 10/4/24. 6 | // 7 | 8 | import PersistentKeyValueKit 9 | import XCTest 10 | 11 | final class PersistentKeyProtocolTests: XCTestCase { 12 | private let userDefaults = UserDefaults() 13 | 14 | // MARK: XCTestCase Implementation 15 | 16 | override func tearDown() { 17 | userDefaults.dictionaryRepresentation().keys.forEach(userDefaults.removeObject) 18 | userDefaults.setVolatileDomain([:], forName: UserDefaults.registrationDomain) 19 | } 20 | } 21 | 22 | // MARK: - General Tests 23 | 24 | extension PersistentKeyProtocolTests { 25 | // MARK: Tests 26 | 27 | func test_registerDefault() { 28 | let defaultValue: String = UUID().uuidString 29 | let key = PersistentKey(UUID().uuidString, defaultValue: defaultValue) 30 | 31 | var dictionary: [String: Any] = [:] 32 | 33 | type(of: defaultValue).persistentKeyValueRepresentation.set(key.id, to: defaultValue, in: &dictionary) 34 | 35 | key.registerDefault(in: &dictionary) 36 | 37 | XCTAssertEqual(dictionary[key.id] as? String, defaultValue) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/PersistentKeyValueKitTests/Tests/Property Wrapper/DefaultPersistentKeyValueStoreViewModifierTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultPersistentKeyValueStoreViewModifierTests.swift 3 | // PersistentKeyValueKit 4 | // 5 | // Created by Kyle Hughes on 10/4/24. 6 | // 7 | 8 | import PersistentKeyValueKit 9 | import SwiftUI 10 | import XCTest 11 | 12 | final class DefaultPersistentKeyValueStoreViewModifierTests: XCTestCase {} 13 | 14 | // MARK: - Initialization Tests 15 | 16 | extension DefaultPersistentKeyValueStoreViewModifierTests { 17 | // MARK: Tests 18 | 19 | @MainActor 20 | func test_init() { 21 | let store = InMemoryPersistentKeyValueStore() 22 | let modifier = DefaultPersistentKeyValueStoreViewModifier(store: store) 23 | 24 | XCTAssertTrue(modifier.store as? InMemoryPersistentKeyValueStore === store) 25 | } 26 | } 27 | 28 | // MARK: - View Extension Tests 29 | 30 | extension DefaultPersistentKeyValueStoreViewModifierTests { 31 | // MARK: Tests 32 | 33 | @MainActor 34 | func test_viewExtension_appliesModifier() { 35 | let store = InMemoryPersistentKeyValueStore() 36 | 37 | let view = Text("Test") 38 | .defaultPersistentKeyValueStore(store) 39 | 40 | XCTAssert(type(of: view) == ModifiedContent.self) 41 | } 42 | } 43 | --------------------------------------------------------------------------------