├── .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 |
--------------------------------------------------------------------------------