Skip to content

Commit

Permalink
Add @Default property wrapper for SwiftUI
Browse files Browse the repository at this point in the history
  • Loading branch information
sindresorhus authored and dezinezync committed Dec 17, 2022
1 parent 9877522 commit 280789d
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 2 deletions.
10 changes: 10 additions & 0 deletions Defaults.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -215,6 +220,7 @@
E339B3B22449ED2000E7A40A /* Reset.swift */,
E3EB3E34216507AE0033B089 /* Observation.swift */,
E286D0C623B8D51100570D1E /* Observation+Combine.swift */,
E38C9F26244ADA2F00A6737A /* SwiftUI.swift */,
E3EB3E32216505920033B089 /* util.swift */,
);
path = Defaults;
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand All @@ -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 */,
Expand All @@ -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 */,
Expand Down
113 changes: 113 additions & 0 deletions Sources/Defaults/SwiftUI.swift
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions Sources/Defaults/util.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
34 changes: 32 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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?
Expand Down

0 comments on commit 280789d

Please sign in to comment.