diff --git a/CHANGELOG.md b/CHANGELOG.md index f9e5dc30..76d3626a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +### 1.8.0 +- **Add**: `TIFoundationUtils.AsyncOperation` - generic subclass of Operation with chaining and result observation support + ### 1.7.0 - **Add**: `TINetworking` - Swagger-frendly networking layer helpers diff --git a/LeadKit.podspec b/LeadKit.podspec index dc8548be..6c6ca909 100644 --- a/LeadKit.podspec +++ b/LeadKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "LeadKit" - s.version = "1.7.0" + s.version = "1.8.0" s.summary = "iOS framework with a bunch of tools for rapid development" s.homepage = "https://github.com/TouchInstinct/LeadKit" s.license = "Apache License, Version 2.0" diff --git a/Package.swift b/Package.swift index 03aebe37..7a6c9970 100644 --- a/Package.swift +++ b/Package.swift @@ -38,7 +38,7 @@ let package = Package( // MARK: - Utils .target(name: "TISwiftUtils", path: "TISwiftUtils/Sources"), - .target(name: "TIFoundationUtils", dependencies: ["TISwiftUtils"], path: "TIFoundationUtils/Sources"), + .target(name: "TIFoundationUtils", dependencies: ["TISwiftUtils"], path: "TIFoundationUtils"), .target(name: "TIKeychainUtils", dependencies: ["TIFoundationUtils", "KeychainAccess"], path: "TIKeychainUtils/Sources"), .target(name: "TITableKitUtils", dependencies: ["TIUIElements", "TableKit"], path: "TITableKitUtils/Sources"), .target(name: "TINetworking", dependencies: ["TISwiftUtils", "Alamofire"], path: "TINetworking/Sources"), diff --git a/TIFoundationUtils/AsyncOperation/Playground.playground/Contents.swift b/TIFoundationUtils/AsyncOperation/Playground.playground/Contents.swift new file mode 100644 index 00000000..a661c942 --- /dev/null +++ b/TIFoundationUtils/AsyncOperation/Playground.playground/Contents.swift @@ -0,0 +1,30 @@ +import Foundation +import TIFoundationUtils + +let operationQueue = OperationQueue() +operationQueue.maxConcurrentOperationCount = 1 + +ClosureAsyncOperation { completion in + DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) { + completion(.success(1)) + } +} +.map { $0 * 2 } +.observe(onSuccess: { result in + debugPrint("Async operation one has finished with \(result)") +}) +.add(to: operationQueue) + +ClosureAsyncOperation { completion in + DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) { + completion(.success(2)) + } +} +.map { $0 * 2 } +.observe(onSuccess: { result in + debugPrint("Async operation two has finished with \(result)") +}) +.add(to: operationQueue) + +// "Async operation one has finished with 2" +// "Async operation two has finished with 4" diff --git a/TIFoundationUtils/AsyncOperation/Playground.playground/contents.xcplayground b/TIFoundationUtils/AsyncOperation/Playground.playground/contents.xcplayground new file mode 100644 index 00000000..cf026f22 --- /dev/null +++ b/TIFoundationUtils/AsyncOperation/Playground.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/TIFoundationUtils/AsyncOperation/README.md b/TIFoundationUtils/AsyncOperation/README.md new file mode 100644 index 00000000..f78a77f6 --- /dev/null +++ b/TIFoundationUtils/AsyncOperation/README.md @@ -0,0 +1,42 @@ +# AsyncOperation + +Generic subclass of Operation with chaining and result observation support. + +## Examples + +### Serial queue of async operations + +Creating serial queue of async operations + +```swift +import Foundation +import TIFoundationUtils + +let operationQueue = OperationQueue() +operationQueue.maxConcurrentOperationCount = 1 + +ClosureAsyncOperation { completion in + DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) { + completion(.success(1)) + } +} +.map { $0 * 2 } +.observe(onSuccess: { result in + debugPrint("Async operation one has finished with \(result)") +}) +.add(to: operationQueue) + +ClosureAsyncOperation { completion in + DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) { + completion(.success(2)) + } +} +.map { $0 * 2 } +.observe(onSuccess: { result in + debugPrint("Async operation two has finished with \(result)") +}) +.add(to: operationQueue) + +// "Async operation one has finished with 2" +// "Async operation two has finished with 4" +``` diff --git a/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Map.swift b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Map.swift new file mode 100644 index 00000000..cb712adb --- /dev/null +++ b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Map.swift @@ -0,0 +1,67 @@ +// +// Copyright (c) 2022 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 + +private final class MapAsyncOperation: AsyncOperation { + private var dependencyObservation: NSKeyValueObservation? + + public init(dependency: AsyncOperation, + mapOutput: @escaping (DependencyOutput) -> Result, + mapFailure: @escaping (DependencyFailure) -> Failure) { + + super.init() + + dependencyObservation = dependency.subscribe { [weak self] in + switch mapOutput($0) { + case let .success(result): + self?.handle(result: result) + + case let .failure(error): + self?.handle(error: error) + } + } onFailure: { [weak self] in + self?.handle(error: mapFailure($0)) + } + + addDependency(dependency) // keeps strong reference to dependency as well + + state = .isReady + } +} + +public extension AsyncOperation { + func map(mapOutput: @escaping (Output) -> Result, + mapFailure: @escaping (Failure) -> NewFailure) + -> AsyncOperation { + + MapAsyncOperation(dependency: self, + mapOutput: mapOutput, + mapFailure: mapFailure) + } + + func map(mapOutput: @escaping (Output) -> NewOutput) -> AsyncOperation { + MapAsyncOperation(dependency: self, + mapOutput: { .success(mapOutput($0)) }, + mapFailure: { $0 }) + } +} diff --git a/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Observe.swift b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Observe.swift new file mode 100644 index 00000000..5115480c --- /dev/null +++ b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation+Observe.swift @@ -0,0 +1,54 @@ +// +// Copyright (c) 2022 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 + +private final class ClosureObserverOperation: AsyncOperation { + private var dependencyObservation: NSKeyValueObservation? + + public init(dependency: AsyncOperation, + onSuccess: ((Output) -> Void)? = nil, + onFailure: ((Failure) -> Void)? = nil) { + + super.init() + + dependencyObservation = dependency.subscribe { [weak self] in + onSuccess?($0) + self?.handle(result: $0) + } onFailure: { [weak self] in + onFailure?($0) + self?.handle(error: $0) + } + + addDependency(dependency) // keeps strong reference to dependency as well + + state = .isReady + } +} + +public extension AsyncOperation { + func observe(onSuccess: ((Output) -> Void)? = nil, + onFailure: ((Failure) -> Void)? = nil) -> AsyncOperation { + + ClosureObserverOperation(dependency: self, onSuccess: onSuccess, onFailure: onFailure) + } +} diff --git a/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation.swift b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation.swift new file mode 100644 index 00000000..72d61b0f --- /dev/null +++ b/TIFoundationUtils/AsyncOperation/Sources/AsyncOperation.swift @@ -0,0 +1,116 @@ +// +// Copyright (c) 2022 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 AsyncOperation: Operation { + + open var result: Result? + + var state: State? = nil { // isReady == false + // KVO support + willSet { + willChangeValue(forKey: newValue.orReady.rawValue) + willChangeValue(forKey: state.orReady.rawValue) + } + + didSet { + didChangeValue(forKey: state.orReady.rawValue) + didChangeValue(forKey: oldValue.orReady.rawValue) + } + } + + // MARK: - Operation override + + open override var isCancelled: Bool { + state == .isCancelled + } + + open override var isExecuting: Bool { + state == .isExecuting + } + + open override var isFinished: Bool { + state == .isFinished + } + + open override var isReady: Bool { + state == .isReady + } + + open override var isAsynchronous: Bool { + true + } + + open override func start() { + state = .isExecuting + + if result != nil { + state = .isFinished + } + } + + open override func cancel() { + state = .isCancelled + } + + // MARK: - Methods for subclass override + + open func handle(result: Output) { + self.result = .success(result) + + state = .isFinished + } + + open func handle(error: Failure) { + self.result = .failure(error) + + state = .isFinished + } + + // MARK: - Completion observation + + public func subscribe(onSuccess: ((Output) -> Void)? = nil, + onFailure: ((Failure) -> Void)? = nil) -> NSKeyValueObservation { + + observe(\.isFinished, options: [.new]) { object, change in + if let isFinished = change.newValue, isFinished { + switch object.result { + case let .success(result)?: + onSuccess?(result) + + case let .failure(failure)?: + onFailure?(failure) + + default: + assertionFailure("Got nil result from operation when isFinished was true!") + } + } + } + } +} + +private extension Optional where Wrapped == Operation.State { + var orReady: Wrapped { + self ?? .isReady + } +} diff --git a/TIFoundationUtils/AsyncOperation/Sources/ClosureAsyncOperation.swift b/TIFoundationUtils/AsyncOperation/Sources/ClosureAsyncOperation.swift new file mode 100644 index 00000000..75488a5e --- /dev/null +++ b/TIFoundationUtils/AsyncOperation/Sources/ClosureAsyncOperation.swift @@ -0,0 +1,49 @@ +// +// Copyright (c) 2022 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 final class ClosureAsyncOperation: AsyncOperation { + public typealias TaskClosure = (@escaping (Result) -> Void) -> Void + + private let taskClosure: TaskClosure + + public init(taskClosure: @escaping TaskClosure) { + self.taskClosure = taskClosure + + super.init() + + self.state = .isReady + } + + public override func start() { + super.start() + + taskClosure { [weak self] in + switch $0 { + case let .success(result): + self?.handle(result: result) + + case let .failure(error): + self?.handle(error: error) + } + } + } +} diff --git a/TIFoundationUtils/AsyncOperation/Sources/Operation+Extensions.swift b/TIFoundationUtils/AsyncOperation/Sources/Operation+Extensions.swift new file mode 100644 index 00000000..1febd5b4 --- /dev/null +++ b/TIFoundationUtils/AsyncOperation/Sources/Operation+Extensions.swift @@ -0,0 +1,33 @@ +// +// Copyright (c) 2022 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 extension Operation { + var flattenDependencies: [Operation] { + dependencies.flatMap { $0.dependencies } + dependencies + } + + func add(to operationQueue: OperationQueue, waitUntilFinished: Bool = false) { + operationQueue.addOperations(flattenDependencies + [self], waitUntilFinished: waitUntilFinished) + } +} diff --git a/TIFoundationUtils/AsyncOperation/Sources/Operation+State.swift b/TIFoundationUtils/AsyncOperation/Sources/Operation+State.swift new file mode 100644 index 00000000..cb54378e --- /dev/null +++ b/TIFoundationUtils/AsyncOperation/Sources/Operation+State.swift @@ -0,0 +1,32 @@ +// +// Copyright (c) 2022 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 extension Operation { + enum State: String { + case isCancelled + case isExecuting + case isFinished + case isReady + } +} diff --git a/TIFoundationUtils/CodableKeyValueStorage/Playground.playground/Contents.swift b/TIFoundationUtils/CodableKeyValueStorage/Playground.playground/Contents.swift new file mode 100644 index 00000000..e3d3d8c6 --- /dev/null +++ b/TIFoundationUtils/CodableKeyValueStorage/Playground.playground/Contents.swift @@ -0,0 +1,38 @@ +import Foundation +import TIFoundationUtils + +struct ProfileInfo: Codable { + let userName: String +} + +extension StorageKey { + static var profileKey: StorageKey { + .init(rawValue: "profileKey") + } + + static var onboardingFinishedKey: StorageKey { + .init(rawValue: "onboardingFinishedKey") + } +} + +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' + +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/CodableKeyValueStorage/Playground.playground/contents.xcplayground b/TIFoundationUtils/CodableKeyValueStorage/Playground.playground/contents.xcplayground new file mode 100644 index 00000000..cf026f22 --- /dev/null +++ b/TIFoundationUtils/CodableKeyValueStorage/Playground.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/TIFoundationUtils/CodableKeyValueStorage/README.md b/TIFoundationUtils/CodableKeyValueStorage/README.md new file mode 100644 index 00000000..36a5f8c7 --- /dev/null +++ b/TIFoundationUtils/CodableKeyValueStorage/README.md @@ -0,0 +1,56 @@ +# 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/CodableKeyValueStorage/Sources/CodableKeyValueStorage+BackingStore.swift similarity index 100% rename from TIFoundationUtils/Sources/CodableKeyValueStorage/CodableKeyValueStorage+BackingStore.swift rename to TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyValueStorage+BackingStore.swift diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/CodableKeyValueStorage.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyValueStorage.swift similarity index 100% rename from TIFoundationUtils/Sources/CodableKeyValueStorage/CodableKeyValueStorage.swift rename to TIFoundationUtils/CodableKeyValueStorage/Sources/CodableKeyValueStorage.swift diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/Decoders/CodableKeyValueDecoder.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/CodableKeyValueDecoder.swift similarity index 100% rename from TIFoundationUtils/Sources/CodableKeyValueStorage/Decoders/CodableKeyValueDecoder.swift rename to TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/CodableKeyValueDecoder.swift diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/Decoders/JSONKeyValueDecoder.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/JSONKeyValueDecoder.swift similarity index 100% rename from TIFoundationUtils/Sources/CodableKeyValueStorage/Decoders/JSONKeyValueDecoder.swift rename to TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/JSONKeyValueDecoder.swift diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/Decoders/UnarchiverKeyValueDecoder.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/UnarchiverKeyValueDecoder.swift similarity index 100% rename from TIFoundationUtils/Sources/CodableKeyValueStorage/Decoders/UnarchiverKeyValueDecoder.swift rename to TIFoundationUtils/CodableKeyValueStorage/Sources/Decoders/UnarchiverKeyValueDecoder.swift diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/Encoders/ArchiverKeyValueEncoder.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/ArchiverKeyValueEncoder.swift similarity index 100% rename from TIFoundationUtils/Sources/CodableKeyValueStorage/Encoders/ArchiverKeyValueEncoder.swift rename to TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/ArchiverKeyValueEncoder.swift diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/Encoders/CodableKeyValueEncoder.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/CodableKeyValueEncoder.swift similarity index 100% rename from TIFoundationUtils/Sources/CodableKeyValueStorage/Encoders/CodableKeyValueEncoder.swift rename to TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/CodableKeyValueEncoder.swift diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/Encoders/JSONKeyValueEncoder.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/JSONKeyValueEncoder.swift similarity index 100% rename from TIFoundationUtils/Sources/CodableKeyValueStorage/Encoders/JSONKeyValueEncoder.swift rename to TIFoundationUtils/CodableKeyValueStorage/Sources/Encoders/JSONKeyValueEncoder.swift diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/StorageError.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/StorageError.swift similarity index 100% rename from TIFoundationUtils/Sources/CodableKeyValueStorage/StorageError.swift rename to TIFoundationUtils/CodableKeyValueStorage/Sources/StorageError.swift diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/StorageKey.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/StorageKey.swift similarity index 100% rename from TIFoundationUtils/Sources/CodableKeyValueStorage/StorageKey.swift rename to TIFoundationUtils/CodableKeyValueStorage/Sources/StorageKey.swift diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/UserDefaults+CodableKeyValueStorage.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/UserDefaults/UserDefaults+CodableKeyValueStorage.swift similarity index 100% rename from TIFoundationUtils/Sources/CodableKeyValueStorage/UserDefaults+CodableKeyValueStorage.swift rename to TIFoundationUtils/CodableKeyValueStorage/Sources/UserDefaults/UserDefaults+CodableKeyValueStorage.swift diff --git a/TIFoundationUtils/Sources/CodableKeyValueStorage/UserDefaultsCodableBackingStore.swift b/TIFoundationUtils/CodableKeyValueStorage/Sources/UserDefaults/UserDefaultsCodableBackingStore.swift similarity index 100% rename from TIFoundationUtils/Sources/CodableKeyValueStorage/UserDefaultsCodableBackingStore.swift rename to TIFoundationUtils/CodableKeyValueStorage/Sources/UserDefaults/UserDefaultsCodableBackingStore.swift diff --git a/TIFoundationUtils/README.md b/TIFoundationUtils/README.md index f01f564f..73e365c2 100644 --- a/TIFoundationUtils/README.md +++ b/TIFoundationUtils/README.md @@ -3,61 +3,6 @@ Set of helpers for Foundation framework classes. * [TITimer](Sources/TITimer) - pretty timer -* [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 -} -``` +* [AsyncOperation](AsyncOperation) - generic subclass of Operation with chaining and result observation support +* [CodableKeyValueStorage](CodableKeyValueStorage) - key-value storage for Codable types + UserDefaults implementation +* [UserDefaultsBackingStore](UserDefaultsBackingStore) - BackingStore property wrapper for UserDefaults diff --git a/TIFoundationUtils/TIFoundationUtils.podspec b/TIFoundationUtils/TIFoundationUtils.podspec index 557d461e..a98907d0 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIFoundationUtils' - s.version = '1.7.0' + s.version = '1.8.0' s.summary = 'Set of helpers for Foundation framework classes.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIFoundationUtils/Sources/TITimer/README.md b/TIFoundationUtils/TITimer/README.md similarity index 100% rename from TIFoundationUtils/Sources/TITimer/README.md rename to TIFoundationUtils/TITimer/README.md diff --git a/TIFoundationUtils/Sources/TITimer/Sources/TITimer/Enums/TimerRunMode.swift b/TIFoundationUtils/TITimer/Sources/TITimer/Enums/TimerRunMode.swift similarity index 100% rename from TIFoundationUtils/Sources/TITimer/Sources/TITimer/Enums/TimerRunMode.swift rename to TIFoundationUtils/TITimer/Sources/TITimer/Enums/TimerRunMode.swift diff --git a/TIFoundationUtils/Sources/TITimer/Sources/TITimer/Enums/TimerType.swift b/TIFoundationUtils/TITimer/Sources/TITimer/Enums/TimerType.swift similarity index 100% rename from TIFoundationUtils/Sources/TITimer/Sources/TITimer/Enums/TimerType.swift rename to TIFoundationUtils/TITimer/Sources/TITimer/Enums/TimerType.swift diff --git a/TIFoundationUtils/Sources/TITimer/Sources/TITimer/Protocols/ITimer.swift b/TIFoundationUtils/TITimer/Sources/TITimer/Protocols/ITimer.swift similarity index 100% rename from TIFoundationUtils/Sources/TITimer/Sources/TITimer/Protocols/ITimer.swift rename to TIFoundationUtils/TITimer/Sources/TITimer/Protocols/ITimer.swift diff --git a/TIFoundationUtils/Sources/TITimer/Sources/TITimer/Protocols/Invalidatable.swift b/TIFoundationUtils/TITimer/Sources/TITimer/Protocols/Invalidatable.swift similarity index 100% rename from TIFoundationUtils/Sources/TITimer/Sources/TITimer/Protocols/Invalidatable.swift rename to TIFoundationUtils/TITimer/Sources/TITimer/Protocols/Invalidatable.swift diff --git a/TIFoundationUtils/Sources/TITimer/Sources/TITimer/TITimer.swift b/TIFoundationUtils/TITimer/Sources/TITimer/TITimer.swift similarity index 100% rename from TIFoundationUtils/Sources/TITimer/Sources/TITimer/TITimer.swift rename to TIFoundationUtils/TITimer/Sources/TITimer/TITimer.swift diff --git a/TIFoundationUtils/UserDefaultsBackingStore/Playground.playground/Contents.swift b/TIFoundationUtils/UserDefaultsBackingStore/Playground.playground/Contents.swift new file mode 100644 index 00000000..134ad229 --- /dev/null +++ b/TIFoundationUtils/UserDefaultsBackingStore/Playground.playground/Contents.swift @@ -0,0 +1,16 @@ +import Foundation +import TIFoundationUtils + +extension StorageKey { + static var onboardingFinishedKey: StorageKey { + .init(rawValue: "onboardingFinishedKey") + } +} + +final class ViewModel { + @UserDefaultsBackingStore(key: .onboardingFinishedKey, + userDefaultsStorage: .standard, + getClosure: { $0.bool(forKey: $1) }, + setClosure: { $0.set($1, forKey: $2) }) + var hasFinishedOnboarding = false // default value if nothing was stored in defaults +} diff --git a/TIFoundationUtils/UserDefaultsBackingStore/Playground.playground/contents.xcplayground b/TIFoundationUtils/UserDefaultsBackingStore/Playground.playground/contents.xcplayground new file mode 100644 index 00000000..cf026f22 --- /dev/null +++ b/TIFoundationUtils/UserDefaultsBackingStore/Playground.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/TIFoundationUtils/UserDefaultsBackingStore/README.md b/TIFoundationUtils/UserDefaultsBackingStore/README.md new file mode 100644 index 00000000..2691b629 --- /dev/null +++ b/TIFoundationUtils/UserDefaultsBackingStore/README.md @@ -0,0 +1,22 @@ +# UserDefaultsBackingStore + +BackingStore property wrapper for UserDefaults + +## Usage example + +```swift +extension StorageKey { + static var onboardingFinishedKey: StorageKey { + .init(rawValue: "onboardingFinishedKey") + } +} + +final class ViewModel { + @UserDefaultsBackingStore(key: .onboardingFinishedKey, + userDefaultsStorage: .standard, + getClosure: { $0.bool(forKey: $1) }, + setClosure: { $0.set($1, forKey: $2) }) + var hasFinishedOnboarding = false // default value if nothing was stored in defaults +} +``` + diff --git a/TIFoundationUtils/Sources/UserDefaultsBackingStore/UserDefaultsBackingStore.swift b/TIFoundationUtils/UserDefaultsBackingStore/Sources/UserDefaultsBackingStore.swift similarity index 100% rename from TIFoundationUtils/Sources/UserDefaultsBackingStore/UserDefaultsBackingStore.swift rename to TIFoundationUtils/UserDefaultsBackingStore/Sources/UserDefaultsBackingStore.swift diff --git a/TIKeychainUtils/TIKeychainUtils.podspec b/TIKeychainUtils/TIKeychainUtils.podspec index 8fe917f7..2cd8099c 100644 --- a/TIKeychainUtils/TIKeychainUtils.podspec +++ b/TIKeychainUtils/TIKeychainUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIKeychainUtils' - s.version = '1.7.0' + s.version = '1.8.0' s.summary = 'Set of helpers for Keychain classes.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TINetworking/TINetworking.podspec b/TINetworking/TINetworking.podspec index 1c025fb1..fe82b221 100644 --- a/TINetworking/TINetworking.podspec +++ b/TINetworking/TINetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworking' - s.version = '1.7.0' + s.version = '1.8.0' s.summary = 'Swagger-frendly networking layer helpers.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TISwiftUtils/TISwiftUtils.podspec b/TISwiftUtils/TISwiftUtils.podspec index b89b2c68..d9c2a975 100644 --- a/TISwiftUtils/TISwiftUtils.podspec +++ b/TISwiftUtils/TISwiftUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUtils' - s.version = '1.7.0' + s.version = '1.8.0' s.summary = 'Bunch of useful helpers for Swift development.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TITableKitUtils/TITableKitUtils.podspec b/TITableKitUtils/TITableKitUtils.podspec index c56eca20..9c1d528a 100644 --- a/TITableKitUtils/TITableKitUtils.podspec +++ b/TITableKitUtils/TITableKitUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITableKitUtils' - s.version = '1.7.0' + s.version = '1.8.0' s.summary = 'Set of helpers for TableKit classes.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TITransitions/TITransitions.podspec b/TITransitions/TITransitions.podspec index af42244b..dfe548e7 100644 --- a/TITransitions/TITransitions.podspec +++ b/TITransitions/TITransitions.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITransitions' - s.version = '1.7.0' + s.version = '1.8.0' s.summary = 'Set of custom transitions to present controller. ' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIUIElements/TIUIElements.podspec b/TIUIElements/TIUIElements.podspec index dc40a6f3..549b400a 100644 --- a/TIUIElements/TIUIElements.podspec +++ b/TIUIElements/TIUIElements.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIElements' - s.version = '1.7.0' + s.version = '1.8.0' s.summary = 'Bunch of useful protocols and views.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIUIKitCore/TIUIKitCore.podspec b/TIUIKitCore/TIUIKitCore.podspec index 88340b88..c33b9d91 100644 --- a/TIUIKitCore/TIUIKitCore.podspec +++ b/TIUIKitCore/TIUIKitCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIKitCore' - s.version = '1.7.0' + s.version = '1.8.0' s.summary = 'Core UI elements: protocols, views and helpers.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' }