├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── CacheClient │ ├── CacheClient.swift │ └── CacheClientOperations.swift ├── DataRepresentable │ ├── AnyDataRepresenable.swift │ └── DataRepresentable.swift ├── HapticEngineClient │ ├── HapticEngineClient.swift │ ├── HapticEngineClientOperations.swift │ └── HapticFeedback.swift ├── HapticEngineClientLive │ ├── Exports.swift │ ├── HapticEngineClientLive.swift │ └── HapticFeedback.swift ├── IDFAPermissionsClient │ ├── IDFAPermissionsClient.swift │ └── IDFAPermissionsClientOperations.swift ├── IDFAPermissionsClientLive │ ├── AuthorizationStatus.swift │ ├── Exports.swift │ └── IDFAPermissionsClientLive.swift ├── KeychainClient │ ├── KeychainClient.swift │ └── KeychainClientOperations.swift ├── KeychainClientLive │ ├── Exports.swift │ ├── Keychain │ │ ├── CoreFoundation+.swift │ │ ├── Keychain+AccessOptions.swift │ │ ├── Keychain+Constants.swift │ │ ├── Keychain+Error.swift │ │ ├── Keychain+OperationResult.swift │ │ ├── Keychain.swift │ │ ├── Licence │ │ │ ├── LICENCE │ │ │ └── README │ │ └── NSLocking+.swift │ ├── KeychainClientAccessPolicy.swift │ └── KeychainClientLive.swift ├── MemoryCacheClient │ ├── Exports.swift │ ├── MemoryCache.swift │ └── MemoryCacheClient.swift ├── NotificationsPermissionsClient │ ├── NotificationsPermissionsClient.swift │ └── NotificationsPermissionsClientOperations.swift ├── NotificationsPermissionsClientLive │ ├── AuthorizationOptions.swift │ ├── AuthorizationStatus.swift │ ├── Exports.swift │ └── NotificationsPermissionsClientLive.swift ├── StandardClients │ └── Exports.swift ├── StandardClientsLive │ └── Exports.swift ├── UserDefaultsClient │ ├── UserDefaultsClient.swift │ └── UserDefaultsClientOperations.swift └── UserDefaultsClientLive │ ├── Exports.swift │ └── UserDefaultsClientLive.swift └── Tests └── DataRepresentableTests └── DataRepresentableTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 CaptureContext 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "swift-standard-clients", 7 | platforms: [ 8 | .macOS(.v10_15), 9 | .iOS(.v13), 10 | .tvOS(.v13), 11 | .watchOS(.v6) 12 | ], 13 | products: [ 14 | // MARK: Exports 15 | .library( 16 | name: "StandardClients", 17 | type: .static, 18 | targets: ["StandardClients"] 19 | ), 20 | .library( 21 | name: "StandardClientsLive", 22 | type: .static, 23 | targets: ["StandardClientsLive"] 24 | ), 25 | 26 | // MARK: Cache 27 | .library( 28 | name: "CacheClient", 29 | type: .static, 30 | targets: ["CacheClient"] 31 | ), 32 | .library( 33 | name: "MemoryCacheClient", 34 | type: .static, 35 | targets: ["MemoryCacheClient"] 36 | ), 37 | 38 | // MARK: DataRepresentable 39 | .library( 40 | name: "DataRepresentable", 41 | type: .static, 42 | targets: ["DataRepresentable"] 43 | ), 44 | 45 | // MARK: HapticEngine 46 | .library( 47 | name: "HapticEngineClient", 48 | type: .static, 49 | targets: ["HapticEngineClient"] 50 | ), 51 | .library( 52 | name: "HapticEngineClientLive", 53 | type: .static, 54 | targets: ["HapticEngineClientLive"] 55 | ), 56 | 57 | // MARK: IDFA 58 | .library( 59 | name: "IDFAPermissionsClient", 60 | type: .static, 61 | targets: ["IDFAPermissionsClient"] 62 | ), 63 | .library( 64 | name: "IDFAPermissionsClientLive", 65 | type: .static, 66 | targets: ["IDFAPermissionsClientLive"] 67 | ), 68 | 69 | // MARK: Keychain 70 | .library( 71 | name: "KeychainClient", 72 | type: .static, 73 | targets: ["KeychainClient"] 74 | ), 75 | .library( 76 | name: "KeychainClientLive", 77 | type: .static, 78 | targets: ["KeychainClientLive"] 79 | ), 80 | 81 | // MARK: Notifications 82 | .library( 83 | name: "NotificationsPermissionsClient", 84 | type: .static, 85 | targets: ["NotificationsPermissionsClient"] 86 | ), 87 | .library( 88 | name: "NotificationsPermissionsClientLive", 89 | type: .static, 90 | targets: ["NotificationsPermissionsClientLive"] 91 | ), 92 | 93 | // MARK: UserDefaults 94 | .library( 95 | name: "UserDefaultsClient", 96 | type: .static, 97 | targets: ["UserDefaultsClient"] 98 | ), 99 | .library( 100 | name: "UserDefaultsClientLive", 101 | type: .static, 102 | targets: ["UserDefaultsClientLive"] 103 | ), 104 | ], 105 | dependencies: [ 106 | .package( 107 | name: "swift-prelude", 108 | url: "https://github.com/capturecontext/swift-prelude.git", 109 | .upToNextMinor(from: "0.0.1") 110 | ) 111 | ], 112 | targets: [ 113 | // MARK: Exports 114 | .target( 115 | name: "StandardClients", 116 | dependencies: [ 117 | .target(name: "CacheClient"), 118 | .target(name: "HapticEngineClient"), 119 | .target(name: "IDFAPermissionsClient"), 120 | .target(name: "KeychainClient"), 121 | .target(name: "NotificationsPermissionsClient"), 122 | .target(name: "UserDefaultsClient") 123 | ] 124 | ), 125 | .target( 126 | name: "StandardClientsLive", 127 | dependencies: [ 128 | .target(name: "MemoryCacheClient"), 129 | .target(name: "HapticEngineClientLive"), 130 | .target(name: "IDFAPermissionsClientLive"), 131 | .target(name: "KeychainClientLive"), 132 | .target(name: "NotificationsPermissionsClientLive"), 133 | .target(name: "UserDefaultsClientLive") 134 | ] 135 | ), 136 | 137 | // MARK: Cache 138 | .target( 139 | name: "CacheClient", 140 | dependencies: [ 141 | .target(name: "DataRepresentable"), 142 | .product( 143 | name: "Prelude", 144 | package: "swift-prelude" 145 | ) 146 | ] 147 | ), 148 | .target( 149 | name: "MemoryCacheClient", 150 | dependencies: [ 151 | .target(name: "CacheClient") 152 | ] 153 | ), 154 | 155 | // MARK: DataRepresentable 156 | .target(name: "DataRepresentable"), 157 | 158 | // MARK: HapticEngine 159 | .target( 160 | name: "HapticEngineClient", 161 | dependencies: [ 162 | .product( 163 | name: "Prelude", 164 | package: "swift-prelude" 165 | ) 166 | ] 167 | ), 168 | .target( 169 | name: "HapticEngineClientLive", 170 | dependencies: [ 171 | .target(name: "HapticEngineClient") 172 | ] 173 | ), 174 | 175 | // MARK: IDFA 176 | .target( 177 | name: "IDFAPermissionsClient", 178 | dependencies: [ 179 | .product( 180 | name: "Prelude", 181 | package: "swift-prelude" 182 | ) 183 | ] 184 | ), 185 | .target( 186 | name: "IDFAPermissionsClientLive", 187 | dependencies: [ 188 | .target(name: "IDFAPermissionsClient") 189 | ] 190 | ), 191 | 192 | // MARK: Keychain 193 | .target( 194 | name: "KeychainClient", 195 | dependencies: [ 196 | .target(name: "DataRepresentable"), 197 | .product( 198 | name: "Prelude", 199 | package: "swift-prelude" 200 | ) 201 | ] 202 | ), 203 | .target( 204 | name: "KeychainClientLive", 205 | dependencies: [ 206 | .target(name: "KeychainClient") 207 | ], 208 | exclude: [ 209 | "Keychain/Licence/LICENCE", 210 | "Keychain/Licence/README" 211 | ] 212 | ), 213 | 214 | // MARK: Notifications 215 | .target( 216 | name: "NotificationsPermissionsClient", 217 | dependencies: [ 218 | .product( 219 | name: "Prelude", 220 | package: "swift-prelude" 221 | ) 222 | ] 223 | ), 224 | .target( 225 | name: "NotificationsPermissionsClientLive", 226 | dependencies: [ 227 | .target(name: "NotificationsPermissionsClient") 228 | ] 229 | ), 230 | 231 | // MARK: UserDefaults 232 | .target( 233 | name: "UserDefaultsClient", 234 | dependencies: [ 235 | .target(name: "DataRepresentable"), 236 | .product( 237 | name: "Prelude", 238 | package: "swift-prelude" 239 | ) 240 | ] 241 | ), 242 | .target( 243 | name: "UserDefaultsClientLive", 244 | dependencies: [ 245 | .target(name: "UserDefaultsClient") 246 | ] 247 | ), 248 | 249 | // MARK: - Tests 250 | 251 | // MARK: DataRepresentable 252 | .testTarget( 253 | name: "DataRepresentableTests", 254 | dependencies: [ 255 | .target(name: "DataRepresentable") 256 | ] 257 | ) 258 | ] 259 | ) 260 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swift-standard-clients 2 | 3 | [![SwiftPM 5.3](https://img.shields.io/badge/swiftpm-5.3-ED523F.svg?style=flat)](https://swift.org/download/) ![Platforms](https://img.shields.io/badge/Platforms-iOS_13_|_macOS_10.15_|_tvOS_14_|_watchOS_7-ED523F.svg?style=flat) [![@maximkrouk](https://img.shields.io/badge/contact-@capturecontext-1DA1F2.svg?style=flat&logo=twitter)](https://twitter.com/capture_context) 4 | 5 | Client declarations and live implementations for standard iOS managers 6 | 7 | > More info about client approach: 8 | > | Source | Description | 9 | > | ------------------------------------------------------------ | ----------------------------------------------- | 10 | > | [Brandon Williams - Protocol Witnesses](https://www.youtube.com/watch?v=3BVkbWXcFS4) | Talk on App Builders Conference 2019 | 11 | > | [Pointfree - Protocol Witnesses](https://www.pointfree.co/collections/protocol-witnesses) | Pointfree collection | 12 | > | [pointfree/isowords](https://github.com/pointfreeco/isowords/tree/main/Sources) | Examples of different clients can be found here | 13 | 14 | 15 | 16 | #### Table of contents 17 | 18 | | Description | Interface | Implementations | 19 | | ------------- | ------------- | ------------- | 20 | | [Caching](#Caching) | [CacheClient](Sources/CacheClient) | [MemoryCacheClient](Sources/MemoryCacheClientLive) | 21 | | [IDFA](#IDFA) | [IDFAPermissionsClient](Sources/IDFAPermissionsClient) | [IDFAPermissionsClientLive](Sources/IDFAPermissionsClientLive) | 22 | | [Keychain](#Keychain) | [KeychainClient](Sources/KeychainClient) | [KeychainClientLive](Sources/KeychainClientLive) | 23 | | [Notifications](#Notifications) | [NotificationsPermissionsClient](Sources/NotificationsPermissionsClient) | [NotificationsPermissionsClientLive](Sources/NotificationsPermissionsClientLive) | 24 | | [HapticEngine](#HapticEngine) | [HapticEngineClient](Sources/HapticEngineClient) | [HapticEngineClientLive](Sources/HapticEngineClientLive) | 25 | | [UserDefaults](#UserDefaults) | [UserDefaultsClient](Sources/UserDefaultsClient) | [UserDefaultsClientLive](Sources/UserDefaultsClientLive) | 26 | 27 | 28 | 29 | ### Todos 30 | 31 | - [ ] Improve readme by adding examples and simplifying descriptions. 32 | - [ ] Add LocalAuthenticationClient [ _Soon_ ] 33 | - [ ] Find out if it's better to use `Any`-based UserDefaults storage instead of `DataRepresentable`-based. 34 | - Add more tests 35 | - [ ] Caching 36 | - [ ] IDFA 37 | - [ ] Keychain 38 | - [ ] Notifications 39 | - [ ] HapticEngine 40 | - [ ] UserDefaults 41 | - [x] DataRepresentable 42 | 43 | 44 | 45 | ## Caching 46 | 47 | `CacheClient` is a generic client over hashable key and value, it provides interfaces for the following operations: 48 | 49 | - `saveValue(_: Value, forKey: Key)` 50 | - `loadValue(of: Value.Type = Value.self, forKey: Key) -> Value` 51 | - `removeValue(forKey: Key)` 52 | - `removeAllValues()` 53 | 54 | #### MemoryCacheClient 55 | 56 | `MemoryCacheClient` is build on top of `NSCache`. Under the hood it uses `MemoryCache` wrapper, improved version of [John Sundells' Cache](https://www.swiftbysundell.com/articles/caching-in-swift/). You can use `MemoryCache` (which also provides a way to save itself to disk if your types are codable) directly to build your own `CacheClient` implementations. 57 | 58 | 59 | 60 | ## IDFA 61 | 62 | `IDFAPermissionClient` is a client for `ASIdentifierManager` and `ATTrackingManager`, it provides interfaces for the following operations: 63 | 64 | - `requestAuthorizationStatus() -> AnyPublisher` 65 | - `requestAuthorization() -> AnyPublisher` 66 | - `requestIDFA() -> AnyPublisher` 67 | 68 | > `IDFAPermissionClient.AuthorizationStatus` is a wrapper for `ATTrackingManager.AuthorizationStatus` type and `ASIdentifierManager.isAdvertisingTrackingEnabled` value it's values are: 69 | > 70 | > - `notDetermined = "Not Determined"` `// ATTrackingManager.AuthorizationStatus.notDetermined` 71 | > - `restricted = "Restricted"` `// ATTrackingManager.AuthorizationStatus.restricted` 72 | > - `denied = "Denied"` `// ATTrackingManager.AuthorizationStatus.denied` 73 | > - `authorized = "Authorized"` `// ATTrackingManager.AuthorizationStatus.authorized` 74 | > - `unknown = "Unknown"` `// ATTrackingManager.AuthorizationStatus.unknown` 75 | > - `unavailableWithTrackingEnabled = "Unavailable: Tracking Enabled"` `// iOS<14 macOS<11, tvOS<14 ASIdentifierManager.shared().isAdvertisingTrackingEnabled == true` 76 | > - `unavailableWithTrackingDisabled = "Unavailable: Tracking Disabled"` `// iOS<14 macOS<11, tvOS<14 ASIdentifierManager.shared().isAdvertisingTrackingEnabled == false` 77 | > 78 | > It also has a computed property `isPermissive` which is `true` for `.authorized` and `.unavailableWithTrackingEnabled` 79 | 80 | 81 | 82 | ## Keychain 83 | 84 | `KeychainClient` is a client for Security framework keychain access, it **stores objects as data** (Using [`DataRepresentable`](#DataRepresentable) protocol) and provides interfaces for the following operations: 85 | 86 | - `saveValue(_: Value, forKey: Key, policy: AccessPolicy)` 87 | - `loadValue(of: Value.Type = Value.self, forKey: Key) -> Value` 88 | - `removeValue(forKey: Key)` 89 | 90 | `KeychainClient.Key` can be initialized by `rawValue: Stirng`, `StringLiteral` or `StringInterpolation`. Also you can use `.bundle(_:Key)` or `.bundle(_:Bundle, _:Key)` to add `bundleID` prefix to your key. 91 | 92 | > `KeychainClient.Operations.Save.AccessPolicy` is a wrapper for kSec access constants and it's values are: 93 | > 94 | > - `accessibleWhenUnlocked` `// kSecAttrAccessibleWhenUnlocked` 95 | > - `accessibleWhenUnlockedThisDeviceOnly` `// kSecAttrAccessibleWhenUnlockedThisDeviceOnly` 96 | > - `accessibleAfterFirstUnlock` `// kSecAttrAccessibleAfterFirstUnlock` 97 | > - `accessibleAfterFirstUnlockThisDeviceOnly` `// kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` 98 | > - `accessibleWhenPasscodeSetThisDeviceOnly` `// kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly` 99 | > - `accessibleAlways` `// kSecAttrAccessibleAlways` 100 | > - `accessibleAlwaysThisDeviceOnly` `// kSecAttrAccessibleAlwaysThisDeviceOnly` 101 | > 102 | 103 | 104 | 105 | ## Notifications 106 | 107 | `NotificationsPermissionsClient` is a client for `UNUserNotificationCenter`, it provides interfaces for the following operations: 108 | 109 | - `requestAuthorizationStatus() -> AnyPublisher` 110 | - `requestAuthorization(options: AuthorizationOptions) -> AnyPublisher` 111 | - `configureRemoteNotifications(_:)` // Pass `.register` or `.unregister` to the function 112 | 113 | > `NotificationsPermissionsClient.AuthorizationStatus` is a wrapper for `UNAuthorizationStatus` type it's values are: 114 | > 115 | > - `notDetermined` 116 | > 117 | > - `denied` 118 | > 119 | > - `authorized` 120 | > 121 | > - `provisional` 122 | > 123 | > - `ephemeral` `// iOS14+ only` 124 | > 125 | > It also has a computed property `isPermissive` which is true for `authorized`, `ephimeral` and `provisional` 126 | 127 | > `NotificationsPermissionsClient.AuthorizationOptions` is a wrapper for `UNAuthorizationOptions` type it's predefined values are: 128 | > 129 | > - `badge` 130 | > - `sound` 131 | > - `alert` 132 | > - `carPlay` 133 | > - `criticalAlert` 134 | > - `providesAppNotificationSettings` 135 | > - `provisional` 136 | > - `announcement` `// iOS only` 137 | > 138 | > You can also construct `AuthorizationOptions` object by providing `UInt` raw value. 139 | 140 | 141 | 142 | ## HapticEngine 143 | 144 | `HapticEngineClient` is a factory-client for `HapticFeedback` clients. `HapticFeedback` is a client for `UIFeedbackGenerator`. 145 | 146 | ### Usage 147 | 148 | ```swift 149 | import HapticEngineClientLive 150 | 151 | // If you need just one generator you can use HapticFeedback directly 152 | HapticFeedback.success.trigger() 153 | 154 | // Otherwise if you need more flexible way to create Haptic feedbacks use HapticEngineClient 155 | HapticEngineClient.live.generator(for: .success).trigger() 156 | ``` 157 | 158 | 159 | 160 | ## UserDefaults 161 | 162 | `UserDefaultsClient` is a client for UserDefaults object, it **stores objects as data** (Using [`DataRepresentable`](#DataRepresentable) protocol) and provides interfaces for the following operations: 163 | 164 | - `saveValue(_: Value, forKey: Key)` 165 | - `loadValue(of: Value.Type = Value.self, forKey: Key) -> Value` 166 | - `removeValue(forKey: Key)` 167 | 168 | `UserDefaultsClient.Key` can be initialized by `rawValue: Stirng`, `StringLiteral` or `StringInterpolation`. Also you can use `.bundle(_:Key)` or `.bundle(_:Bundle, _:Key)` to add `bundleID` prefix to your key. 169 | 170 | 171 | 172 | ## DataRepresentable 173 | 174 | [`DataRepresentable`](Sources/DataRepresentable) module provides a protocol for objects data representation. It is used by `UserDefaultsClient` and `KeychainClient` to store objects as data. 175 | 176 | 177 | 178 | ## Installation 179 | 180 | ### Basic 181 | 182 | You can add StandardClients to an Xcode project by adding it as a package dependency. 183 | 184 | 1. From the **File** menu, select **Swift Packages › Add Package Dependency…** 185 | 2. Enter [`"https://github.com/capturecontext/swift-standard-clients.git"`](https://github.com/capturecontext/swift-standard-clients.git) into the package repository URL text field 186 | 3. Choose products you need to link them to your project. 187 | 188 | ### Recommended 189 | 190 | If you use SwiftPM for your project, you can add StandardClients to your package file. 191 | 192 | ```swift 193 | .package( 194 | name: "swift-standard-clients", 195 | url: "https://github.com/capturecontext/swift-standard-clients.git", 196 | .upToNextMinor(from: "0.1.0") 197 | ) 198 | ``` 199 | 200 | Do not forget about target dependencies: 201 | 202 | ```swift 203 | .product( 204 | name: "SomeClientOrClientLive", 205 | package: "swift-standard-clients" 206 | ) 207 | ``` 208 | 209 | 210 | 211 | ## License 212 | 213 | This library is released under the MIT license. See [LICENSE](LICENSE) for details. 214 | -------------------------------------------------------------------------------- /Sources/CacheClient/CacheClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct CacheClient { 4 | public init( 5 | saveValue: Operations.Save, 6 | loadValue: Operations.Load, 7 | removeValue: Operations.Remove, 8 | removeAllValues: Operations.RemoveAllValues 9 | ) { 10 | self.saveValue = saveValue 11 | self.loadValue = loadValue 12 | self.removeValue = removeValue 13 | self.removeAllValues = removeAllValues 14 | } 15 | 16 | public var saveValue: Operations.Save 17 | public var loadValue: Operations.Load 18 | public var removeValue: Operations.Remove 19 | public var removeAllValues: Operations.RemoveAllValues 20 | } 21 | -------------------------------------------------------------------------------- /Sources/CacheClient/CacheClientOperations.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Prelude 3 | 4 | extension CacheClient { 5 | public enum Operations {} 6 | } 7 | 8 | extension CacheClient.Operations { 9 | public struct Save: Function { 10 | public typealias Input = (Key, Value, Int?) 11 | public typealias Output = Void 12 | 13 | public init(_ call: @escaping Signature) { 14 | self.call = call 15 | } 16 | 17 | public var call: Signature 18 | public func callAsFunction(_ value: Value, withCost cost: Int? = nil, forKey key: Key) { 19 | call((key, value, cost)) 20 | } 21 | } 22 | 23 | public struct Load: Function { 24 | public typealias Input = Key 25 | public typealias Output = Value? 26 | 27 | public init(_ call: @escaping Signature) { 28 | self.call = call 29 | } 30 | 31 | public var call: Signature 32 | public func callAsFunction(forKey key: Key) -> Value? { 33 | return call(key) 34 | } 35 | } 36 | 37 | public struct Remove: Function { 38 | public typealias Input = Key 39 | public typealias Output = Void 40 | 41 | public init(_ call: @escaping Signature) { 42 | self.call = call 43 | } 44 | 45 | public var call: Signature 46 | public func callAsFunction(forKey key: Key) { 47 | return call(key) 48 | } 49 | } 50 | 51 | public struct RemoveAllValues: Function { 52 | public typealias Input = Void 53 | public typealias Output = Void 54 | 55 | public init(_ call: @escaping Signature) { 56 | self.call = call 57 | } 58 | 59 | public var call: Signature 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /Sources/DataRepresentable/AnyDataRepresenable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct AnyDataRepresentable: DataRepresentable { 4 | private let data: () -> Data 5 | 6 | public init(_ representable: DataRepresentable) { 7 | self.init { representable.dataRepresentation } 8 | } 9 | 10 | public init(dataRepresentation: Data) { 11 | self.init { dataRepresentation } 12 | } 13 | 14 | public init(data: @escaping () -> Data) { 15 | self.data = data 16 | } 17 | 18 | public var dataRepresentation: Data { data() } 19 | } 20 | 21 | extension DataRepresentable { 22 | public func eraseToAnyDataRepresentable() -> AnyDataRepresentable { 23 | return AnyDataRepresentable(self) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/DataRepresentable/DataRepresentable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol DataRepresentable { 4 | var dataRepresentation: Data { get } 5 | init?(dataRepresentation: Data) 6 | } 7 | 8 | extension DataRepresentable { 9 | public static func converting(_ representable: DataRepresentable) -> Self? { 10 | return .init(dataRepresentation: representable.dataRepresentation) 11 | } 12 | } 13 | 14 | extension Data: DataRepresentable { 15 | public var dataRepresentation: Data { self } 16 | public init(dataRepresentation data: Data) { 17 | self = data 18 | } 19 | } 20 | 21 | extension String: DataRepresentable { 22 | public var dataRepresentation: Data { data(using: .utf8)! } 23 | 24 | public init?(dataRepresentation data: Data) { 25 | self.init(data: data, encoding: .utf8) 26 | } 27 | } 28 | 29 | extension Bool: DataRepresentable { 30 | public var dataRepresentation: Data { 31 | var value = self 32 | return Data(bytes: &value, count: MemoryLayout.size) 33 | } 34 | 35 | public init?(dataRepresentation: Data) { 36 | let size = MemoryLayout.size 37 | guard dataRepresentation.count == size else { return nil } 38 | var value: Bool = false 39 | let actualSize = withUnsafeMutableBytes(of: &value, { 40 | dataRepresentation.copyBytes(to: $0) 41 | }) 42 | assert(actualSize == MemoryLayout.size(ofValue: value)) 43 | self = value 44 | } 45 | } 46 | 47 | extension Numeric { 48 | public var dataRepresentation: Data { 49 | var value = self 50 | return Data(bytes: &value, count: MemoryLayout.size) 51 | } 52 | 53 | public init?(dataRepresentation: Data) { 54 | let size = MemoryLayout.size 55 | guard dataRepresentation.count == size else { return nil } 56 | var value: Self = .zero 57 | let actualSize = withUnsafeMutableBytes(of: &value, { 58 | dataRepresentation.copyBytes(to: $0) 59 | }) 60 | assert(actualSize == MemoryLayout.size(ofValue: value)) 61 | self = value 62 | } 63 | } 64 | 65 | extension Int: DataRepresentable {} 66 | extension Int8: DataRepresentable {} 67 | extension Int16: DataRepresentable {} 68 | extension Int32: DataRepresentable {} 69 | extension Int64: DataRepresentable {} 70 | 71 | extension UInt: DataRepresentable {} 72 | extension UInt8: DataRepresentable {} 73 | extension UInt16: DataRepresentable {} 74 | extension UInt32: DataRepresentable {} 75 | extension UInt64: DataRepresentable {} 76 | 77 | extension Double: DataRepresentable {} 78 | extension Float: DataRepresentable {} 79 | 80 | #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) 81 | import CoreGraphics 82 | extension CGFloat: DataRepresentable {} 83 | #endif 84 | -------------------------------------------------------------------------------- /Sources/HapticEngineClient/HapticEngineClient.swift: -------------------------------------------------------------------------------- 1 | public struct HapticEngineClient { 2 | public init(generator: Operations.CreateFeedback) { 3 | self.generator = generator 4 | } 5 | 6 | public var generator: Operations.CreateFeedback 7 | } 8 | -------------------------------------------------------------------------------- /Sources/HapticEngineClient/HapticEngineClientOperations.swift: -------------------------------------------------------------------------------- 1 | import Prelude 2 | 3 | extension HapticEngineClient { 4 | public enum Operations {} 5 | } 6 | 7 | extension HapticEngineClient.Operations { 8 | public struct CreateFeedback: Function { 9 | public enum Input: Equatable { 10 | case success 11 | case warning 12 | case error 13 | case selection 14 | 15 | case light(intensity: Double? = nil) 16 | case medium(intensity: Double? = nil) 17 | case heavy(intensity: Double? = nil) 18 | case soft(intensity: Double? = nil) 19 | case rigid(intensity: Double? = nil) 20 | } 21 | 22 | public typealias Output = HapticFeedback 23 | 24 | public init(_ call: @escaping Signature) { 25 | self.call = call 26 | } 27 | 28 | public var call: Signature 29 | 30 | public func callAsFunction(for input: Input) -> Output { 31 | return call(input) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/HapticEngineClient/HapticFeedback.swift: -------------------------------------------------------------------------------- 1 | public struct HapticFeedback { 2 | public init( 3 | prepare: @escaping () -> Void, 4 | action: @escaping () -> Void 5 | ) { 6 | self._prepare = prepare 7 | self._action = action 8 | } 9 | 10 | private let _prepare: () -> Void 11 | private let _action: () -> Void 12 | public func prepare() { _prepare() } 13 | public func trigger() { _action() } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/HapticEngineClientLive/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import HapticEngineClient 2 | -------------------------------------------------------------------------------- /Sources/HapticEngineClientLive/HapticEngineClientLive.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) 2 | import HapticEngineClient 3 | import UIKit 4 | 5 | extension HapticEngineClient { 6 | public static let live: HapticEngineClient = .init(generator: .live) 7 | } 8 | 9 | extension HapticEngineClient.Operations.CreateFeedback { 10 | public static var live: Self { 11 | return .init { input in 12 | switch input { 13 | case .success : return .success 14 | case .warning : return .warning 15 | case .error : return .error 16 | case .selection : return .selection 17 | 18 | case let .light(intensity): 19 | guard let intensity = intensity 20 | else { return .light } 21 | return .light(intensity: CGFloat(intensity)) 22 | 23 | case let .medium(intensity): 24 | guard let intensity = intensity 25 | else { return .medium } 26 | return .medium(intensity: CGFloat(intensity)) 27 | 28 | case let .heavy(intensity): 29 | guard let intensity = intensity 30 | else { return .heavy } 31 | return .heavy(intensity: CGFloat(intensity)) 32 | 33 | case let .soft(intensity): 34 | guard let intensity = intensity 35 | else { return .soft } 36 | return .soft(intensity: CGFloat(intensity)) 37 | 38 | case let .rigid(intensity): 39 | guard let intensity = intensity 40 | else { return .rigid } 41 | return .rigid(intensity: CGFloat(intensity)) 42 | 43 | } 44 | } 45 | } 46 | } 47 | #endif 48 | -------------------------------------------------------------------------------- /Sources/HapticEngineClientLive/HapticFeedback.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) 2 | import HapticEngineClient 3 | import UIKit 4 | 5 | extension HapticFeedback { 6 | public static func custom( 7 | _ generator: Generator, 8 | _ action: @escaping (Generator) -> Void 9 | ) -> HapticFeedback { 10 | HapticFeedback( 11 | prepare: { generator.prepare() }, 12 | action: { action(generator) } 13 | ) 14 | } 15 | 16 | public static var light: HapticFeedback { 17 | .custom(UIImpactFeedbackGenerator(style: .light)) { 18 | $0.impactOccurred() 19 | } 20 | } 21 | 22 | public static var medium: HapticFeedback { 23 | .custom(UIImpactFeedbackGenerator(style: .medium)) { 24 | $0.impactOccurred() 25 | } 26 | } 27 | 28 | public static var heavy: HapticFeedback { 29 | .custom(UIImpactFeedbackGenerator(style: .heavy)) { 30 | $0.impactOccurred() 31 | } 32 | } 33 | 34 | public static var soft: HapticFeedback { 35 | .custom(UIImpactFeedbackGenerator(style: .soft)) { 36 | $0.impactOccurred() 37 | } 38 | } 39 | 40 | public static var rigid: HapticFeedback { 41 | .custom(UIImpactFeedbackGenerator(style: .rigid)) { 42 | $0.impactOccurred() 43 | } 44 | } 45 | 46 | public static func light(intensity: CGFloat) -> HapticFeedback { 47 | .custom(UIImpactFeedbackGenerator(style: .light)) { 48 | $0.impactOccurred(intensity: intensity) 49 | } 50 | } 51 | 52 | public static func medium(intensity: CGFloat) -> HapticFeedback { 53 | .custom(UIImpactFeedbackGenerator(style: .medium)) { 54 | $0.impactOccurred(intensity: intensity) 55 | } 56 | } 57 | 58 | public static func heavy(intensity: CGFloat) -> HapticFeedback { 59 | .custom(UIImpactFeedbackGenerator(style: .heavy)) { 60 | $0.impactOccurred(intensity: intensity) 61 | } 62 | } 63 | 64 | public static func soft(intensity: CGFloat) -> HapticFeedback { 65 | .custom(UIImpactFeedbackGenerator(style: .soft)) { 66 | $0.impactOccurred(intensity: intensity) 67 | } 68 | } 69 | 70 | public static func rigid(intensity: CGFloat) -> HapticFeedback { 71 | .custom(UIImpactFeedbackGenerator(style: .rigid)) { 72 | $0.impactOccurred(intensity: intensity) 73 | } 74 | } 75 | 76 | public static var success: HapticFeedback { 77 | .custom(UINotificationFeedbackGenerator()) { 78 | $0.notificationOccurred(.success) 79 | } 80 | } 81 | 82 | public static var warning: HapticFeedback { 83 | .custom(UINotificationFeedbackGenerator()) { 84 | $0.notificationOccurred(.success) 85 | } 86 | } 87 | 88 | public static var error: HapticFeedback { 89 | .custom(UINotificationFeedbackGenerator()) { 90 | $0.notificationOccurred(.success) 91 | } 92 | } 93 | 94 | public static var selection: HapticFeedback { 95 | .custom(UISelectionFeedbackGenerator()) { 96 | $0.selectionChanged() 97 | } 98 | } 99 | } 100 | #endif 101 | 102 | 103 | -------------------------------------------------------------------------------- /Sources/IDFAPermissionsClient/IDFAPermissionsClient.swift: -------------------------------------------------------------------------------- 1 | public struct IDFAPermissionsClient { 2 | public init( 3 | requestAuthorizationStatus: Operations.RequestAuthorizationStatus, 4 | requestAuthorization: Operations.RequestAuthorization, 5 | requestIDFA: Operations.RequestIDFA 6 | ) { 7 | self.requestAuthorizationStatus = requestAuthorizationStatus 8 | self.requestAuthorization = requestAuthorization 9 | self.requestIDFA = requestIDFA 10 | } 11 | 12 | public var requestAuthorizationStatus: Operations.RequestAuthorizationStatus 13 | public var requestAuthorization: Operations.RequestAuthorization 14 | public var requestIDFA: Operations.RequestIDFA 15 | } 16 | 17 | extension IDFAPermissionsClient { 18 | public struct AuthorizationStatus: RawRepresentable, Equatable { 19 | public enum RawValue: Equatable { 20 | public enum TrackingManagerStatus: Int, Equatable { 21 | case notDetermined = 0 22 | case restricted = 1 23 | case denied = 2 24 | case authorized = 3 25 | } 26 | 27 | case trackingManager(TrackingManagerStatus) 28 | case identifierManager(isTrackingEnabled: Bool) 29 | case unknown 30 | } 31 | 32 | public var rawValue: RawValue 33 | 34 | public init(rawValue: RawValue) { 35 | self.rawValue = rawValue 36 | } 37 | 38 | public static var notDetermined: AuthorizationStatus { 39 | return .init(rawValue: .trackingManager(.notDetermined)) 40 | } 41 | 42 | public static var restricted: AuthorizationStatus { 43 | return .init(rawValue: .trackingManager(.restricted)) 44 | } 45 | 46 | public static var denied: AuthorizationStatus { 47 | return .init(rawValue: .trackingManager(.denied)) 48 | } 49 | 50 | public static var authorized: AuthorizationStatus { 51 | return .init(rawValue: .trackingManager(.authorized)) 52 | } 53 | 54 | public static var unknown: AuthorizationStatus { 55 | return .init(rawValue: .unknown) 56 | } 57 | 58 | public static var unavailableWithTrackingEnabled: AuthorizationStatus { 59 | return .init(rawValue: .identifierManager(isTrackingEnabled: true)) 60 | } 61 | 62 | public static var unavailableWithTrackingDisabled: AuthorizationStatus { 63 | return .init(rawValue: .identifierManager(isTrackingEnabled: false)) 64 | } 65 | 66 | public var isPermissive: Bool { 67 | self == .authorized || self == .unavailableWithTrackingEnabled 68 | } 69 | } 70 | } 71 | 72 | public func ~=( 73 | rhs: IDFAPermissionsClient.AuthorizationStatus, 74 | lhs: IDFAPermissionsClient.AuthorizationStatus 75 | ) -> Bool { return rhs == lhs } 76 | -------------------------------------------------------------------------------- /Sources/IDFAPermissionsClient/IDFAPermissionsClientOperations.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Prelude 3 | import Foundation 4 | 5 | extension IDFAPermissionsClient { 6 | public enum Operations {} 7 | } 8 | 9 | extension IDFAPermissionsClient.Operations { 10 | public struct RequestAuthorizationStatus: Function { 11 | public typealias Input = Void 12 | public typealias Output = AnyPublisher 13 | 14 | public init(_ call: @escaping Signature) { 15 | self.call = call 16 | } 17 | 18 | public let call: Signature 19 | } 20 | 21 | public struct RequestAuthorization: Function { 22 | public typealias Input = Void 23 | public typealias Output = AnyPublisher 24 | 25 | public init(_ call: @escaping Signature) { 26 | self.call = call 27 | } 28 | 29 | public let call: Signature 30 | } 31 | 32 | public struct RequestIDFA: Function { 33 | public typealias Input = Void 34 | public typealias Output = AnyPublisher 35 | 36 | public init(_ call: @escaping Signature) { 37 | self.call = call 38 | } 39 | 40 | public let call: Signature 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/IDFAPermissionsClientLive/AuthorizationStatus.swift: -------------------------------------------------------------------------------- 1 | #if !os(watchOS) 2 | import AppTrackingTransparency 3 | import IDFAPermissionsClient 4 | 5 | extension IDFAPermissionsClient.AuthorizationStatus { 6 | @available(iOS 14, macOS 11, tvOS 14, *) 7 | public init(_ status: ATTrackingManager.AuthorizationStatus) { 8 | switch status { 9 | case .notDetermined: self = .notDetermined 10 | case .authorized: self = .authorized 11 | case .restricted: self = .restricted 12 | case .denied: self = .denied 13 | @unknown default: self = .unknown 14 | } 15 | } 16 | 17 | @available(iOS 14, macOS 11, tvOS 14, *) 18 | public var status: ATTrackingManager.AuthorizationStatus? { 19 | switch self { 20 | case .notDetermined: return .notDetermined 21 | case .authorized: return .authorized 22 | case .restricted: return .restricted 23 | case .denied: return .denied 24 | default: return .none 25 | } 26 | } 27 | } 28 | #endif 29 | -------------------------------------------------------------------------------- /Sources/IDFAPermissionsClientLive/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import IDFAPermissionsClient 2 | -------------------------------------------------------------------------------- /Sources/IDFAPermissionsClientLive/IDFAPermissionsClientLive.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import IDFAPermissionsClient 3 | 4 | #if !os(watchOS) 5 | import AdSupport 6 | import AppTrackingTransparency 7 | 8 | extension IDFAPermissionsClient { 9 | public static let live = IDFAPermissionsClient( 10 | requestAuthorizationStatus: .live, 11 | requestAuthorization: .live, 12 | requestIDFA: .live 13 | ) 14 | } 15 | 16 | extension IDFAPermissionsClient.Operations.RequestAuthorizationStatus { 17 | public static var live: IDFAPermissionsClient.Operations.RequestAuthorizationStatus { 18 | return .init { 19 | Future { promise in 20 | if #available(iOS 14.0, macOS 11, tvOS 14, *) { 21 | promise(.success(.init(ATTrackingManager.trackingAuthorizationStatus))) 22 | } else { 23 | promise( 24 | .success( 25 | ASIdentifierManager.shared().isAdvertisingTrackingEnabled 26 | ? .unavailableWithTrackingEnabled 27 | : .unavailableWithTrackingDisabled 28 | ) 29 | ) 30 | } 31 | }.eraseToAnyPublisher() 32 | } 33 | } 34 | } 35 | 36 | extension IDFAPermissionsClient.Operations.RequestAuthorization { 37 | public static var live: IDFAPermissionsClient.Operations.RequestAuthorization { 38 | return.init { params in 39 | Future { promise in 40 | if #available(iOS 14.0, macOS 11, tvOS 14, *) { 41 | ATTrackingManager.requestTrackingAuthorization { status in 42 | promise(.success(.init(status))) 43 | } 44 | } else { 45 | promise( 46 | .success( 47 | ASIdentifierManager.shared().isAdvertisingTrackingEnabled 48 | ? .unavailableWithTrackingEnabled 49 | : .unavailableWithTrackingDisabled 50 | ) 51 | ) 52 | } 53 | }.eraseToAnyPublisher() 54 | } 55 | } 56 | } 57 | 58 | extension IDFAPermissionsClient.Operations.RequestIDFA { 59 | public static var live: IDFAPermissionsClient.Operations.RequestIDFA { 60 | .init { action in 61 | Just(ASIdentifierManager.shared().advertisingIdentifier).eraseToAnyPublisher() 62 | } 63 | } 64 | } 65 | #endif 66 | -------------------------------------------------------------------------------- /Sources/KeychainClient/KeychainClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct KeychainClient { 4 | public init( 5 | saveValue: Operations.Save, 6 | loadValue: Operations.Load, 7 | removeValue: Operations.Remove 8 | ) { 9 | self.saveValue = saveValue 10 | self.loadValue = loadValue 11 | self.removeValue = removeValue 12 | } 13 | 14 | public var saveValue: Operations.Save 15 | public var loadValue: Operations.Load 16 | public var removeValue: Operations.Remove 17 | } 18 | 19 | extension KeychainClient { 20 | public struct Key: 21 | RawRepresentable, 22 | ExpressibleByStringLiteral, 23 | ExpressibleByStringInterpolation 24 | { 25 | public var rawValue: String 26 | 27 | public init(rawValue: String) { 28 | self.rawValue = rawValue 29 | } 30 | 31 | public init(stringLiteral value: String) { 32 | self.init(rawValue: value) 33 | } 34 | 35 | public static func bundle(_ key: Key) -> Key { 36 | return .bundle(.main, key) 37 | } 38 | 39 | public static func bundle(_ bundle: Bundle?, _ key: Key) -> Key { 40 | let prefix = bundle?.bundleIdentifier.map { $0.appending(".") } ?? "" 41 | return .init(rawValue: prefix.appending(key.rawValue)) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/KeychainClient/KeychainClientOperations.swift: -------------------------------------------------------------------------------- 1 | import DataRepresentable 2 | import Foundation 3 | import Prelude 4 | 5 | extension KeychainClient { 6 | public enum Operations {} 7 | } 8 | 9 | extension KeychainClient.Operations { 10 | public struct Save: Function { 11 | public typealias Input = (KeychainClient.Key, Data, AccessPolicy) 12 | public typealias Output = Void 13 | 14 | public init(_ call: @escaping Signature) { 15 | self.call = call 16 | } 17 | 18 | public var call: Signature 19 | 20 | public func callAsFunction( 21 | _ value: DataRepresentable, 22 | forKey key: KeychainClient.Key, 23 | policy: AccessPolicy = .default 24 | ) { return call((key, value.dataRepresentation, policy)) } 25 | 26 | public enum AccessPolicy { 27 | public static var `default`: AccessPolicy { .accessibleWhenUnlocked } 28 | case accessibleWhenUnlocked 29 | case accessibleWhenUnlockedThisDeviceOnly 30 | case accessibleAfterFirstUnlock 31 | case accessibleAfterFirstUnlockThisDeviceOnly 32 | case accessibleWhenPasscodeSetThisDeviceOnly 33 | 34 | @available( 35 | iOS, 36 | introduced: 4.0, 37 | deprecated: 12.0, 38 | message: 39 | "Use an accessibility level that provides some user protection, such as .accessibleAfterFirstUnlock" 40 | ) 41 | case accessibleAlways 42 | 43 | @available( 44 | iOS, 45 | introduced: 4.0, 46 | deprecated: 12.0, 47 | message: 48 | "Use an accessibility level that provides some user protection, such as .accessibleAfterFirstUnlockThisDeviceOnly" 49 | ) 50 | case accessibleAlwaysThisDeviceOnly 51 | } 52 | } 53 | 54 | public struct Load: Function { 55 | public typealias Input = KeychainClient.Key 56 | public typealias Output = Data? 57 | 58 | public init(_ call: @escaping Signature) { 59 | self.call = call 60 | } 61 | 62 | public var call: Signature 63 | 64 | public func callAsFunction( 65 | of type: Value.Type = Value.self, 66 | forKey key: KeychainClient.Key 67 | ) -> Value? { 68 | return call(key).flatMap(Value.init(dataRepresentation:)) 69 | } 70 | } 71 | 72 | public struct Remove: Function { 73 | public typealias Input = KeychainClient.Key 74 | public typealias Output = Void 75 | 76 | public init(_ call: @escaping Signature) { 77 | self.call = call 78 | } 79 | 80 | public var call: Signature 81 | public func callAsFunction(forKey key: KeychainClient.Key) { 82 | return call(key) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/KeychainClientLive/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import KeychainClient 2 | -------------------------------------------------------------------------------- /Sources/KeychainClientLive/Keychain/CoreFoundation+.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Security) 2 | import Security 3 | 4 | extension CFString { 5 | var string: String { 6 | self as String 7 | } 8 | } 9 | 10 | extension Bool { 11 | var cfBoolean: CFBoolean { self ? kCFBooleanTrue : kCFBooleanFalse } 12 | } 13 | #endif 14 | -------------------------------------------------------------------------------- /Sources/KeychainClientLive/Keychain/Keychain+AccessOptions.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Security) 2 | import Foundation 3 | import Security 4 | 5 | extension Keychain { 6 | 7 | /// Options, used to determine keychain item access. The default value is accessibleWhenUnlocked. 8 | public enum AccessPolicy { 9 | /// Default access option. 10 | /// 11 | /// Returns: .accessibleWhenUnlocked 12 | public static var `default`: AccessPolicy { .accessibleWhenUnlocked } 13 | 14 | /// Access provided only if the device is unlocked by the user. 15 | /// 16 | /// Recommended for items that need to be accessible only while the application is in the foreground. 17 | /// - Items with this attribute migrate to a new device when using encrypted backups. 18 | case accessibleWhenUnlocked 19 | 20 | /// Access provided only if the device is unlocked by the user. 21 | /// 22 | /// This is recommended for items that need to be accessible only while the application is in the foreground. 23 | /// - Items with this attribute do not migrate to a new device, 24 | case accessibleWhenUnlockedThisDeviceOnly 25 | 26 | /// Access is not provided after a restart until the device has been unlocked once by the user. 27 | /// After the first unlock, the data remains accessible until the next restart. 28 | /// 29 | /// Recommended for items that need to be accessed by background applications. 30 | /// - Items with this attribute migrate to a new device when using encrypted backups. 31 | case accessibleAfterFirstUnlock 32 | 33 | /// Access is not provided after a restart until the device has been unlocked once by the user. 34 | /// After the first unlock, the data remains accessible until the next restart. 35 | /// 36 | /// Recommended for items that need to be accessed by background applications. 37 | /// - Items with this attribute do not migrate to a new device. 38 | case accessibleAfterFirstUnlockThisDeviceOnly 39 | 40 | /// Access is provided when the device is unlocked. Only available if a passcode is set on the device. 41 | /// 42 | /// This is recommended for items that only need to be accessible while the application is in the foreground. 43 | /// - Items with this attribute never migrate to a new device. 44 | /// - After a backup is restored to a new device, these items are missing. 45 | /// - No items can be stored in this class on devices without a passcode. 46 | /// - Disabling the device passcode causes all items in this class to be deleted. 47 | case accessibleWhenPasscodeSetThisDeviceOnly 48 | 49 | /// Access is provided regardless of whether the device is locked. 50 | /// 51 | /// - Items with this attribute migrate to a new device when using encrypted backups. 52 | @available( 53 | iOS, 54 | introduced: 4.0, 55 | deprecated: 12.0, 56 | message: 57 | "Use an accessibility level that provides some user protection, such as .accessibleAfterFirstUnlock" 58 | ) 59 | case accessibleAlways 60 | /// 61 | 62 | /// Access is provided regardless of whether the device is locked. 63 | /// 64 | /// - Items with this attribute do not migrate to a new device. 65 | @available( 66 | iOS, 67 | introduced: 4.0, 68 | deprecated: 12.0, 69 | message: 70 | "Use an accessibility level that provides some user protection, such as .accessibleAfterFirstUnlockThisDeviceOnly" 71 | ) 72 | case accessibleAlwaysThisDeviceOnly 73 | 74 | public var value: CFString { 75 | switch self { 76 | case .accessibleWhenUnlocked: 77 | return kSecAttrAccessibleWhenUnlocked 78 | case .accessibleWhenUnlockedThisDeviceOnly: 79 | return kSecAttrAccessibleWhenUnlockedThisDeviceOnly 80 | case .accessibleAfterFirstUnlock: 81 | return kSecAttrAccessibleAfterFirstUnlock 82 | case .accessibleAfterFirstUnlockThisDeviceOnly: 83 | return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly 84 | case .accessibleWhenPasscodeSetThisDeviceOnly: 85 | return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly 86 | case .accessibleAlways: 87 | return kSecAttrAccessibleAlways 88 | case .accessibleAlwaysThisDeviceOnly: 89 | return kSecAttrAccessibleAlwaysThisDeviceOnly 90 | } 91 | } 92 | } 93 | 94 | } 95 | #endif 96 | -------------------------------------------------------------------------------- /Sources/KeychainClientLive/Keychain/Keychain+Constants.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Security) 2 | import Foundation 3 | import Security 4 | 5 | extension Keychain { 6 | 7 | /// Constants used by the library 8 | public enum Key { 9 | /// Specifies a Keychain access group. Used for sharing Keychain items between apps. 10 | case accessGroup 11 | 12 | /// A value that indicates when your app needs access to the data in a keychain item. 13 | /// 14 | /// The default value is .accessibleWhenUnlocked. 15 | /// 16 | /// For a list of possible values, see Keychain.AccessOption. 17 | case accessible 18 | 19 | /// Used for specifying a String key when setting/getting a Keychain value. 20 | case account 21 | 22 | /// Used for specifying synchronization of keychain items between devices. 23 | case synchronizable 24 | 25 | /// An item class key used to construct a Keychain search dictionary. 26 | case `class` 27 | 28 | /// Specifies the number of values returned from the keychain. The library only supports single values. 29 | case matchLimit 30 | 31 | /// A return data type used to get the data from the Keychain. 32 | case returnData 33 | 34 | /// Used for specifying a value when setting a Keychain value. 35 | case valueData 36 | 37 | /// Used for returning a reference to the data from the keychain 38 | case persistentRef 39 | 40 | /// String value, of the key. 41 | var value: String { 42 | switch self { 43 | case .accessGroup: 44 | return kSecAttrAccessGroup.string 45 | case .accessible: 46 | return kSecAttrAccessible.string 47 | case .account: 48 | return kSecAttrAccount.string 49 | case .synchronizable: 50 | return kSecAttrSynchronizable.string 51 | case .`class`: 52 | return kSecClass.string 53 | case .matchLimit: 54 | return kSecMatchLimit.string 55 | case .returnData: 56 | return kSecReturnData.string 57 | case .valueData: 58 | return kSecValueData.string 59 | case .persistentRef: 60 | return kSecReturnPersistentRef.string 61 | } 62 | } 63 | } 64 | 65 | } 66 | #endif 67 | -------------------------------------------------------------------------------- /Sources/KeychainClientLive/Keychain/Keychain+Error.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Security) 2 | import Foundation 3 | 4 | extension Keychain { 5 | public enum Error: Swift.Error { 6 | case osError(OSStatus) 7 | case custom(message: String) 8 | case unknown 9 | var localizedDescription: String { 10 | switch self { 11 | case .osError(let code): 12 | return "Keychain error. OSStatus code: \(code)" 13 | case .custom(let message): 14 | return message 15 | case .unknown: 16 | return "Unknown error" 17 | } 18 | } 19 | } 20 | } 21 | #endif 22 | -------------------------------------------------------------------------------- /Sources/KeychainClientLive/Keychain/Keychain+OperationResult.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Security) 2 | import Foundation 3 | import Security 4 | 5 | extension Keychain { 6 | 7 | public struct OperationResult { 8 | /// Result code. Equal to noErr (0) if operation succeed. 9 | var code: OSStatus 10 | 11 | /// Creates a new instance. 12 | /// 13 | /// - Parameter code: Result code, noErr (0) by default. 14 | init(code: OSStatus = noErr) { 15 | self.code = code 16 | } 17 | 18 | var isSuccess: Bool { code == noErr } 19 | var result: Result { 20 | isSuccess ? .success(()) : .failure(Error.osError(code)) 21 | } 22 | } 23 | 24 | } 25 | #endif 26 | -------------------------------------------------------------------------------- /Sources/KeychainClientLive/Keychain/Keychain.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Security) 2 | import Foundation 3 | 4 | extension Keychain.Query { 5 | 6 | /// Adds specified access group attribute to the query. 7 | fileprivate mutating func addAccessGroup(_ accessGroup: String) { 8 | self[Keychain.Key.accessGroup.value] = accessGroup 9 | } 10 | 11 | /// Adds specified synchronizability group attribute to the query. 12 | /// 13 | /// Adds kSecAttrSynchronizableAny if passed value was nil. 14 | /// - Parameter value: Pass true if you save an item, false if you read, nil if you delete. 15 | fileprivate mutating func addSynchronizability(value: Bool? = nil) { 16 | self[Keychain.Key.synchronizable.value] = value ?? kSecAttrSynchronizableAny 17 | } 18 | 19 | /// Returns self as CFDictionary. 20 | fileprivate var cfDictionary: CFDictionary { self as CFDictionary } 21 | 22 | } 23 | 24 | public final class Keychain { 25 | typealias Query = [String: Any] 26 | typealias Attribute = Query.Element 27 | 28 | // MARK: - Properties 29 | 30 | private let lock = NSLock() 31 | 32 | public static let `default`: Keychain = { 33 | let prefix = Bundle.main.bundleIdentifier.map { $0.appending(".") } ?? "" 34 | return Keychain(keyPrefix: prefix, accessGroup: Bundle.main.bundleIdentifier) 35 | }() 36 | 37 | /// Contains result code from the last operation. 38 | /// 39 | /// If code is noErr (0) the operation succeed. 40 | private(set) public var lastResult: OperationResult = .init() 41 | 42 | /// A prefix that is added before the key. 43 | /// 44 | /// Note that `clear` method still clears all of the application data from the Keychain. 45 | private(set) public var keyPrefix: String 46 | 47 | /// Access group that will be used to access keychain items. 48 | /// 49 | /// Access groups can be used to share keychain items between applications. 50 | /// When access group value is nil all application access groups are being accessed. 51 | private(set) public var accessGroup: String? 52 | 53 | /// Specifies whether the items can be synchronized with other devices through iCloud. 54 | /// 55 | /// Setting this property to true will add the item to other devices. 56 | /// Default value is false. 57 | /// Deleting synchronizable items will remove them from all devices. 58 | /// In order for keychain synchronization to work the user must enable "Keychain" in iCloud settings. 59 | /// Does not work on OSX. 60 | public var isSynchronizable: Bool = false 61 | 62 | // MARK: - Init 63 | 64 | /// Creates a new object. 65 | /// 66 | /// - Parameter keyPrefix: A prefix that is added before the key. 67 | /// Note that `clear` method still clears all of the application items from the Keychain. 68 | public init(keyPrefix: String = "", accessGroup: String? = nil) { 69 | self.keyPrefix = keyPrefix 70 | self.accessGroup = accessGroup 71 | } 72 | 73 | // MARK: - StorageManager 74 | 75 | public func loadData(forKey key: String) -> Result { 76 | getData(forKey: key) 77 | } 78 | 79 | public func saveData(_ data: Data, forKey key: String) 80 | -> Result 81 | { 82 | setData(data, forKey: key) 83 | } 84 | 85 | @discardableResult 86 | public func delete(key: String) -> Result { 87 | lock.execute { unsafeDelete(key) } 88 | } 89 | 90 | @discardableResult 91 | public func clear() -> Result { 92 | lock.execute { 93 | var query: Query = [ 94 | Key.class.value: kSecClassGenericPassword, 95 | Key.synchronizable.value: kSecAttrSynchronizableAny, 96 | ] 97 | 98 | if let group = accessGroup { query.addAccessGroup(group) } 99 | if isSynchronizable { query.addSynchronizability() } 100 | 101 | lastResult.code = SecItemDelete(query.cfDictionary) 102 | return lastResult.result 103 | } 104 | } 105 | 106 | @discardableResult 107 | public func delete(all keys: Keys.Type) -> [Result] 108 | where Keys: RawRepresentable, Keys.RawValue == String { 109 | keys.allCases.map { self.delete(key: $0.rawValue) } 110 | } 111 | 112 | // MARK: - Methods 113 | 114 | /// Stores the data in the keychain. 115 | /// 116 | /// - Parameter key: Key under which the data is stored in the keychain. 117 | /// - Parameter value: Data to be written to the keychain. 118 | /// - Parameter withAccess: Value that indicates when your app needs access to the text in the keychain item. 119 | /// 120 | /// - Returns: .success if the data was successfully written to the keychain. 121 | @discardableResult 122 | public func setData( 123 | _ data: Data, 124 | forKey key: String, 125 | policy: AccessPolicy = .default 126 | ) -> Result { 127 | lock.execute { 128 | unsafeDelete(key) 129 | 130 | let accessible = policy.value 131 | var query: [String: Any] = [ 132 | Key.class.value: kSecClassGenericPassword, 133 | Key.account.value: keyPrefix.appending(key), 134 | Key.valueData.value: data, 135 | Key.accessible.value: accessible, 136 | ] 137 | 138 | if let group = accessGroup { query.addAccessGroup(group) } 139 | if isSynchronizable { query.addSynchronizability(value: true) } 140 | 141 | lastResult.code = SecItemAdd(query.cfDictionary, nil) 142 | return lastResult.result 143 | } 144 | } 145 | 146 | /// Retrieves the data from the keychain. 147 | /// 148 | /// - Parameter key: The key that is used to read the keychain item. 149 | /// - Parameter asReference: If true, returns the data as reference (needed for things like NEVPNProtocol). 150 | /// - Returns: The data from the keychain. Returns nil if unable to read the item. 151 | public func getData( 152 | forKey key: String, 153 | asReference: Bool = false 154 | ) -> Result { 155 | lock.execute { 156 | var query: [String: Any] = [ 157 | Key.class.value: kSecClassGenericPassword, 158 | Key.account.value: keyPrefix.appending(key), 159 | Key.matchLimit.value: kSecMatchLimitOne, 160 | ] 161 | 162 | if asReference { 163 | query[Key.persistentRef.value] = true.cfBoolean 164 | } else { 165 | query[Key.returnData.value] = true.cfBoolean 166 | } 167 | 168 | if let group = accessGroup { query.addAccessGroup(group) } 169 | if isSynchronizable { query.addSynchronizability(value: false) } 170 | 171 | var result: AnyObject? 172 | lastResult.code = withUnsafeMutablePointer(to: &result) { 173 | SecItemCopyMatching(query.cfDictionary, UnsafeMutablePointer($0)) 174 | } 175 | 176 | if let data = result as? Data { 177 | return .success(data) 178 | } else { 179 | return .failure( 180 | Error 181 | .custom(message: "Couldn't fetch data from the keychain.") 182 | ) 183 | } 184 | } 185 | } 186 | 187 | /// Thread-unsafe delete method. 188 | @discardableResult 189 | private func unsafeDelete(_ key: String) -> Result { 190 | var query: [String: Any] = [ 191 | Key.class.value: kSecClassGenericPassword, 192 | Key.account.value: keyPrefix.appending(key), 193 | ] 194 | 195 | if let group = accessGroup { query.addAccessGroup(group) } 196 | if isSynchronizable { query.addSynchronizability(value: false) } 197 | 198 | lastResult.code = SecItemDelete(query.cfDictionary) 199 | return lastResult.result 200 | } 201 | 202 | } 203 | #endif 204 | -------------------------------------------------------------------------------- /Sources/KeychainClientLive/Keychain/Licence/LICENCE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License 3 | 4 | Copyright (c) 2015 - 2019 Evgenii Neumerzhitckii 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Sources/KeychainClientLive/Keychain/Licence/README: -------------------------------------------------------------------------------- 1 | Keychain object implementation is highly inspired by https://github.com/evgenyneu/keychain-swift 2 | -------------------------------------------------------------------------------- /Sources/KeychainClientLive/Keychain/NSLocking+.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension NSLocking { 4 | func execute(_ action: () -> T) -> T { 5 | lock(); defer { unlock() } 6 | return action() 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/KeychainClientLive/KeychainClientAccessPolicy.swift: -------------------------------------------------------------------------------- 1 | import KeychainClient 2 | 3 | #if canImport(Security) 4 | import Security 5 | 6 | extension Keychain.AccessPolicy { 7 | init(_ operationPolicy: KeychainClient.Operations.Save.AccessPolicy) { 8 | switch operationPolicy { 9 | case .accessibleWhenUnlocked: 10 | self = .accessibleWhenUnlocked 11 | case .accessibleWhenUnlockedThisDeviceOnly: 12 | self = .accessibleWhenUnlockedThisDeviceOnly 13 | case .accessibleAfterFirstUnlock: 14 | self = .accessibleAfterFirstUnlock 15 | case .accessibleAfterFirstUnlockThisDeviceOnly: 16 | self = .accessibleAfterFirstUnlockThisDeviceOnly 17 | case .accessibleWhenPasscodeSetThisDeviceOnly: 18 | self = .accessibleWhenPasscodeSetThisDeviceOnly 19 | case .accessibleAlways: 20 | self = .accessibleAlways 21 | case .accessibleAlwaysThisDeviceOnly: 22 | self = .accessibleAlwaysThisDeviceOnly 23 | } 24 | } 25 | } 26 | #endif 27 | -------------------------------------------------------------------------------- /Sources/KeychainClientLive/KeychainClientLive.swift: -------------------------------------------------------------------------------- 1 | import KeychainClient 2 | import Foundation 3 | 4 | #if canImport(Security) 5 | import Security 6 | 7 | extension KeychainClient { 8 | public static let live: KeychainClient = .default() 9 | 10 | public static func `default`( 11 | keyPrefix: String = Bundle.main.bundleIdentifier.map { $0.appending(".") } ?? "", 12 | accessGroup: String? = .none 13 | ) -> KeychainClient { 14 | let keychain = Keychain(keyPrefix: keyPrefix, accessGroup: accessGroup) 15 | return KeychainClient( 16 | saveValue: .init { key, data, policy in 17 | keychain.setData(data, forKey: key.rawValue, policy: .init(policy)) 18 | }, 19 | loadValue: .init { key in 20 | try? keychain.loadData(forKey: key.rawValue).get() 21 | }, 22 | removeValue: .init { key in 23 | keychain.delete(key: key.rawValue) 24 | } 25 | ) 26 | } 27 | } 28 | #endif 29 | -------------------------------------------------------------------------------- /Sources/MemoryCacheClient/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import CacheClient 2 | -------------------------------------------------------------------------------- /Sources/MemoryCacheClient/MemoryCache.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public final class MemoryCache { 4 | private let _cache = NSCache() 5 | private let keyTracker = KeyTracker() 6 | private let dateProvider: () -> Date 7 | private let entryLifetime: TimeInterval? 8 | 9 | public init( 10 | dateProvider: @escaping () -> Date = Date.init, 11 | entryLifetime: TimeInterval? = 1 * 60 * 60, // 1h 12 | totalCostLimit: Int? = 128 * 1024 * 1024, // 128mb 13 | countLimit: Int? = .none 14 | ) { 15 | self.dateProvider = dateProvider 16 | self.entryLifetime = entryLifetime 17 | self._cache.totalCostLimit = totalCostLimit ?? 0 18 | self._cache.countLimit = countLimit ?? 0 19 | self._cache.delegate = keyTracker 20 | } 21 | 22 | public func insert(_ value: Value, withCost cost: Int? = nil, forKey key: Key) { 23 | let date = entryLifetime.map { dateProvider().addingTimeInterval($0) } 24 | let entry = Entry( 25 | key: keyTracker.wrappedKey(for: key), 26 | value: value, 27 | cost: cost, 28 | expirationDate: date 29 | ) 30 | keyTracker.keys.insert(entry.key) 31 | store(entry) 32 | } 33 | 34 | public func value(forKey key: Key) -> Value? { 35 | return entry(forKey: key)?.value 36 | } 37 | 38 | public func removeValue(forKey key: Key) { 39 | _cache.removeObject(forKey: keyTracker.wrappedKey(for: key)) 40 | } 41 | 42 | public func removeAllValues() { 43 | _cache.removeAllObjects() 44 | } 45 | 46 | private func calculateSize() -> Int { 47 | keyTracker.keys.map(\.key).compactMap(entry(forKey:)).reduce(into: 0) { buffer, entry in 48 | buffer += entry.cost ?? 0 49 | } 50 | } 51 | 52 | func entry(forKey key: Key) -> Entry? { 53 | entry(forKey: keyTracker.wrappedKey(for: key)) 54 | } 55 | 56 | func entry(forKey wrappedKey: WrappedKey) -> Entry? { 57 | guard let entry = _cache.object(forKey: wrappedKey) 58 | else { return nil } 59 | 60 | if let expirationDate = entry.expirationDate, dateProvider() > expirationDate { 61 | removeValue(forKey: wrappedKey.key) 62 | return nil 63 | } 64 | 65 | return entry 66 | } 67 | 68 | func store(_ entry: Entry) { 69 | if let cost = entry.cost { 70 | _cache.setObject(entry, forKey: entry.key, cost: cost) 71 | } else { 72 | _cache.setObject(entry, forKey: entry.key) 73 | } 74 | } 75 | } 76 | 77 | extension MemoryCache { 78 | final class WrappedKey: NSObject { 79 | let key: Key 80 | 81 | init(_ key: Key) { self.key = key } 82 | 83 | override var hash: Int { return key.hashValue } 84 | 85 | override func isEqual(_ object: Any?) -> Bool { 86 | guard let value = object as? WrappedKey else { 87 | return false 88 | } 89 | 90 | return value.key == key 91 | } 92 | } 93 | } 94 | 95 | extension MemoryCache { 96 | final class Entry { 97 | let key: WrappedKey 98 | let cost: Int? 99 | let value: Value 100 | let expirationDate: Date? 101 | 102 | init(key: WrappedKey, value: Value, cost: Int? = nil, expirationDate: Date?) { 103 | self.key = key 104 | self.value = value 105 | self.cost = cost 106 | self.expirationDate = expirationDate 107 | } 108 | } 109 | } 110 | 111 | extension MemoryCache { 112 | final class KeyTracker: NSObject, NSCacheDelegate { 113 | let lock = NSLock() 114 | var _keys = Set() 115 | 116 | var keys: Set { 117 | get { _keys } 118 | set { 119 | lock.lock() 120 | _keys = newValue 121 | lock.unlock() 122 | } 123 | } 124 | 125 | func wrappedKey(for key: Key) -> WrappedKey { 126 | keys.first(where: { $0.key == key }) ?? WrappedKey(key) 127 | } 128 | 129 | func cache( 130 | _ cache: NSCache, 131 | willEvictObject object: Any 132 | ) { 133 | guard let entry = object as? Entry 134 | else { return } 135 | keys.remove(entry.key) 136 | } 137 | } 138 | } 139 | 140 | extension MemoryCache { 141 | public subscript(key: Key) -> Value? { 142 | get { return value(forKey: key) } 143 | set { 144 | guard let value = newValue else { 145 | removeValue(forKey: key) 146 | return 147 | } 148 | 149 | insert(value, forKey: key) 150 | } 151 | } 152 | } 153 | 154 | extension MemoryCache.WrappedKey: Codable where Key: Codable {} 155 | extension MemoryCache.Entry: Codable where Key: Codable, Value: Codable {} 156 | 157 | extension MemoryCache: Codable where Key: Codable, Value: Codable { 158 | public convenience init(from decoder: Decoder) throws { 159 | self.init() 160 | 161 | let container = try decoder.singleValueContainer() 162 | let entries = try container.decode([Entry].self) 163 | entries.forEach(store) 164 | } 165 | 166 | public func encode(to encoder: Encoder) throws { 167 | var container = encoder.singleValueContainer() 168 | try container.encode(keyTracker.keys.compactMap(entry)) 169 | } 170 | } 171 | 172 | extension MemoryCache where Key: Codable, Value: Codable { 173 | public func saveToDisk( 174 | withName name: String, 175 | folderURL: URL, 176 | fileManager: FileManager = .default 177 | ) throws { 178 | let fileURL = folderURL.appendingPathComponent(name + ".cache") 179 | let data = try JSONEncoder().encode(self) 180 | try data.write(to: fileURL) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Sources/MemoryCacheClient/MemoryCacheClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CacheClient 3 | 4 | extension CacheClient { 5 | public static func memory( 6 | dateProvider: @escaping () -> Date = Date.init, 7 | entryLifetime: TimeInterval? = 1 * 60 * 60, // 1h 8 | totalCostLimit: Int? = 128 * 1024 * 1024, // 128mb 9 | countLimit: Int? = .none 10 | ) -> CacheClient { 11 | return .memory( 12 | MemoryCache( 13 | dateProvider: dateProvider, 14 | entryLifetime: entryLifetime, 15 | totalCostLimit: totalCostLimit 16 | ) 17 | ) 18 | } 19 | 20 | public static func memory(_ cache: MemoryCache) -> CacheClient { 21 | return CacheClient( 22 | saveValue: .init { key, value, cost in cache.insert(value, withCost: cost, forKey: key) }, 23 | loadValue: .init { key in cache.value(forKey: key) }, 24 | removeValue: .init { key in cache.removeValue(forKey: key) }, 25 | removeAllValues: .init { cache.removeAllValues() } 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/NotificationsPermissionsClient/NotificationsPermissionsClient.swift: -------------------------------------------------------------------------------- 1 | public struct NotificationsPermissionsClient { 2 | public init( 3 | requestAuthorizationStatus: Operations.RequestAuthorizationStatus, 4 | requestAuthorization: Operations.RequestAuthorization, 5 | configureRemoteNotifications: Operations.ConfigureRemoteNotifications 6 | ) { 7 | self.requestAuthorizationStatus = requestAuthorizationStatus 8 | self.requestAuthorization = requestAuthorization 9 | self.configureRemoteNotifications = configureRemoteNotifications 10 | } 11 | 12 | public var requestAuthorizationStatus: Operations.RequestAuthorizationStatus 13 | public var requestAuthorization: Operations.RequestAuthorization 14 | public var configureRemoteNotifications: Operations.ConfigureRemoteNotifications 15 | } 16 | 17 | extension NotificationsPermissionsClient { 18 | public struct AuthorizationOptions: OptionSet, Equatable { 19 | public var rawValue: UInt 20 | public init(rawValue: UInt) { 21 | self.rawValue = rawValue 22 | } 23 | 24 | public static var badge: AuthorizationOptions { .init(rawValue: 1 << 0) } 25 | 26 | public static var sound: AuthorizationOptions { .init(rawValue: 1 << 1) } 27 | 28 | public static var alert: AuthorizationOptions { .init(rawValue: 1 << 2) } 29 | 30 | public static var carPlay: AuthorizationOptions { .init(rawValue: 1 << 3) } 31 | 32 | public static var criticalAlert: AuthorizationOptions { .init(rawValue: 1 << 4) } 33 | 34 | public static var providesAppNotificationSettings: AuthorizationOptions { .init(rawValue: 1 << 5) } 35 | 36 | public static var provisional: AuthorizationOptions { .init(rawValue: 1 << 6) } 37 | 38 | #if os(iOS) 39 | public static var announcement: AuthorizationOptions { .init(rawValue: 1 << 7) } 40 | #endif 41 | } 42 | 43 | public enum AuthorizationStatus: Equatable { 44 | case notDetermined 45 | case denied 46 | case authorized 47 | case provisional 48 | 49 | #if os(iOS) 50 | @available(iOS 14.0, *) 51 | case ephemeral 52 | #endif 53 | 54 | case unknown 55 | 56 | public var isPermissive: Bool { 57 | #if os(iOS) 58 | if #available(iOS 14.0, *) { 59 | if self == .ephemeral { return true } 60 | } 61 | #endif 62 | return self == .authorized || self == .provisional 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/NotificationsPermissionsClient/NotificationsPermissionsClientOperations.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Prelude 3 | 4 | extension NotificationsPermissionsClient { 5 | public enum Operations {} 6 | } 7 | 8 | extension NotificationsPermissionsClient.Operations { 9 | public struct RequestAuthorizationStatus: Function { 10 | public typealias Input = Void 11 | public typealias Output = AnyPublisher 12 | 13 | public init(_ call: @escaping Signature) { 14 | self.call = call 15 | } 16 | 17 | public let call: Signature 18 | } 19 | 20 | public struct RequestAuthorization: Function { 21 | public typealias Input = NotificationsPermissionsClient.AuthorizationOptions 22 | public typealias Output = AnyPublisher 23 | 24 | public init(_ call: @escaping Signature) { 25 | self.call = call 26 | } 27 | 28 | public let call: Signature 29 | 30 | public func callAsFunction(options: Input) -> Output { 31 | return call(options) 32 | } 33 | } 34 | 35 | public struct ConfigureRemoteNotifications: Function { 36 | public enum Input { case register, unregister } 37 | public typealias Output = Void 38 | 39 | public init(_ call: @escaping Signature) { 40 | self.call = call 41 | } 42 | 43 | public let call: Signature 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/NotificationsPermissionsClientLive/AuthorizationOptions.swift: -------------------------------------------------------------------------------- 1 | import UserNotifications 2 | import NotificationsPermissionsClient 3 | 4 | extension NotificationsPermissionsClient.AuthorizationOptions { 5 | public init(options: UNAuthorizationOptions) { 6 | self.init(rawValue: options.rawValue) 7 | } 8 | 9 | public var options: UNAuthorizationOptions { 10 | UNAuthorizationOptions(rawValue: rawValue) 11 | } 12 | } 13 | 14 | extension UNAuthorizationOptions { 15 | public init(options: NotificationsPermissionsClient.AuthorizationOptions) { 16 | self.init(rawValue: options.rawValue) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/NotificationsPermissionsClientLive/AuthorizationStatus.swift: -------------------------------------------------------------------------------- 1 | import UserNotifications 2 | import NotificationsPermissionsClient 3 | 4 | extension NotificationsPermissionsClient.AuthorizationStatus { 5 | public init(status: UNAuthorizationStatus) { 6 | switch status { 7 | case .authorized: 8 | self = .authorized 9 | 10 | case .denied: 11 | self = .denied 12 | 13 | #if os(iOS) 14 | case .ephemeral: 15 | if #available(iOS 14.0, *) { 16 | self = .ephemeral 17 | } else { 18 | self = .unknown 19 | } 20 | #endif 21 | 22 | case .notDetermined: 23 | self = .notDetermined 24 | 25 | case .provisional: 26 | if #available(iOS 12.0, *) { 27 | self = .provisional 28 | } else { 29 | self = .unknown 30 | } 31 | 32 | default: 33 | self = .unknown 34 | } 35 | } 36 | 37 | public var status: UNAuthorizationStatus? { 38 | switch self { 39 | case .authorized: 40 | return .authorized 41 | 42 | case .denied: 43 | return .denied 44 | 45 | #if os(iOS) 46 | case .ephemeral: 47 | if #available(iOS 14.0, *) { 48 | return .ephemeral 49 | } else { 50 | return .none 51 | } 52 | #endif 53 | 54 | case .notDetermined: 55 | return .notDetermined 56 | 57 | case .provisional: 58 | return .provisional 59 | 60 | case .unknown: 61 | return .none 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/NotificationsPermissionsClientLive/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import NotificationsPermissionsClient 2 | -------------------------------------------------------------------------------- /Sources/NotificationsPermissionsClientLive/NotificationsPermissionsClientLive.swift: -------------------------------------------------------------------------------- 1 | import NotificationsPermissionsClient 2 | import UserNotifications 3 | import Combine 4 | 5 | #if os(macOS) 6 | import AppKit 7 | #elseif os(watchOS) 8 | import WatchKit 9 | #else 10 | import UIKit 11 | #endif 12 | 13 | extension NotificationsPermissionsClient { 14 | public static let live = NotificationsPermissionsClient( 15 | requestAuthorizationStatus: .live, 16 | requestAuthorization: .live, 17 | configureRemoteNotifications: .live 18 | ) 19 | } 20 | 21 | extension NotificationsPermissionsClient.Operations.RequestAuthorizationStatus { 22 | public static let live = NotificationsPermissionsClient.Operations.RequestAuthorizationStatus { 23 | Deferred { 24 | Future { promise in 25 | let userNotificationCenter = UNUserNotificationCenter.current() 26 | userNotificationCenter.getNotificationSettings { settings in 27 | promise(.success(.init(status: settings.authorizationStatus))) 28 | } 29 | } 30 | }.eraseToAnyPublisher() 31 | } 32 | } 33 | 34 | extension NotificationsPermissionsClient.Operations.RequestAuthorization { 35 | public static let live = NotificationsPermissionsClient.Operations.RequestAuthorization { params in 36 | Deferred { 37 | Future { promise in 38 | let userNotificationCenter = UNUserNotificationCenter.current() 39 | userNotificationCenter.requestAuthorization(options: params.options) { granted, error in 40 | userNotificationCenter.getNotificationSettings { settings in 41 | promise(.success(.init(status: settings.authorizationStatus))) 42 | } 43 | } 44 | } 45 | }.eraseToAnyPublisher() 46 | } 47 | } 48 | 49 | extension NotificationsPermissionsClient.Operations.ConfigureRemoteNotifications { 50 | public static let live = NotificationsPermissionsClient.Operations.ConfigureRemoteNotifications { action in 51 | #if os(macOS) 52 | let application = NSApplication.shared 53 | #elseif os(watchOS) 54 | let application = WKExtension.shared() 55 | #else 56 | let application = UIApplication.shared 57 | #endif 58 | switch action { 59 | case .register: application.registerForRemoteNotifications() 60 | case .unregister: application.unregisterForRemoteNotifications() 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/StandardClients/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import CacheClient 2 | @_exported import HapticEngineClient 3 | @_exported import IDFAPermissionsClient 4 | @_exported import KeychainClient 5 | @_exported import NotificationsPermissionsClient 6 | @_exported import UserDefaultsClient 7 | -------------------------------------------------------------------------------- /Sources/StandardClientsLive/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import MemoryCacheClient 2 | @_exported import HapticEngineClientLive 3 | @_exported import IDFAPermissionsClientLive 4 | @_exported import KeychainClientLive 5 | @_exported import NotificationsPermissionsClientLive 6 | @_exported import UserDefaultsClientLive 7 | -------------------------------------------------------------------------------- /Sources/UserDefaultsClient/UserDefaultsClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct UserDefaultsClient { 4 | public init( 5 | saveValue: Operations.Save, 6 | loadValue: Operations.Load, 7 | removeValue: Operations.Remove 8 | ) { 9 | self.saveValue = saveValue 10 | self.loadValue = loadValue 11 | self.removeValue = removeValue 12 | } 13 | 14 | public var saveValue: Operations.Save 15 | public var loadValue: Operations.Load 16 | public var removeValue: Operations.Remove 17 | } 18 | 19 | extension UserDefaultsClient { 20 | public struct Key: 21 | RawRepresentable, 22 | ExpressibleByStringLiteral, 23 | ExpressibleByStringInterpolation 24 | { 25 | public var rawValue: String 26 | 27 | public init(rawValue: String) { 28 | self.rawValue = rawValue 29 | } 30 | 31 | public init(stringLiteral value: String) { 32 | self.init(rawValue: value) 33 | } 34 | 35 | public static func bundle(_ key: Key) -> Key { 36 | return .bundle(.main, key) 37 | } 38 | 39 | public static func bundle(_ bundle: Bundle, _ key: Key) -> Key { 40 | let prefix = bundle.bundleIdentifier.map { $0.appending(".") } ?? "" 41 | return .init(rawValue: prefix.appending(key.rawValue)) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/UserDefaultsClient/UserDefaultsClientOperations.swift: -------------------------------------------------------------------------------- 1 | import DataRepresentable 2 | import Foundation 3 | import Prelude 4 | 5 | extension UserDefaultsClient { 6 | public enum Operations {} 7 | } 8 | 9 | extension UserDefaultsClient.Operations { 10 | public struct Save: Function { 11 | public typealias Input = (UserDefaultsClient.Key, Data) 12 | public typealias Output = Void 13 | 14 | public init(_ call: @escaping Signature) { 15 | self.call = call 16 | } 17 | 18 | public var call: Signature 19 | 20 | public func callAsFunction( 21 | _ value: DataRepresentable, 22 | forKey key: UserDefaultsClient.Key 23 | ) { return call((key, value.dataRepresentation)) } 24 | } 25 | 26 | public struct Load: Function { 27 | public typealias Input = UserDefaultsClient.Key 28 | public typealias Output = Data? 29 | 30 | public init(_ call: @escaping Signature) { 31 | self.call = call 32 | } 33 | 34 | public var call: Signature 35 | 36 | public func callAsFunction( 37 | of type: Value.Type = Value.self, 38 | forKey key: UserDefaultsClient.Key 39 | ) -> Value? { 40 | return call(key).flatMap(Value.init(dataRepresentation:)) 41 | } 42 | } 43 | 44 | public struct Remove: Function { 45 | public typealias Input = UserDefaultsClient.Key 46 | public typealias Output = Void 47 | 48 | public init(_ call: @escaping Signature) { 49 | self.call = call 50 | } 51 | 52 | public var call: Signature 53 | 54 | public func callAsFunction(forKey key: UserDefaultsClient.Key) { 55 | return call(key) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/UserDefaultsClientLive/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import UserDefaultsClient 2 | -------------------------------------------------------------------------------- /Sources/UserDefaultsClientLive/UserDefaultsClientLive.swift: -------------------------------------------------------------------------------- 1 | import UserDefaultsClient 2 | import DataRepresentable 3 | import Foundation 4 | 5 | extension UserDefaultsClient { 6 | public static let standard: UserDefaultsClient = .live(for: .standard) 7 | 8 | public static func live(for userDefaults: UserDefaults) -> UserDefaultsClient { 9 | UserDefaultsClient( 10 | saveValue: .init { key, value in 11 | userDefaults.setValue(value.dataRepresentation, forKey: key.rawValue) 12 | }, 13 | loadValue: .init { key in 14 | userDefaults.object(forKey: key.rawValue) as? Data 15 | }, 16 | removeValue: .init { key in 17 | userDefaults.removeObject(forKey: key.rawValue) 18 | } 19 | ) 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /Tests/DataRepresentableTests/DataRepresentableTests.swift: -------------------------------------------------------------------------------- 1 | import DataRepresentable 2 | import Foundation 3 | import XCTest 4 | 5 | public class DataRepresentableTests: XCTestCase { 6 | func testSimpleTypes() { 7 | let stringValue = "Hello, World! 🚀" 8 | XCTAssertEqual(stringValue, String(dataRepresentation: stringValue.dataRepresentation)) 9 | 10 | let intValue = 420 11 | XCTAssertEqual(intValue, Int(dataRepresentation: intValue.dataRepresentation)) 12 | 13 | let doubleValue = 420.69 14 | XCTAssertEqual(doubleValue, Double(dataRepresentation: doubleValue.dataRepresentation)) 15 | 16 | XCTAssertEqual(true, Bool(dataRepresentation: true.dataRepresentation)) 17 | XCTAssertEqual(false, Bool(dataRepresentation: false.dataRepresentation)) 18 | } 19 | 20 | func testJSON() { 21 | struct Object: Codable, Equatable, DataRepresentable { 22 | init( 23 | title: String, 24 | description: String? = nil, 25 | value: Int, 26 | flag: Bool 27 | ) { 28 | self.title = title 29 | self.description = description 30 | self.value = value 31 | self.flag = flag 32 | } 33 | 34 | var title: String 35 | var description: String? 36 | var value: Int 37 | var flag: Bool 38 | 39 | init?(dataRepresentation: Data) { 40 | guard let object = try? JSONDecoder().decode(Self.self, from: dataRepresentation) 41 | else { return nil } 42 | self = object 43 | } 44 | 45 | var dataRepresentation: Data { 46 | try! JSONEncoder().encode(self) 47 | } 48 | } 49 | 50 | let object = Object( 51 | title: "Test", 52 | description: "Codable object", 53 | value: 69, 54 | flag: true 55 | ) 56 | 57 | XCTAssertEqual(object, Object(dataRepresentation: object.dataRepresentation)) 58 | } 59 | } 60 | --------------------------------------------------------------------------------