├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Sources └── BigUIUserPreferences │ ├── Extensions │ └── Date+RawRepresentable.swift │ ├── CodablePreferenceValue.swift │ ├── UserPreferenceKey.swift │ ├── UserDefaults.swift │ └── UserPreference.swift ├── Package.swift ├── LICENSE ├── Tests └── BigUIUserPreferencesTests │ └── BigUIUserPreferencesTests.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/BigUIUserPreferences/Extensions/Date+RawRepresentable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Date: RawRepresentable { 4 | 5 | public typealias RawValue = String 6 | 7 | public init?(rawValue: String) { 8 | guard let data = rawValue.data(using: .utf8), 9 | let date = try? JSONDecoder().decode(Date.self, from: data) else { 10 | return nil 11 | } 12 | self = date 13 | } 14 | 15 | public var rawValue: String { 16 | guard let data = try? JSONEncoder().encode(self), 17 | let json = String(data: data, encoding: .utf8) else { 18 | return "" 19 | } 20 | return json 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "BigUIUserPreferences", 8 | platforms: [.iOS(.v14), .macOS(.v11)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, making them visible to other packages. 11 | .library( 12 | name: "BigUIUserPreferences", 13 | targets: ["BigUIUserPreferences"]), 14 | ], 15 | targets: [ 16 | // Targets are the basic building blocks of a package, defining a module or a test suite. 17 | // Targets can depend on other targets in this package and products from dependencies. 18 | .target( 19 | name: "BigUIUserPreferences"), 20 | .testTarget( 21 | name: "BigUIUserPreferencesTests", 22 | dependencies: ["BigUIUserPreferences"]), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 The Not So Big Company 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 | -------------------------------------------------------------------------------- /Sources/BigUIUserPreferences/CodablePreferenceValue.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A wrapper for storing `Codable` values. 4 | /// 5 | /// First create a key that wraps the value you wish to store: 6 | /// 7 | /// ```swift 8 | /// struct User: Codable, Equatable { 9 | /// let username: String 10 | /// let login: Date? 11 | /// } 12 | /// 13 | /// struct LoggedInUserKey: UserPreferenceKey { 14 | /// static var defaultValue: CodablePreferenceValue = .init() 15 | /// } 16 | /// ``` 17 | /// 18 | /// You can then access the underlying value by calling ``wrappedValue``: 19 | /// 20 | /// ```swift 21 | /// @UserPreference(LoggedInUserKey.self) private var user 22 | /// 23 | /// var body: some View { 24 | /// if let user = user.wrappedValue { 25 | /// Text("Hello \(user.username)") 26 | /// } 27 | /// } 28 | /// ``` 29 | /// 30 | public struct CodablePreferenceValue: Codable where Value : Codable { 31 | 32 | public var wrappedValue: Value? 33 | 34 | public init(_ wrappedValue: Value? = nil) { 35 | self.wrappedValue = wrappedValue 36 | } 37 | } 38 | 39 | extension CodablePreferenceValue: RawRepresentable { 40 | 41 | public typealias RawValue = String 42 | 43 | public var rawValue: String { 44 | guard let data = try? JSONEncoder().encode(self.wrappedValue), 45 | let json = String(data: data, encoding: .utf8) else { 46 | return .init() 47 | } 48 | return json 49 | } 50 | 51 | public init?(rawValue: String) { 52 | let data = Data(rawValue.utf8) 53 | guard let value = try? JSONDecoder().decode( 54 | Value.self, 55 | from: data 56 | ) else { 57 | return nil 58 | } 59 | self = CodablePreferenceValue(value) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/BigUIUserPreferences/UserPreferenceKey.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// A key for accessing user default values in the environment. 4 | /// 5 | /// First declare a new preference key type and specify a value for the 6 | /// required ``defaultValue`` property: 7 | /// 8 | /// ```swift 9 | /// private struct MyPreferenceKey: UserPreferenceKey { 10 | /// static let defaultValue: Bool = false 11 | /// } 12 | /// ``` 13 | /// 14 | /// You then access the value using the ``UserPreference`` property 15 | /// wrapper: 16 | /// 17 | /// ```swift 18 | /// @UserPreference(MyPreferenceKey.self) private var myPreference 19 | /// ``` 20 | /// 21 | /// The Swift compiler automatically infers the associated ``Value`` type as the 22 | /// type you specify for the default value. 23 | /// 24 | public protocol UserPreferenceKey { 25 | 26 | /// The associated type representing the type of the preference key's value. 27 | associatedtype Value 28 | 29 | /// The default value for the preference key. 30 | static var defaultValue: Value { get } 31 | 32 | } 33 | 34 | /// A key for accessing user default values with an explicit key name. 35 | /// 36 | /// By default `UserPreferenceKey` uses an auto-generated key name to lookup values 37 | /// inside the store. If you wish to use an explicit key name instead (say, to support 38 | /// pre-existing values) adopt `ExplicitlyNamedUserPreferenceKey` and return a ``name``: 39 | /// 40 | /// ```swift 41 | /// struct MyExplicitPreferenceKey: ExplicitlyNamedUserPreferenceKey { 42 | /// static let defaultValue: Bool = false 43 | /// // The name of the value inside the store 44 | /// static let name: String = "my_explicit_key" 45 | /// } 46 | /// ``` 47 | public protocol ExplicitlyNamedUserPreferenceKey: UserPreferenceKey { 48 | 49 | /// The key name in the user defaults store. 50 | static var name: String { get } 51 | 52 | } 53 | 54 | // MARK: - Key Name Lookup 55 | 56 | extension UserPreferenceKey { 57 | 58 | static var lookupName: String { 59 | if let explicit = self as? any ExplicitlyNamedUserPreferenceKey.Type { 60 | return explicit.name 61 | } 62 | if let mangled = _mangledTypeName(type(of: self)) { 63 | return mangled 64 | } 65 | fatalError("Unable to resolve key name for \(Self.self)") 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /Tests/BigUIUserPreferencesTests/BigUIUserPreferencesTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftUI 3 | import Foundation 4 | @testable import BigUIUserPreferences 5 | 6 | // MARK: - Test Keys 7 | 8 | struct MyPreferenceKey: UserPreferenceKey { 9 | static let defaultValue: Bool = false 10 | } 11 | 12 | struct MyOptionalPreferenceKey: UserPreferenceKey { 13 | static var defaultValue: String? = nil 14 | } 15 | 16 | struct MyOptionalIntegerPreferenceKey: UserPreferenceKey { 17 | static var defaultValue: Int? = nil 18 | } 19 | 20 | struct MyDatePreferenceKey: UserPreferenceKey { 21 | static var defaultValue: Date? = Date() 22 | } 23 | 24 | enum TestEnum: Int { 25 | case one 26 | case two 27 | case three 28 | } 29 | 30 | struct MyEnumPreferenceKey: UserPreferenceKey { 31 | static var defaultValue: TestEnum = .one 32 | } 33 | 34 | struct MyExplicitPreferenceKey: ExplicitlyNamedUserPreferenceKey { 35 | static let defaultValue: Bool = false 36 | static let name: String = "my_explicit_key" 37 | } 38 | 39 | // MARK: - Codable 40 | 41 | struct User: Codable, Equatable { 42 | let username: String 43 | let login: Date? 44 | } 45 | 46 | struct LoggedInUserKey: UserPreferenceKey { 47 | static var defaultValue: CodablePreferenceValue = .init() 48 | } 49 | 50 | // MARK: - Tests 51 | 52 | @available(iOS 15.0, *) 53 | final class BigUIUserPreferencesTests: XCTestCase { 54 | 55 | @UserPreference(MyPreferenceKey.self) private var myPreference 56 | 57 | @UserPreference(MyEnumPreferenceKey.self) private var myEnumPreference 58 | 59 | @UserPreference(MyDatePreferenceKey.self) private var myDate 60 | 61 | @UserPreference(LoggedInUserKey.self) private var user 62 | 63 | func testWrapperDefaultValue() { 64 | XCTAssertEqual(myPreference, false) 65 | } 66 | 67 | func testBool() { 68 | let defaults = UserDefaults(suiteName: UUID().uuidString)! 69 | 70 | // Test default value 71 | let myDefaultValue = defaults[MyPreferenceKey.self] 72 | XCTAssertEqual(myDefaultValue, MyPreferenceKey.defaultValue) 73 | 74 | // Test set value 75 | defaults[MyPreferenceKey.self] = true 76 | let myValue = defaults[MyPreferenceKey.self] 77 | XCTAssertEqual(myValue, true) 78 | } 79 | 80 | func testOptionalString() { 81 | let defaults = UserDefaults(suiteName: UUID().uuidString)! 82 | 83 | // Test nil value 84 | let myString = defaults[MyOptionalPreferenceKey.self] 85 | XCTAssertNil(myString) 86 | 87 | // Test set value 88 | defaults[MyOptionalPreferenceKey.self] = "Hello World" 89 | let myOptionalString = defaults[MyOptionalPreferenceKey.self] 90 | XCTAssertEqual(myOptionalString, "Hello World") 91 | } 92 | 93 | func testOptionalIntNotZero() { 94 | let defaults = UserDefaults(suiteName: UUID().uuidString)! 95 | 96 | // Test nil isn't 0 (UserDefaults default) 97 | let value = defaults[MyOptionalIntegerPreferenceKey.self] 98 | XCTAssertEqual(value, nil) 99 | 100 | // Test we can still use 0 101 | defaults[MyOptionalIntegerPreferenceKey.self] = 0 102 | XCTAssertEqual(defaults[MyOptionalIntegerPreferenceKey.self], 0) 103 | } 104 | 105 | func testEnum() { 106 | // Check default value 107 | XCTAssertEqual(myEnumPreference, .one) 108 | 109 | // Check subscript 110 | let defaults = UserDefaults(suiteName: UUID().uuidString)! 111 | defaults[MyEnumPreferenceKey.self] = .three 112 | XCTAssertEqual(defaults[MyEnumPreferenceKey.self], .three) 113 | } 114 | 115 | func testDate() { 116 | let defaults = UserDefaults(suiteName: UUID().uuidString)! 117 | let expected = Date.now 118 | defaults[MyDatePreferenceKey.self] = expected 119 | XCTAssertEqual(defaults[MyDatePreferenceKey.self], expected) 120 | } 121 | 122 | func testCodable() { 123 | let defaults = UserDefaults(suiteName: UUID().uuidString)! 124 | let expected = User(username: "demo", login: .now) 125 | defaults[LoggedInUserKey.self] = CodablePreferenceValue(expected) 126 | let value = defaults[LoggedInUserKey.self] 127 | XCTAssertEqual(expected, value.wrappedValue) 128 | } 129 | 130 | func testMultipleKeyNames() { 131 | let defaults = UserDefaults(suiteName: UUID().uuidString)! 132 | defaults[MyPreferenceKey.self] = true 133 | defaults[MyOptionalPreferenceKey.self] = "test" 134 | 135 | XCTAssertEqual(defaults[MyPreferenceKey.self], true) 136 | XCTAssertEqual(defaults[MyOptionalPreferenceKey.self], "test") 137 | } 138 | 139 | func testExplicitKeyName() { 140 | let defaults = UserDefaults(suiteName: UUID().uuidString)! 141 | defaults[MyExplicitPreferenceKey.self] = true 142 | XCTAssertEqual(defaults[MyExplicitPreferenceKey.self], true) 143 | XCTAssertEqual(MyExplicitPreferenceKey.lookupName, "my_explicit_key") 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BigUIUserPreferences 2 | 3 | A strongly typed Swift and SwiftUI wrapper for UserDefaults. 4 | 5 | This package adds two new interfaces for working with `UserDefaults`: 6 | 7 | 1. A `@UserPreference` property wrapper for SwiftUI views: 8 | 9 | ```swift 10 | @UserPreference(MiniSidebarKey.self) private var enabled 11 | ``` 12 | 13 | 2. A subscript interface for `UserDefaults`: 14 | 15 | ```swift 16 | UserDefaults.standard[MiniSidebarKey.self] = true 17 | ``` 18 | 19 | By tightly coupling key names and default values together with `UserPreferenceKey` 20 | you eliminate the possibility of key typos, differing defaults, and type mismatches. 21 | This package also adds support for `Codable` and `Date` types. 22 | 23 | ## Installation 24 | 25 | Add this repository to your Package.swift file: 26 | 27 | ```swift 28 | .package(url: "https://github.com/notsobigcompany/BigUIUserPreferences.git", from: "1.0.0") 29 | ``` 30 | 31 | If you’re adding to an Xcode project go to File -> Add Packages, then link the 32 | package to your required target. 33 | 34 | ## Getting Started 35 | 36 | First declare a `UserPreferenceKey` type and specify a default value: 37 | 38 | ```swift 39 | struct MiniSidebarKey: UserPreferenceKey { 40 | static let defaultValue: Bool = false 41 | } 42 | ``` 43 | 44 | > [!TIP] 45 | > Both `EnvironmentKey` and `UserPreferenceKey` have the exact same type requirements. 46 | 47 | ### SwiftUI 48 | 49 | Using the key you can access the value inside of a SwiftUI view with the `@UserPreference` 50 | property wrapper: 51 | 52 | ```swift 53 | struct MiniSidebarToggle: View { 54 | 55 | @UserPreference(MiniSidebarKey.self) private var enabled 56 | 57 | var body: some View { 58 | Toggle(isOn: $enabled) { 59 | Text("Enable Minibar") 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | Unlike `AppStorage` you don't need to specify the type or default value. 66 | 67 | The wrapper always reflects the latest value in the `UserDefaults` and 68 | invalidates the view on any external changes. 69 | 70 | ### Subscript 71 | 72 | You can also access the value by using the `subscript` interface directly on 73 | `UserDefaults` itself: 74 | 75 | ```swift 76 | let defaults = UserDefaults.standard 77 | defaults[MiniSidebarToggle.self] = true 78 | 79 | var isMiniBarEnabled = defaults[MiniSidebarToggle.self] 80 | // true 81 | print(isMiniBarEnabled) 82 | ``` 83 | 84 | > [!NOTE] 85 | > If there's no value in the store the subscript API will return the key's default 86 | value, rather than the store's default. 87 | 88 | ## Supported Types 89 | 90 | - `String` 91 | - `Int` 92 | - `Double` 93 | - `Data` 94 | - `Date` 95 | - `URL` 96 | - `RawRepresentable` where `RawValue` is `Int` 97 | - `RawRepresentable` where `RawValue` is `String` 98 | - `Codable` when wrapped in `CodablePreferenceValue` 99 | 100 | ## Codable Values 101 | 102 | You can store small `Codable` values by wrapping the value in 103 | `CodablePreferenceValue`: 104 | 105 | ```swift 106 | struct User: Codable, Equatable { 107 | // ... 108 | } 109 | 110 | struct LoggedInUserKey: UserPreferenceKey { 111 | static var defaultValue: CodablePreferenceValue = .init() 112 | } 113 | ``` 114 | 115 | > [!IMPORTANT] 116 | > The wrapper itself cannot be optional, but the underlying wrapped type can be. 117 | 118 | You can then access the underlying value by calling `wrappedValue`: 119 | 120 | ```swift 121 | @UserPreference(LoggedInUserKey.self) private var user 122 | 123 | var body: some View { 124 | if let user = user.wrappedValue { 125 | Text("Hello \(user.username)") 126 | } 127 | } 128 | ``` 129 | 130 | ## Explicit Key Names 131 | 132 | By default `UserPreferenceKey` uses an auto-generated key name to lookup values 133 | inside the store. If you wish to use an explicit key name instead (say, to support 134 | pre-existing values) adopt `ExplicitlyNamedUserPreferenceKey` and return a `name`: 135 | 136 | ```swift 137 | struct MyExplicitPreferenceKey: ExplicitlyNamedUserPreferenceKey { 138 | static let defaultValue: Bool = false 139 | // The name of the value inside the store 140 | static let name: String = "my_explicit_key" 141 | } 142 | ``` 143 | 144 | ## Requirements 145 | 146 | - iOS 14.0 147 | - macOS 11.0 148 | 149 | ## License 150 | 151 | Copyright 2023 NOT SO BIG TECH LIMITED 152 | 153 | 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: 154 | 155 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 156 | 157 | 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. 158 | -------------------------------------------------------------------------------- /Sources/BigUIUserPreferences/UserDefaults.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 4 | extension UserDefaults { 5 | 6 | public subscript(key: K.Type) -> K.Value where K : UserPreferenceKey, K.Value == Bool { 7 | get { 8 | // UserDefaults will return false if no entry, so check for nil instead 9 | if object(forKey: K.lookupName) == nil { 10 | return K.defaultValue 11 | } 12 | return bool(forKey: K.lookupName) 13 | } 14 | set { 15 | set(newValue, forKey: K.lookupName) 16 | } 17 | } 18 | 19 | public subscript(key: K.Type) -> K.Value where K : UserPreferenceKey, K.Value == String { 20 | get { 21 | string(forKey: K.lookupName) ?? K.defaultValue 22 | } 23 | set { 24 | set(newValue, forKey: K.lookupName) 25 | } 26 | } 27 | 28 | public subscript(key: K.Type) -> K.Value where K : UserPreferenceKey, K.Value == URL { 29 | get { 30 | url(forKey: K.lookupName) ?? K.defaultValue 31 | } 32 | set { 33 | set(newValue, forKey: K.lookupName) 34 | } 35 | } 36 | 37 | public subscript(key: K.Type) -> K.Value where K : UserPreferenceKey, K.Value == Data { 38 | get { 39 | data(forKey: K.lookupName) ?? K.defaultValue 40 | } 41 | set { 42 | set(newValue, forKey: K.lookupName) 43 | } 44 | } 45 | 46 | public subscript(key: K.Type) -> K.Value where K : UserPreferenceKey, K.Value == Int { 47 | get { 48 | // UserDefaults will return 0 if no entry, so check for nil instead 49 | if object(forKey: K.lookupName) == nil { 50 | return K.defaultValue 51 | } 52 | return integer(forKey: K.lookupName) 53 | } 54 | set { 55 | set(newValue, forKey: K.lookupName) 56 | } 57 | } 58 | 59 | public subscript(key: K.Type) -> K.Value where K : UserPreferenceKey, K.Value == Double { 60 | get { 61 | if object(forKey: K.lookupName) == nil { 62 | return K.defaultValue 63 | } 64 | return double(forKey: K.lookupName) 65 | } 66 | set { 67 | set(newValue, forKey: K.lookupName) 68 | } 69 | } 70 | } 71 | 72 | // MARK: - Optionals 73 | 74 | extension UserDefaults { 75 | 76 | public subscript(key: K.Type) -> K.Value where K : UserPreferenceKey, K.Value == Bool? { 77 | get { 78 | if object(forKey: K.lookupName) == nil { 79 | return K.defaultValue 80 | } 81 | return bool(forKey: K.lookupName) 82 | } 83 | set { 84 | set(newValue, forKey: K.lookupName) 85 | } 86 | } 87 | 88 | public subscript(key: K.Type) -> K.Value where K : UserPreferenceKey, K.Value == String? { 89 | get { 90 | string(forKey: K.lookupName) 91 | } 92 | set { 93 | set(newValue, forKey: K.lookupName) 94 | } 95 | } 96 | 97 | public subscript(key: K.Type) -> K.Value where K : UserPreferenceKey, K.Value == Data? { 98 | get { 99 | data(forKey: K.lookupName) 100 | } 101 | set { 102 | set(newValue, forKey: K.lookupName) 103 | } 104 | } 105 | 106 | public subscript(key: K.Type) -> K.Value where K : UserPreferenceKey, K.Value == URL? { 107 | get { 108 | url(forKey: K.lookupName) 109 | } 110 | set { 111 | set(newValue, forKey: K.lookupName) 112 | } 113 | } 114 | 115 | public subscript(key: K.Type) -> K.Value where K : UserPreferenceKey, K.Value == Int? { 116 | get { 117 | if object(forKey: K.lookupName) == nil { 118 | return K.defaultValue 119 | } 120 | return integer(forKey: K.lookupName) 121 | } 122 | set { 123 | set(newValue, forKey: K.lookupName) 124 | } 125 | } 126 | 127 | public subscript(key: K.Type) -> K.Value where K : UserPreferenceKey, K.Value == Double? { 128 | get { 129 | if object(forKey: K.lookupName) == nil { 130 | return K.defaultValue 131 | } 132 | return double(forKey: K.lookupName) 133 | } 134 | set { 135 | set(newValue, forKey: K.lookupName) 136 | } 137 | } 138 | } 139 | 140 | // MARK: - RawRepresentable 141 | 142 | extension UserDefaults { 143 | 144 | public subscript(key: K.Type) -> K.Value where K : UserPreferenceKey, K.Value : RawRepresentable, K.Value.RawValue == Int { 145 | get { 146 | guard let rawValue = object(forKey: K.lookupName) as? Int else { 147 | return K.defaultValue 148 | } 149 | return K.Value(rawValue: rawValue) ?? K.defaultValue 150 | } 151 | set { 152 | set(newValue.rawValue, forKey: K.lookupName) 153 | } 154 | } 155 | 156 | public subscript(key: K.Type) -> K.Value where K : UserPreferenceKey, K.Value : RawRepresentable, K.Value.RawValue == String { 157 | get { 158 | guard let rawValue = object(forKey: K.lookupName) as? String else { 159 | return K.defaultValue 160 | } 161 | return K.Value(rawValue: rawValue) ?? K.defaultValue 162 | } 163 | set { 164 | set(newValue.rawValue, forKey: K.lookupName) 165 | } 166 | } 167 | } 168 | 169 | // MARK: - Date 170 | 171 | extension UserDefaults { 172 | 173 | public subscript(key: K.Type) -> K.Value where K : UserPreferenceKey, K.Value == Date? { 174 | get { 175 | guard let rawValue = object(forKey: K.lookupName) as? String else { 176 | return K.defaultValue 177 | } 178 | return Date(rawValue: rawValue) 179 | } 180 | set { 181 | if let newValue { 182 | set(newValue.rawValue, forKey: K.lookupName) 183 | } else { 184 | removeObject(forKey: K.lookupName) 185 | } 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /Sources/BigUIUserPreferences/UserPreference.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// A property wrapper type that reflects a value from `UserDefaults` and 4 | /// invalidates a view on a change in value in that user default. 5 | /// 6 | /// First declare a ``UserPreferenceKey`` type and specify a value for the 7 | /// required ``UserPreferenceKey/defaultValue`` property: 8 | /// 9 | /// ```swift 10 | /// private struct MyPreferenceKey: UserPreferenceKey { 11 | /// static let defaultValue: Bool = false 12 | /// } 13 | /// ``` 14 | /// 15 | /// You then access the value using the property wrapper: 16 | /// 17 | /// ```swift 18 | /// @UserPreference(MyPreferenceKey.self) private var myPreference 19 | /// ``` 20 | /// 21 | /// The Swift compiler automatically infers the associated ``UserPreferenceKey/Value`` type as the 22 | /// type you specify for the default value. 23 | /// 24 | /// If the value changes, SwiftUI updates any parts of your view that depend on 25 | /// the value. 26 | /// 27 | @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) 28 | @frozen @propertyWrapper public struct UserPreference : DynamicProperty where Key : UserPreferenceKey { 29 | 30 | @AppStorage private var value: Key.Value 31 | 32 | public var wrappedValue: Key.Value { 33 | get { 34 | value 35 | } 36 | nonmutating set { 37 | value = newValue 38 | } 39 | } 40 | 41 | public var projectedValue: Binding { 42 | .init { 43 | wrappedValue 44 | } set: { newValue in 45 | wrappedValue = newValue 46 | } 47 | } 48 | } 49 | 50 | // MARK: - Initialisers 51 | 52 | extension UserPreference { 53 | 54 | /// Creates a property that can read and write to a boolean user default. 55 | /// 56 | /// - Parameters: 57 | /// - key: The key to read and write the value to in the user defaults 58 | /// store. 59 | /// - store: The user defaults store to read and write to. A value 60 | /// of `nil` will use the user default store from the environment. 61 | /// 62 | public init(_ key: Key.Type, store: UserDefaults? = nil) where Key.Value == Bool { 63 | self._value = AppStorage(wrappedValue: Key.defaultValue, Key.lookupName, store: store) 64 | } 65 | 66 | /// Creates a property that can read and write to a string user default. 67 | /// 68 | /// - Parameters: 69 | /// - key: The key to read and write the value to in the user defaults 70 | /// store. 71 | /// - store: The user defaults store to read and write to. A value 72 | /// of `nil` will use the user default store from the environment. 73 | /// 74 | public init(_ key: Key.Type, store: UserDefaults? = nil) where Key.Value == String { 75 | self._value = AppStorage(wrappedValue: Key.defaultValue, Key.lookupName, store: store) 76 | } 77 | 78 | /// Creates a property that can read and write to a double user default. 79 | /// 80 | /// - Parameters: 81 | /// - key: The key to read and write the value to in the user defaults 82 | /// store. 83 | /// - store: The user defaults store to read and write to. A value 84 | /// of `nil` will use the user default store from the environment. 85 | /// 86 | public init(_ key: Key.Type, store: UserDefaults? = nil) where Key.Value == Double { 87 | self._value = AppStorage(wrappedValue: Key.defaultValue, Key.lookupName, store: store) 88 | } 89 | 90 | /// Creates a property that can read and write to a integer user default. 91 | /// 92 | /// - Parameters: 93 | /// - key: The key to read and write the value to in the user defaults 94 | /// store. 95 | /// - store: The user defaults store to read and write to. A value 96 | /// of `nil` will use the user default store from the environment. 97 | /// 98 | public init(_ key: Key.Type, store: UserDefaults? = nil) where Key.Value == Int { 99 | self._value = AppStorage(wrappedValue: Key.defaultValue, Key.lookupName, store: store) 100 | } 101 | 102 | /// Creates a property that can read and write to a url user default. 103 | /// 104 | /// - Parameters: 105 | /// - key: The key to read and write the value to in the user defaults 106 | /// store. 107 | /// - store: The user defaults store to read and write to. A value 108 | /// of `nil` will use the user default store from the environment. 109 | /// 110 | public init(_ key: Key.Type, store: UserDefaults? = nil) where Key.Value == URL { 111 | self._value = AppStorage(wrappedValue: Key.defaultValue, Key.lookupName, store: store) 112 | } 113 | 114 | /// Creates a property that can read and write to a user default as data. 115 | /// 116 | /// Avoid storing large data blobs in user defaults, such as image data, 117 | /// as it can negatively affect performance of your app. On tvOS, a 118 | /// `NSUserDefaultsSizeLimitExceededNotification` notification is posted 119 | /// if the total user default size reaches 512kB. 120 | /// 121 | /// - Parameters: 122 | /// - key: The key to read and write the value to in the user defaults 123 | /// store. 124 | /// - store: The user defaults store to read and write to. A value 125 | /// of `nil` will use the user default store from the environment. 126 | /// 127 | public init(_ key: Key.Type, store: UserDefaults? = nil) where Key.Value == Data { 128 | self._value = AppStorage(wrappedValue: Key.defaultValue, Key.lookupName, store: store) 129 | } 130 | 131 | /// Creates a property that can read and write to an integer user default, 132 | /// transforming that to `RawRepresentable` data type. 133 | /// 134 | /// A common usage is with enumerations: 135 | /// 136 | /// enum MyEnum: Int { 137 | /// case a 138 | /// case b 139 | /// case c 140 | /// } 141 | /// struct MyView: View { 142 | /// @UserPreference(MyKey.self) private var value 143 | /// var body: some View { ... } 144 | /// } 145 | /// 146 | /// - Parameters: 147 | /// - key: The key to read and write the value to in the user defaults 148 | /// store. 149 | /// - store: The user defaults store to read and write to. A value 150 | /// of `nil` will use the user default store from the environment. 151 | /// 152 | public init(_ key: Key.Type, store: UserDefaults? = nil) where Key.Value : RawRepresentable, Key.Value.RawValue == Int { 153 | self._value = AppStorage(wrappedValue: Key.defaultValue, Key.lookupName, store: store) 154 | } 155 | 156 | /// Creates a property that can read and write to a string user default, 157 | /// transforming that to `RawRepresentable` data type. 158 | /// 159 | /// A common usage is with enumerations: 160 | /// 161 | /// enum MyEnum: String { 162 | /// case a 163 | /// case b 164 | /// case c 165 | /// } 166 | /// struct MyView: View { 167 | /// @UserPreference(MyKey.self) private var value 168 | /// var body: some View { ... } 169 | /// } 170 | /// 171 | /// - Parameters: 172 | /// - key: The key to read and write the value to in the user defaults 173 | /// store. 174 | /// - store: The user defaults store to read and write to. A value 175 | /// of `nil` will use the user default store from the environment. 176 | /// 177 | public init(_ key: Key.Type, store: UserDefaults? = nil) where Key.Value : RawRepresentable, Key.Value.RawValue == String { 178 | self._value = AppStorage(wrappedValue: Key.defaultValue, Key.lookupName, store: store) 179 | } 180 | } 181 | 182 | // MARK: - Optional Initialisers 183 | 184 | extension UserPreference where Key.Value : ExpressibleByNilLiteral { 185 | 186 | public init(_ key: Key.Type, store: UserDefaults? = nil) where Key.Value == Bool? { 187 | self._value = AppStorage(Key.lookupName, store: store) 188 | } 189 | 190 | public init(_ key: Key.Type, store: UserDefaults? = nil) where Key.Value == String? { 191 | self._value = AppStorage(Key.lookupName, store: store) 192 | } 193 | 194 | public init(_ key: Key.Type, store: UserDefaults? = nil) where Key.Value == Double? { 195 | self._value = AppStorage(Key.lookupName, store: store) 196 | } 197 | 198 | public init(_ key: Key.Type, store: UserDefaults? = nil) where Key.Value == Int? { 199 | self._value = AppStorage(Key.lookupName, store: store) 200 | } 201 | 202 | public init(_ key: Key.Type, store: UserDefaults? = nil) where Key.Value == URL? { 203 | self._value = AppStorage(Key.lookupName, store: store) 204 | } 205 | 206 | public init(_ key: Key.Type, store: UserDefaults? = nil) where Key.Value == Data? { 207 | self._value = AppStorage(Key.lookupName, store: store) 208 | } 209 | } 210 | 211 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) 212 | extension UserPreference { 213 | 214 | public init(_ key: Key.Type, store: UserDefaults? = nil) where Key.Value == R?, R : RawRepresentable, R.RawValue == String { 215 | self._value = AppStorage(Key.lookupName, store: store) 216 | } 217 | 218 | public init(_ key: Key.Type, store: UserDefaults? = nil) where Key.Value == R?, R : RawRepresentable, R.RawValue == Int { 219 | self._value = AppStorage(Key.lookupName, store: store) 220 | } 221 | } 222 | --------------------------------------------------------------------------------