parent
39160d389f
commit
12a65c057d
|
@ -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 */,
|
||||
|
|
|
@ -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
|
|
@ -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
readme.md
34
readme.md
|
@ -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?
|
||||
|
|
Loading…
Reference in New Issue