Add `@Default` property wrapper for SwiftUI

Fixes #28
This commit is contained in:
Sindre Sorhus 2020-04-18 15:25:39 +08:00
parent 39160d389f
commit 12a65c057d
4 changed files with 169 additions and 2 deletions

View File

@ -29,6 +29,10 @@
E339B3B92449F10D00E7A40A /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E339B3B72449F10D00E7A40A /* UserDefaults.swift */; };
E339B3BA2449F10D00E7A40A /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E339B3B72449F10D00E7A40A /* UserDefaults.swift */; };
E339B3BB2449F10D00E7A40A /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E339B3B72449F10D00E7A40A /* UserDefaults.swift */; };
E38C9F27244ADA2F00A6737A /* SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38C9F26244ADA2F00A6737A /* SwiftUI.swift */; };
E38C9F28244ADA2F00A6737A /* SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38C9F26244ADA2F00A6737A /* SwiftUI.swift */; };
E38C9F29244ADA2F00A6737A /* SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38C9F26244ADA2F00A6737A /* SwiftUI.swift */; };
E38C9F2A244ADA2F00A6737A /* SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38C9F26244ADA2F00A6737A /* SwiftUI.swift */; };
E3EB3E33216505920033B089 /* util.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E32216505920033B089 /* util.swift */; };
E3EB3E35216507AE0033B089 /* Observation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E34216507AE0033B089 /* Observation.swift */; };
E3EB3E36216507B50033B089 /* Observation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E34216507AE0033B089 /* Observation.swift */; };
@ -79,6 +83,7 @@
E286D0C623B8D51100570D1E /* Observation+Combine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "Observation+Combine.swift"; sourceTree = "<group>"; usesTabs = 1; };
E339B3B22449ED2000E7A40A /* Reset.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Reset.swift; sourceTree = "<group>"; usesTabs = 1; };
E339B3B72449F10D00E7A40A /* UserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = UserDefaults.swift; sourceTree = "<group>"; usesTabs = 1; };
E38C9F26244ADA2F00A6737A /* SwiftUI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = SwiftUI.swift; sourceTree = "<group>"; usesTabs = 1; };
E3EB3E32216505920033B089 /* util.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = util.swift; sourceTree = "<group>"; usesTabs = 1; };
E3EB3E34216507AE0033B089 /* Observation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Observation.swift; sourceTree = "<group>"; usesTabs = 1; };
/* End PBXFileReference section */
@ -215,6 +220,7 @@
E339B3B22449ED2000E7A40A /* Reset.swift */,
E3EB3E34216507AE0033B089 /* Observation.swift */,
E286D0C623B8D51100570D1E /* Observation+Combine.swift */,
E38C9F26244ADA2F00A6737A /* SwiftUI.swift */,
E3EB3E32216505920033B089 /* util.swift */,
);
path = Defaults;
@ -502,6 +508,7 @@
buildActionMask = 2147483647;
files = (
E286D0C823B8D54C00570D1E /* Observation+Combine.swift in Sources */,
E38C9F28244ADA2F00A6737A /* SwiftUI.swift in Sources */,
8933C7851EB5B820000D00A4 /* Defaults.swift in Sources */,
E339B3B92449F10D00E7A40A /* UserDefaults.swift in Sources */,
E3EB3E35216507AE0033B089 /* Observation.swift in Sources */,
@ -523,6 +530,7 @@
buildActionMask = 2147483647;
files = (
E286D0CA23B8D54E00570D1E /* Observation+Combine.swift in Sources */,
E38C9F2A244ADA2F00A6737A /* SwiftUI.swift in Sources */,
E3EB3E3A216507C40033B089 /* util.swift in Sources */,
E339B3BB2449F10D00E7A40A /* UserDefaults.swift in Sources */,
E3EB3E37216507B50033B089 /* Observation.swift in Sources */,
@ -536,6 +544,7 @@
buildActionMask = 2147483647;
files = (
E286D0C923B8D54D00570D1E /* Observation+Combine.swift in Sources */,
E38C9F29244ADA2F00A6737A /* SwiftUI.swift in Sources */,
E3EB3E3B216507C40033B089 /* util.swift in Sources */,
E339B3BA2449F10D00E7A40A /* UserDefaults.swift in Sources */,
E3EB3E38216507B60033B089 /* Observation.swift in Sources */,
@ -549,6 +558,7 @@
buildActionMask = 2147483647;
files = (
E286D0C723B8D51100570D1E /* Observation+Combine.swift in Sources */,
E38C9F27244ADA2F00A6737A /* SwiftUI.swift in Sources */,
E3EB3E39216507C30033B089 /* util.swift in Sources */,
E339B3B82449F10D00E7A40A /* UserDefaults.swift in Sources */,
E3EB3E36216507B50033B089 /* Observation.swift in Sources */,

View File

@ -0,0 +1,113 @@
#if canImport(Combine)
import SwiftUI
import Combine
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension Defaults {
final class Observable<Value: Codable>: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
private var observation: DefaultsObservation?
private let key: Defaults.Key<Value>
var value: Value {
get { Defaults[key] }
set {
objectWillChange.send()
Defaults[key] = newValue
}
}
init(_ key: Key<Value>) {
self.key = key
self.observation = Defaults.observe(key, options: [.prior]) { [weak self] change in
guard change.isPrior else {
return
}
DispatchQueue.mainSafeAsync {
self?.objectWillChange.send()
}
}
}
/// Reset the key back to its default value.
func reset() {
key.reset()
}
}
}
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper
public struct Default<Value: Codable>: DynamicProperty {
public typealias Publisher = AnyPublisher<Defaults.KeyChange<Value>, Never>
private let key: Defaults.Key<Value>
@ObservedObject private var observable: Defaults.Observable<Value>
/**
Get/set a `Defaults` item and also have the view be updated when the value changes. This is similar to `@State`.
```
extension Defaults.Keys {
static let hasUnicorn = Key<Bool>("hasUnicorn", default: false)
}
struct ContentView: View {
@Default(.hasUnicorn) var hasUnicorn
var body: some View {
Text("Has Unicorn: \(hasUnicorn)")
Toggle("Toggle Unicorn", isOn: $hasUnicorn)
}
}
```
*/
public init(_ key: Defaults.Key<Value>) {
self.key = key
self.observable = Defaults.Observable(key)
}
public var wrappedValue: Value {
get { observable.value }
nonmutating set {
observable.value = newValue
}
}
public var projectedValue: Binding<Value> { $observable.value }
/// Combine publisher that publishes values when the `Defaults` item changes.
public var publisher: Publisher { Defaults.publisher(key) }
public mutating func update() {
_observable.update()
}
/**
Reset the key back to its default value.
```
extension Defaults.Keys {
static let opacity = Key<Double>("opacity", default: 1)
}
struct ContentView: View {
@Default(.opacity) var opacity
var body: some View {
Button("Reset") {
self._opacity.reset()
}
}
}
```
*/
public func reset() {
key.reset()
}
}
#endif

View File

@ -132,3 +132,17 @@ extension Optional: _DefaultsOptionalType {
func isOptionalType<T>(_ type: T.Type) -> Bool {
type is _DefaultsOptionalType.Type
}
extension DispatchQueue {
/**
Performs the `execute` closure immediately if we're on the main thread or asynchronously puts it on the main thread otherwise.
*/
static func mainSafeAsync(execute work: @escaping () -> Void) {
if Thread.isMainThread {
work()
} else {
main.async(execute: work)
}
}
}

View File

@ -13,9 +13,10 @@ It's used in production by apps like [Gifski](https://github.com/sindresorhus/Gi
- **Strongly typed:** You declare the type and default value upfront.
- **Codable support:** You can store any [Codable](https://developer.apple.com/documentation/swift/codable) value, like an enum.
- **NSSecureCoding support:** You can store any [NSSecureCoding](https://developer.apple.com/documentation/foundation/nssecurecoding) value.
- **Debuggable:** The data is stored as JSON-serialized values.
- **Observation:** Observe changes to keys.
- **SwiftUI:** Property wrapper that updates the view when the `UserDefaults` value changes.
- **Publishers:** Combine publishers built-in.
- **Observation:** Observe changes to keys.
- **Debuggable:** The data is stored as JSON-serialized values.
## Compatibility
@ -141,6 +142,29 @@ Defaults[isUnicorn]
//=> true
```
### SwiftUI support
You can use the `@Default` property wrapper to get/set a `Defaults` item and also have the view be updated when the value changes. This is similar to `@State`.
```swift
extension Defaults.Keys {
static let hasUnicorn = Key<Bool>("hasUnicorn", default: false)
}
struct ContentView: View {
@Default(.hasUnicorn) var hasUnicorn
var body: some View {
Text("Has Unicorn: \(hasUnicorn)")
Toggle("Toggle Unicorn", isOn: $hasUnicorn)
}
}
```
Note that it's `@Default`, not `@Defaults`.
This is only implemented for `Defaults.Key`. PR welcome for `Defaults.NSSecureCoding` if you need it.
### Observe changes to a key
```swift
@ -449,6 +473,12 @@ Break the lifetime tie created by `tieToLifetime(of:)`, if one exists.
The effects of any call to `tieToLifetime(of:)` are reversed. Note however that if the tied-to object has already died, then the observation is already invalid and this method has no logical effect.
### `@Default(_ key:)`
Get/set a `Defaults` item and also have the view be updated when the value changes.
This is only implemented for `Defaults.Key`. PR welcome for `Defaults.NSSecureCoding` if you need it.
## FAQ
### How can I store a dictionary of arbitrary values?