This repository has been archived by the owner on May 10, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 441
/
Preferences.swift
242 lines (227 loc) · 8.84 KB
/
Preferences.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import Shared
import os.log
import Combine
/// The applications preferences container
///
/// Properties in this object should be of the the type `Option` with the object which is being
/// stored to automatically interact with `UserDefaults`
public class Preferences {
/// The default `UserDefaults` that all `Option`s will use unless specified
public static let defaultContainer = UserDefaults(suiteName: AppInfo.sharedContainerIdentifier)!
}
/// Defines an object which may watch a set of `Preference.Option`s
/// - note: @objc was added here due to a Swift compiler bug which doesn't allow a class-bound protocol
/// to act as `AnyObject` in a `AnyObject` generic constraint (i.e. `WeakList`)
@objc public protocol PreferencesObserver: AnyObject {
/// A preference value was changed for some given preference key
func preferencesDidChange(for key: String)
}
extension Preferences {
/// An entry in the `Preferences`
///
/// `ValueType` defines the type of value that will stored in the UserDefaults object
public class Option<ValueType: Equatable>: ObservableObject {
/// The list of observers for this option
private let observers = WeakList<PreferencesObserver>()
/// The UserDefaults container that you wish to save to
public let container: UserDefaults
/// The current value of this preference
///
/// Upon setting this value, UserDefaults will be updated and any observers will be called
@Published public var value: ValueType {
didSet {
if value == oldValue { return }
writePreferenceValue(container, key, value)
container.synchronize()
let key = self.key
observers.forEach {
$0.preferencesDidChange(for: key)
}
}
}
/// Adds `object` as an observer for this Option.
public func observe(from object: PreferencesObserver) {
observers.insert(object)
}
/// The key used for getting/setting the value in `UserDefaults`
public let key: String
/// The default value of this preference
public let defaultValue: ValueType
/// Reset's the preference to its original default value
public func reset() {
value = defaultValue
}
private var writePreferenceValue: ((UserDefaults, String, ValueType) -> Void)
fileprivate init(
key: String,
initialValue: ValueType,
defaultValue: ValueType,
container: UserDefaults = Preferences.defaultContainer,
writePreferenceValue: @escaping (UserDefaults, String, ValueType) -> Void
) {
self.key = key
self.container = container
self.value = initialValue
self.defaultValue = defaultValue
self.writePreferenceValue = writePreferenceValue
}
}
}
extension Preferences.Option {
/// Creates a preference and fetches the initial value from the container the default way
private convenience init(
key: String,
defaultValue: ValueType,
container: UserDefaults
) where ValueType: UserDefaultsEncodable {
let initialValue = (container.value(forKey: key) as? ValueType) ?? defaultValue
self.init(
key: key,
initialValue: initialValue,
defaultValue: defaultValue,
container: container,
writePreferenceValue: { $0.set($2, forKey: $1) }
)
}
/// Creates a preference storing a user defaults supported value type
public convenience init(
key: String,
default: ValueType,
container: UserDefaults = Preferences.defaultContainer
) where ValueType: UserDefaultsEncodable {
self.init(key: key, defaultValue: `default`, container: container)
}
/// Creates a preference storing an array of user defaults supported value types
public convenience init<V>(
key: String,
default: ValueType,
container: UserDefaults = Preferences.defaultContainer
) where V: UserDefaultsEncodable, ValueType == [V] {
self.init(key: key, defaultValue: `default`, container: container)
}
/// Creates a preference storing an dictionary of user defaults supported value types
public convenience init<K, V>(
key: String, default: ValueType,
container: UserDefaults = Preferences.defaultContainer
) where K: StringProtocol, V: UserDefaultsEncodable, ValueType == [K: V] {
self.init(key: key, defaultValue: `default`, container: container)
}
}
extension Preferences.Option where ValueType: ExpressibleByNilLiteral {
/// Creates a preference and fetches the initial value from the container the default way
private convenience init<V>(
key: String,
defaultValue: ValueType,
container: UserDefaults
) where V: UserDefaultsEncodable, ValueType == V? {
let initialValue = (container.value(forKey: key) as? ValueType) ?? defaultValue
self.init(
key: key,
initialValue: initialValue,
defaultValue: defaultValue,
container: container,
writePreferenceValue: { container, key, value in
if let value = value {
container.set(value, forKey: key)
} else {
container.removeObject(forKey: key)
}
}
)
}
/// Creates a preference storing an optional user defaults supported value type
public convenience init<V>(
key: String,
default: ValueType,
container: UserDefaults = Preferences.defaultContainer
) where ValueType == V?, V: UserDefaultsEncodable {
self.init(key: key, defaultValue: `default`, container: container)
}
/// Creates a preference storing an optional array of user defaults supported value types
public convenience init<V>(
key: String,
default: ValueType,
container: UserDefaults = Preferences.defaultContainer
) where V: UserDefaultsEncodable, ValueType == [V]? {
self.init(key: key, defaultValue: `default`, container: container)
}
/// Creates a preference storing an optional dictionary of user defaults supported value types
public convenience init<K, V>(
key: String,
default: ValueType,
container: UserDefaults = Preferences.defaultContainer
) where K: StringProtocol, V: UserDefaultsEncodable, ValueType == [K: V]? {
self.init(key: key, defaultValue: `default`, container: container)
}
}
extension Preferences.Option {
/// Creates a preference storing a raw representable where the raw value is a user defaults supported value type
public convenience init(
key: String,
default: ValueType,
container: UserDefaults = Preferences.defaultContainer
) where ValueType: RawRepresentable, ValueType.RawValue: UserDefaultsEncodable {
let initialValue: ValueType = {
if let rawValue = (container.value(forKey: key) as? ValueType.RawValue) {
if let value = ValueType(rawValue: rawValue) {
return value
} else {
Logger.module.error("Failed to load enum preference \"\(key)\" with raw value \(String(describing: rawValue))")
}
}
return `default`
}()
self.init(
key: key,
initialValue: initialValue,
defaultValue: `default`,
container: container,
writePreferenceValue: { $0.setValue($2.rawValue, forKey: $1) }
)
}
/// Creates a preference storing an optional raw representable where the raw value is a user defaults
/// supported value type
public convenience init<R>(
key: String,
default: ValueType,
container: UserDefaults = Preferences.defaultContainer
) where ValueType == R?, R: RawRepresentable, R.RawValue: UserDefaultsEncodable {
let initialValue: R? = {
if let rawValue = (container.value(forKey: key) as? R.RawValue) {
return R(rawValue: rawValue)
}
return nil
}()
self.init(
key: key,
initialValue: initialValue,
defaultValue: `default`,
container: container,
writePreferenceValue: { container, key, value in
if let value = value {
container.setValue(value.rawValue, forKey: key)
} else {
container.removeObject(forKey: key)
}
}
)
}
}
/// An empty protocol simply here to force the developer to use a user defaults encodable value via generic constraint.
/// DO NOT ADD CONFORMANCE TO ANY OTHER TYPES. These are specifically the types supported by UserDefaults
public protocol UserDefaultsEncodable: Equatable {}
extension Bool: UserDefaultsEncodable {}
extension Int: UserDefaultsEncodable {}
extension UInt: UserDefaultsEncodable {}
extension Float: UserDefaultsEncodable {}
extension Double: UserDefaultsEncodable {}
extension String: UserDefaultsEncodable {}
extension URL: UserDefaultsEncodable {}
extension Data: UserDefaultsEncodable {}
extension Date: UserDefaultsEncodable {}
extension Array: UserDefaultsEncodable where Element: UserDefaultsEncodable {}
extension Dictionary: UserDefaultsEncodable where Key: StringProtocol, Value: UserDefaultsEncodable {}