diff --git a/CHANGELOG.md b/CHANGELOG.md index 039c70bd..0d96147d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ - **Refactored**: NetworkManager to use new Alamofire API - **API BreakingChanges**: NetworkServiceConfiguration no longer accepts `ServerTrustPolicy`, it is now replaced by an instance of a `ServerTrustEvaluating` protocol. Full description and default implementations can be found at Alamofire [sources](https://github.com/Alamofire/Alamofire/blob/master/Source/ServerTrustEvaluation.swift). Since new evaluation is used, evaluation against self-signed certificates will now throw an AfError and abort any outcoming request. To support self-signed certificates use `DisabledTrustEvaluator` for specified host in configuration. +### 0.9.44 +- **Add**: `TIFoundationUtils` - set of helpers for Foundation framework classes. + +#### TISwiftUtils +- **Add**: `BackingStore` - a property wrapper that wraps storage and defines getter and setter for accessing value from it. + +#### TIFoundationUtils +- **Add**: `CodableKeyValueStorage` - storage that can get and set codable objects by the key. + + ### 0.9.43 - **Fix**: `OTPSwiftView`'s dependencies. diff --git a/Package.swift b/Package.swift index f15b6fb6..698d8992 100644 --- a/Package.swift +++ b/Package.swift @@ -10,6 +10,7 @@ let package = Package( .library(name: "TITransitions", targets: ["TITransitions"]), .library(name: "TIUIKitCore", targets: ["TIUIKitCore"]), .library(name: "TISwiftUtils", targets: ["TISwiftUtils"]), + .library(name: "TIFoundationUtils", targets: ["TIFoundationUtils"]), .library(name: "TIUIElements", targets: ["TIUIElements"]), .library(name: "OTPSwiftView", targets: ["OTPSwiftView"]) ], @@ -17,6 +18,7 @@ let package = Package( .target(name: "TITransitions", path: "TITransitions/Sources"), .target(name: "TIUIKitCore", path: "TIUIKitCore/Sources"), .target(name: "TISwiftUtils", path: "TISwiftUtils/Sources"), + .target(name: "TIFoundationUtils", dependencies: ["TISwiftUtils"], path: "TIFoundationUtils/Sources"), .target(name: "TIUIElements", dependencies: ["TIUIKitCore"], path: "TIUIElements/Sources"), .target(name: "OTPSwiftView", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "OTPSwiftView/Sources") ] diff --git a/TIFoundationUtils/README.md b/TIFoundationUtils/README.md new file mode 100644 index 00000000..71adc88a --- /dev/null +++ b/TIFoundationUtils/README.md @@ -0,0 +1,62 @@ +# TIFoundationUtils + +Set of helpers for Foundation framework classes. + +* [CodableKeyValueStorage](#codablekeyvaluestorage) + +## CodableKeyValueStorage + +Storage that can get and set codable objects by the key. + +Implementations: `UserDefaults` + +### Example + +```swift +struct ProfileInfo: Codable { + let userName: String +} +``` + +Keys: + +```swift +extension StorageKey { + static var profileKey: StorageKey { + .init(rawValue: "profileKey") + } + + static var onboardingFinishedKey: StorageKey { + .init(rawValue: "onboardingFinishedKey") + } +} +``` + +#### Subscript example + +```swift +var defaults = UserDefaults.standard +defaults[.profileKey] = ProfileInfo(userName: "John Appleseed") +defaults[.profileKey] = "Agent Smith" // this will threat compile error: +// Cannot assign value of type 'String' to subscript of type 'ProfileInfo' +``` +#### @propertyWrapper example + +```swift +final class ViewModel { + @UserDefaultsCodableBackingStore(key: .profileKey, codableKeyValueStorage: .standard) + var profile: ProfileInfo? + + // This will threat compile error: + // Cannot convert value of type 'BackingStore' to specified type 'ProfileInfo?' + @UserDefaultsCodableBackingStore(key: .onboardingFinishedKey, codableKeyValueStorage: .standard) + var wrongKeyProfile: ProfileInfo? + + // For primitive types we can't use default json decoder/encoder + @UserDefaultsCodableBackingStore(key: .onboardingFinishedKey, + codableKeyValueStorage: .standard, + decoder: UnarchiverKeyValueDecoder(), + encoder: ArchiverKeyValueEncoder()) + var onboardingFinished = false +} +``` diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/CodableKeyValueStorage+BackingStore.swift b/TIFoundationUtils/Sources/CodableKeyValueStorage/CodableKeyValueStorage+BackingStore.swift new file mode 100644 index 00000000..9824afbc --- /dev/null +++ b/TIFoundationUtils/Sources/CodableKeyValueStorage/CodableKeyValueStorage+BackingStore.swift @@ -0,0 +1,49 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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. +// + +import TISwiftUtils + +public typealias CodableKeyValueBackingStore = BackingStore + +public extension BackingStore where Store: CodableKeyValueStorage, StoreContent: Codable { + init(key: StorageKey, + codableKeyValueStorage: Store, + decoder: CodableKeyValueDecoder = JSONKeyValueDecoder(), + encoder: CodableKeyValueEncoder = JSONKeyValueEncoder()) + where StoreContent == Value? { + + self.init(store: codableKeyValueStorage, + getClosure: { try? $0.codableObject(forKey: key, decoder: decoder) }, + setClosure: { try? $0.setOrRemove(codableObject: $1, forKey: key, encoder: encoder) }) + } + + init(wrappedValue: StoreContent, + key: StorageKey, + codableKeyValueStorage: Store, + decoder: CodableKeyValueDecoder = JSONKeyValueDecoder(), + encoder: CodableKeyValueEncoder = JSONKeyValueEncoder()) { + + self.init(store: codableKeyValueStorage, + getClosure: { $0.codableObject(forKey: key, defaultValue: wrappedValue, decoder: decoder) }, + setClosure: { try? $0.setOrRemove(codableObject: $1, forKey: key, encoder: encoder) }) + } +} diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/CodableKeyValueStorage.swift b/TIFoundationUtils/Sources/CodableKeyValueStorage/CodableKeyValueStorage.swift new file mode 100644 index 00000000..f1e4f8d4 --- /dev/null +++ b/TIFoundationUtils/Sources/CodableKeyValueStorage/CodableKeyValueStorage.swift @@ -0,0 +1,91 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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. +// + +import Foundation + +public protocol CodableKeyValueStorage { + /// Returns the object with specified type associated with the first occurrence of the specified key. + /// - Parameters: + /// - key: A key in the storage. + /// - decoder: CodableKeyValueDecoder to decode stored data. + /// - Returns: The object with specified type associated with the specified key, + /// or throw exception if the key was not found. + /// - Throws: CodableStorageError + func codableObject(forKey key: StorageKey, + decoder: CodableKeyValueDecoder) throws -> Value + + /// Set or remove the value of the specified key in the storage. + /// - Parameters: + /// - object: The object with specified type to store. + /// - key: The key with which to associate with the value. + /// - encoder: CodableKeyValueEncoder to encode to encode passed object. + /// - Throws: EncodingError if error is occured during passed object encoding. + func set(encodableObject: Value, + forKey key: StorageKey, + encoder: CodableKeyValueEncoder) throws + + /// Removes value for specific key + /// - Parameter key: The key with which to associate with the value. + /// - Throws: EncodingError if error is occured during reading/writing. + func removeCodableValue(forKey key: StorageKey) throws +} + +public extension CodableKeyValueStorage { + + /// Returns the object with specified type associated with the first occurrence of the specified key. + /// - Parameters: + /// - key: A key in the storage. + /// - defaultValue: A default value that will be used if there is no such value for specified key, + /// - decoder: CodableKeyValueDecoder to decode stored data. + /// or if error occurred during decoding + /// - Returns: The object with specified type associated with the specified key, or passed default value + /// if there is no such value for specified key or if error occurred during mapping. + func codableObject(forKey key: StorageKey, + defaultValue: Value, + decoder: CodableKeyValueDecoder = JSONKeyValueDecoder()) -> Value { + + (try? codableObject(forKey: key, decoder: decoder)) ?? defaultValue + } + + func setOrRemove(codableObject: Value?, + forKey key: StorageKey, + encoder: CodableKeyValueEncoder = JSONKeyValueEncoder()) throws { + + if let codableObject = codableObject { + try set(encodableObject: codableObject, forKey: key, encoder: encoder) + } else { + try? removeCodableValue(forKey: key) + } + } + + subscript(key: StorageKey, + decoder: CodableKeyValueDecoder = JSONKeyValueDecoder(), + encoder: CodableKeyValueEncoder = JSONKeyValueEncoder()) -> Value? { + + get { + try? codableObject(forKey: key, decoder: decoder) + } + set { + try? setOrRemove(codableObject: newValue, forKey: key, encoder: encoder) + } + } +} diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/Decoders/CodableKeyValueDecoder.swift b/TIFoundationUtils/Sources/CodableKeyValueStorage/Decoders/CodableKeyValueDecoder.swift new file mode 100644 index 00000000..54e4cf99 --- /dev/null +++ b/TIFoundationUtils/Sources/CodableKeyValueStorage/Decoders/CodableKeyValueDecoder.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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. +// + +import Foundation + +public protocol CodableKeyValueDecoder { + func decodeDecodable(from data: Data, for key: StorageKey) throws -> Value +} diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/Decoders/JSONKeyValueDecoder.swift b/TIFoundationUtils/Sources/CodableKeyValueStorage/Decoders/JSONKeyValueDecoder.swift new file mode 100644 index 00000000..406f89b9 --- /dev/null +++ b/TIFoundationUtils/Sources/CodableKeyValueStorage/Decoders/JSONKeyValueDecoder.swift @@ -0,0 +1,39 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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. +// + +import Foundation + +open class JSONKeyValueDecoder: CodableKeyValueDecoder { + private let jsonDecoder: JSONDecoder + + public init(jsonDecoder: JSONDecoder = JSONDecoder()) { + self.jsonDecoder = jsonDecoder + } + + open func decodeDecodable(from data: Data, for key: StorageKey) throws -> Value { + do { + return try jsonDecoder.decode(Value.self, from: data) + } catch { + throw StorageError.unableToDecode(underlyingError: error) + } + } +} diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/Decoders/UnarchiverKeyValueDecoder.swift b/TIFoundationUtils/Sources/CodableKeyValueStorage/Decoders/UnarchiverKeyValueDecoder.swift new file mode 100644 index 00000000..49e0d5a7 --- /dev/null +++ b/TIFoundationUtils/Sources/CodableKeyValueStorage/Decoders/UnarchiverKeyValueDecoder.swift @@ -0,0 +1,47 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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. +// + +import Foundation + +open class UnarchiverKeyValueDecoder: CodableKeyValueDecoder { + public init() {} + + open func decodeDecodable(from data: Data, for key: StorageKey) throws -> Value { + let unarchiver: NSKeyedUnarchiver + + do { + unarchiver = try NSKeyedUnarchiver(forReadingFrom: data) + } catch { + throw StorageError.unableToDecode(underlyingError: error) + } + + defer { + unarchiver.finishDecoding() + } + + guard let decodableObject = unarchiver.decodeDecodable(Value.self, forKey: key.rawValue) else { + throw StorageError.valueNotFound + } + + return decodableObject + } +} diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/Encoders/ArchiverKeyValueEncoder.swift b/TIFoundationUtils/Sources/CodableKeyValueStorage/Encoders/ArchiverKeyValueEncoder.swift new file mode 100644 index 00000000..80a8a954 --- /dev/null +++ b/TIFoundationUtils/Sources/CodableKeyValueStorage/Encoders/ArchiverKeyValueEncoder.swift @@ -0,0 +1,41 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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. +// + +import Foundation + +open class ArchiverKeyValueEncoder: CodableKeyValueEncoder { + public init() {} + + open func encodeEncodable(value: Value, for key: StorageKey) throws -> Data { + let archiver = NSKeyedArchiver(requiringSecureCoding: true) + + do { + try archiver.encodeEncodable(value, forKey: key.rawValue) + } catch { + throw StorageError.unableToEncode(underlyingError: error) + } + + archiver.finishEncoding() + + return archiver.encodedData + } +} diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/Encoders/CodableKeyValueEncoder.swift b/TIFoundationUtils/Sources/CodableKeyValueStorage/Encoders/CodableKeyValueEncoder.swift new file mode 100644 index 00000000..30bf3fb0 --- /dev/null +++ b/TIFoundationUtils/Sources/CodableKeyValueStorage/Encoders/CodableKeyValueEncoder.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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. +// + +import Foundation + +public protocol CodableKeyValueEncoder { + func encodeEncodable(value: Value, for key: StorageKey) throws -> Data +} diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/Encoders/JSONKeyValueEncoder.swift b/TIFoundationUtils/Sources/CodableKeyValueStorage/Encoders/JSONKeyValueEncoder.swift new file mode 100644 index 00000000..0aacb1ae --- /dev/null +++ b/TIFoundationUtils/Sources/CodableKeyValueStorage/Encoders/JSONKeyValueEncoder.swift @@ -0,0 +1,39 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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. +// + +import Foundation + +open class JSONKeyValueEncoder: CodableKeyValueEncoder { + private let jsonEncoder: JSONEncoder + + public init(jsonEncoder: JSONEncoder = JSONEncoder()) { + self.jsonEncoder = jsonEncoder + } + + open func encodeEncodable(value: Value, for key: StorageKey) throws -> Data { + do { + return try jsonEncoder.encode(value) + } catch { + throw StorageError.unableToEncode(underlyingError: error) + } + } +} diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/StorageError.swift b/TIFoundationUtils/Sources/CodableKeyValueStorage/StorageError.swift new file mode 100644 index 00000000..0e64dc35 --- /dev/null +++ b/TIFoundationUtils/Sources/CodableKeyValueStorage/StorageError.swift @@ -0,0 +1,29 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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. +// + +enum StorageError: Error { + case valueNotFound + case unableToExtractData(underlyingError: Error) + case unableToDecode(underlyingError: Error) + case unableToEncode(underlyingError: Error) + case unableToWriteData(underlyingError: Error) +} diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/StorageKey.swift b/TIFoundationUtils/Sources/CodableKeyValueStorage/StorageKey.swift new file mode 100644 index 00000000..8c9cc837 --- /dev/null +++ b/TIFoundationUtils/Sources/CodableKeyValueStorage/StorageKey.swift @@ -0,0 +1,29 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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. +// + +public struct StorageKey: RawRepresentable { + public let rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } +} diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/UserDefaults+CodableKeyValueStorage.swift b/TIFoundationUtils/Sources/CodableKeyValueStorage/UserDefaults+CodableKeyValueStorage.swift new file mode 100644 index 00000000..e3a3ac64 --- /dev/null +++ b/TIFoundationUtils/Sources/CodableKeyValueStorage/UserDefaults+CodableKeyValueStorage.swift @@ -0,0 +1,52 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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. +// + +import Foundation + +extension UserDefaults: CodableKeyValueStorage { + public func codableObject(forKey key: StorageKey, + decoder: CodableKeyValueDecoder) throws -> Value { + + guard let storedData = data(forKey: key.rawValue) else { + throw StorageError.valueNotFound + } + + return try decoder.decodeDecodable(from: storedData, for: key) + } + + public func set(encodableObject: Value, + forKey key: StorageKey, + encoder: CodableKeyValueEncoder) throws { + + let encodedData = try encoder.encodeEncodable(value: encodableObject, for: key) + + set(encodedData, forKey: key.rawValue) + } + + public func removeCodableValue(forKey key: StorageKey) throws { + guard data(forKey: key.rawValue) != nil else { + throw StorageError.valueNotFound + } + + removeObject(forKey: key.rawValue) + } +} diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/UserDefaultsCodableBackingStore.swift b/TIFoundationUtils/Sources/CodableKeyValueStorage/UserDefaultsCodableBackingStore.swift new file mode 100644 index 00000000..cfd9fbac --- /dev/null +++ b/TIFoundationUtils/Sources/CodableKeyValueStorage/UserDefaultsCodableBackingStore.swift @@ -0,0 +1,25 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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. +// + +import Foundation + +public typealias UserDefaultsCodableBackingStore = CodableKeyValueBackingStore diff --git a/TISwiftUtils/README.md b/TISwiftUtils/README.md index d135d3e7..993c9cfe 100644 --- a/TISwiftUtils/README.md +++ b/TISwiftUtils/README.md @@ -2,6 +2,23 @@ Bunch of useful helpers for development. +* [BackingStore](#backingstore) + +## BackingStore + +A property wrapper that wraps storage and defines getter and setter for accessing value from it. + +### Example + +```swift +final class ViewModel { + @BackingStore(store: UserDefaults.standard, + getClosure: { $0.bool(forKey: "hasFinishedOnboarding") }, + setClosure: { $0.set($1, forKey: "hasFinishedOnboarding") }) + var hasFinishedOnboarding: Bool +} +``` + # Installation via SPM You can install this framework as a target of LeadKit. diff --git a/TISwiftUtils/Sources/PropertyWrappers/BackingStore.swift b/TISwiftUtils/Sources/PropertyWrappers/BackingStore.swift new file mode 100644 index 00000000..6f9f1146 --- /dev/null +++ b/TISwiftUtils/Sources/PropertyWrappers/BackingStore.swift @@ -0,0 +1,65 @@ +// +// Copyright (c) 2020 Touch Instinct +// +// 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: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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. +// + +@propertyWrapper public struct BackingStore { + + public typealias InitClosure = (StoreContent) -> Store + public typealias GetClosure = (Store) -> StoreContent + public typealias SetClosure = (Store, StoreContent) -> Void + + private let getClosure: GetClosure + private let setClosure: SetClosure + + private let store: Store + + public init(wrappedValue: StoreContent, + storageInitClosure: InitClosure, + getClosure: @escaping GetClosure, + setClosure: @escaping SetClosure) { + + self.store = storageInitClosure(wrappedValue) + self.getClosure = getClosure + self.setClosure = setClosure + } + + public init(store: Store, + getClosure: @escaping GetClosure, + setClosure: @escaping SetClosure) { + + self.store = store + self.getClosure = getClosure + self.setClosure = setClosure + } + + public var wrappedValue: StoreContent { + get { + getClosure(store) + } + set { + setClosure(store, newValue) + } + } + + public var projectedValue: Store { + store + } +}