parent
ea11b7ac4f
commit
7a22d37874
|
@ -76,7 +76,7 @@ extension Defaults {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- Warning: The key must be ASCII, not start with `@`, and cannot contain a dot (`.`).
|
- Warning: The `UserDefaults` name must be ASCII, not start with `@`, and cannot contain a dot (`.`).
|
||||||
*/
|
*/
|
||||||
public final class Key<Value: Serializable>: _AnyKey {
|
public final class Key<Value: Serializable>: _AnyKey {
|
||||||
/**
|
/**
|
||||||
|
@ -90,7 +90,7 @@ extension Defaults {
|
||||||
public var defaultValue: Value { defaultValueGetter() }
|
public var defaultValue: Value { defaultValueGetter() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Create a defaults key.
|
Create a key.
|
||||||
|
|
||||||
- Parameter key: The key must be ASCII, not start with `@`, and cannot contain a dot (`.`).
|
- Parameter key: The key must be ASCII, not start with `@`, and cannot contain a dot (`.`).
|
||||||
|
|
||||||
|
@ -118,7 +118,7 @@ extension Defaults {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Create a defaults key with a dynamic default value.
|
Create a key with a dynamic default value.
|
||||||
|
|
||||||
This can be useful in cases where you cannot define a static default value as it may change during the lifetime of the app.
|
This can be useful in cases where you cannot define a static default value as it may change during the lifetime of the app.
|
||||||
|
|
||||||
|
@ -143,7 +143,7 @@ extension Defaults {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Create a defaults key with an optional value.
|
Create a key with an optional value.
|
||||||
|
|
||||||
- Parameter key: The key must be ASCII, not start with `@`, and cannot contain a dot (`.`).
|
- Parameter key: The key must be ASCII, not start with `@`, and cannot contain a dot (`.`).
|
||||||
*/
|
*/
|
||||||
|
@ -286,3 +286,83 @@ extension Defaults {
|
||||||
*/
|
*/
|
||||||
typealias CodableBridge = _DefaultsCodableBridge
|
typealias CodableBridge = _DefaultsCodableBridge
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Defaults {
|
||||||
|
/**
|
||||||
|
Observe updates to a stored value.
|
||||||
|
|
||||||
|
- Parameter initial: Trigger an initial event on creation. This can be useful for setting default values on controls.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
extension Defaults.Keys {
|
||||||
|
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// …
|
||||||
|
|
||||||
|
Task {
|
||||||
|
for await value in Defaults.updates(.isUnicornMode) {
|
||||||
|
print("Value:", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
public static func updates<Value: Serializable>(
|
||||||
|
_ key: Key<Value>,
|
||||||
|
initial: Bool = true
|
||||||
|
) -> AsyncStream<Value> { // TODO: Make this `some AsyncSequence<Value>` when Swift 6 is out.
|
||||||
|
.init { continuation in
|
||||||
|
let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in
|
||||||
|
// TODO: Use the `.deserialize` method directly.
|
||||||
|
let value = KeyChange(change: change, defaultValue: key.defaultValue).newValue
|
||||||
|
continuation.yield(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
observation.start(options: initial ? [.initial] : [])
|
||||||
|
|
||||||
|
continuation.onTermination = { _ in
|
||||||
|
observation.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Make this include a tuple with the values when Swift supports variadic generics. I can then simply use `merge()` with the first `updates()` method.
|
||||||
|
/**
|
||||||
|
Observe updates to multiple stored values.
|
||||||
|
|
||||||
|
- Parameter initial: Trigger an initial event on creation. This can be useful for setting default values on controls.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
Task {
|
||||||
|
for await _ in Defaults.updates([.foo, .bar]) {
|
||||||
|
print("One of the values changed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Note: This does not include which of the values changed. Use ``Defaults/updates(_:initial:)-9eh8`` if you need that. You could use [`merge`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Merge.md) to merge them into a single sequence.
|
||||||
|
*/
|
||||||
|
public static func updates(
|
||||||
|
_ keys: [_AnyKey],
|
||||||
|
initial: Bool = true
|
||||||
|
) -> AsyncStream<Void> { // TODO: Make this `some AsyncSequence<Value>` when Swift 6 is out.
|
||||||
|
.init { continuation in
|
||||||
|
let observations = keys.indexed().map { index, key in
|
||||||
|
let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { _ in
|
||||||
|
continuation.yield()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we only trigger a single initial event.
|
||||||
|
observation.start(options: initial && index == 0 ? [.initial] : [])
|
||||||
|
|
||||||
|
return observation
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation.onTermination = { _ in
|
||||||
|
for observation in observations {
|
||||||
|
observation.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -40,6 +40,8 @@ Defaults[.quality] = 0.5
|
||||||
|
|
||||||
### Methods
|
### Methods
|
||||||
|
|
||||||
|
- ``Defaults/updates(_:initial:)-9eh8``
|
||||||
|
- ``Defaults/updates(_:initial:)-1mqkb``
|
||||||
- ``Defaults/reset(_:)-7jv5v``
|
- ``Defaults/reset(_:)-7jv5v``
|
||||||
- ``Defaults/reset(_:)-7es1e``
|
- ``Defaults/reset(_:)-7es1e``
|
||||||
- ``Defaults/removeAll(suite:)``
|
- ``Defaults/removeAll(suite:)``
|
||||||
|
@ -49,11 +51,6 @@ Defaults[.quality] = 0.5
|
||||||
- ``Default``
|
- ``Default``
|
||||||
- ``Defaults/Toggle``
|
- ``Defaults/Toggle``
|
||||||
|
|
||||||
### Events
|
|
||||||
|
|
||||||
- ``Defaults/publisher(_:options:)``
|
|
||||||
- ``Defaults/publisher(keys:options:)``
|
|
||||||
|
|
||||||
### Force Type Resolution
|
### Force Type Resolution
|
||||||
|
|
||||||
- ``Defaults/PreferRawRepresentable``
|
- ``Defaults/PreferRawRepresentable``
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
#if canImport(Combine)
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
|
@ -84,6 +83,8 @@ extension Defaults {
|
||||||
//=> false
|
//=> false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-9eh8`` instead.
|
||||||
*/
|
*/
|
||||||
public static func publisher<Value: Serializable>(
|
public static func publisher<Value: Serializable>(
|
||||||
_ key: Key<Value>,
|
_ key: Key<Value>,
|
||||||
|
@ -97,6 +98,8 @@ extension Defaults {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Publisher for multiple `Key<T>` observation, but without specific information about changes.
|
Publisher for multiple `Key<T>` observation, but without specific information about changes.
|
||||||
|
|
||||||
|
- Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-9eh8`` instead.
|
||||||
*/
|
*/
|
||||||
public static func publisher(
|
public static func publisher(
|
||||||
keys: _AnyKey...,
|
keys: _AnyKey...,
|
||||||
|
@ -118,4 +121,3 @@ extension Defaults {
|
||||||
return combinedPublisher
|
return combinedPublisher
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
|
@ -140,7 +140,7 @@ extension Defaults {
|
||||||
object?.addObserver(self, forKeyPath: key, options: options.toNSKeyValueObservingOptions, context: nil)
|
object?.addObserver(self, forKeyPath: key, options: options.toNSKeyValueObservingOptions, context: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func invalidate() {
|
func invalidate() {
|
||||||
object?.removeObserver(self, forKeyPath: key, context: nil)
|
object?.removeObserver(self, forKeyPath: key, context: nil)
|
||||||
object = nil
|
object = nil
|
||||||
lifetimeAssociation?.cancel()
|
lifetimeAssociation?.cancel()
|
||||||
|
@ -148,7 +148,7 @@ extension Defaults {
|
||||||
|
|
||||||
private var lifetimeAssociation: LifetimeAssociation?
|
private var lifetimeAssociation: LifetimeAssociation?
|
||||||
|
|
||||||
public func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self {
|
func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self {
|
||||||
// swiftlint:disable:next trailing_closure
|
// swiftlint:disable:next trailing_closure
|
||||||
lifetimeAssociation = LifetimeAssociation(of: self, with: weaklyHeldObject, deinitHandler: { [weak self] in
|
lifetimeAssociation = LifetimeAssociation(of: self, with: weaklyHeldObject, deinitHandler: { [weak self] in
|
||||||
self?.invalidate()
|
self?.invalidate()
|
||||||
|
@ -157,7 +157,7 @@ extension Defaults {
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
public func removeLifetimeTie() {
|
func removeLifetimeTie() {
|
||||||
lifetimeAssociation?.cancel()
|
lifetimeAssociation?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,7 +216,7 @@ extension Defaults {
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func start(options: ObservationOptions) {
|
func start(options: ObservationOptions) {
|
||||||
for observable in observables {
|
for observable in observables {
|
||||||
observable.suite?.addObserver(
|
observable.suite?.addObserver(
|
||||||
self,
|
self,
|
||||||
|
@ -227,7 +227,7 @@ extension Defaults {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func invalidate() {
|
func invalidate() {
|
||||||
for observable in observables {
|
for observable in observables {
|
||||||
observable.suite?.removeObserver(self, forKeyPath: observable.key, context: &Self.observationContext)
|
observable.suite?.removeObserver(self, forKeyPath: observable.key, context: &Self.observationContext)
|
||||||
observable.suite = nil
|
observable.suite = nil
|
||||||
|
@ -236,7 +236,7 @@ extension Defaults {
|
||||||
lifetimeAssociation?.cancel()
|
lifetimeAssociation?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self {
|
func tieToLifetime(of weaklyHeldObject: AnyObject) -> Self {
|
||||||
// swiftlint:disable:next trailing_closure
|
// swiftlint:disable:next trailing_closure
|
||||||
lifetimeAssociation = LifetimeAssociation(of: self, with: weaklyHeldObject, deinitHandler: { [weak self] in
|
lifetimeAssociation = LifetimeAssociation(of: self, with: weaklyHeldObject, deinitHandler: { [weak self] in
|
||||||
self?.invalidate()
|
self?.invalidate()
|
||||||
|
@ -245,7 +245,7 @@ extension Defaults {
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
public func removeLifetimeTie() {
|
func removeLifetimeTie() {
|
||||||
lifetimeAssociation?.cancel()
|
lifetimeAssociation?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,6 +293,8 @@ extension Defaults {
|
||||||
//=> false
|
//=> false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-9eh8`` instead.
|
||||||
*/
|
*/
|
||||||
public static func observe<Value: Serializable>(
|
public static func observe<Value: Serializable>(
|
||||||
_ key: Key<Value>,
|
_ key: Key<Value>,
|
||||||
|
@ -322,6 +324,8 @@ extension Defaults {
|
||||||
// …
|
// …
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- Warning: This method exists for backwards compatibility and will be deprecated sometime in the future. Use ``Defaults/updates(_:initial:)-9eh8`` instead.
|
||||||
*/
|
*/
|
||||||
public static func observe(
|
public static func observe(
|
||||||
keys: _AnyKey...,
|
keys: _AnyKey...,
|
||||||
|
|
|
@ -25,7 +25,7 @@ extension Defaults {
|
||||||
if #available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) {
|
if #available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) {
|
||||||
// The `@MainActor` is important as the `.send()` method doesn't inherit the `@MainActor` from the class.
|
// The `@MainActor` is important as the `.send()` method doesn't inherit the `@MainActor` from the class.
|
||||||
self.task = .detached(priority: .userInitiated) { @MainActor [weak self] in
|
self.task = .detached(priority: .userInitiated) { @MainActor [weak self] in
|
||||||
for await _ in Defaults.events(key) {
|
for await _ in Defaults.updates(key) {
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -230,28 +230,6 @@ extension Defaults.Toggle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Defaults {
|
|
||||||
// TODO: Expose this publicly at some point.
|
|
||||||
private static func events<Value: Serializable>(
|
|
||||||
_ key: Defaults.Key<Value>,
|
|
||||||
initial: Bool = true
|
|
||||||
) -> AsyncStream<Value> { // TODO: Make this `some AsyncSequence<Value>` when Swift 6 is out.
|
|
||||||
.init { continuation in
|
|
||||||
let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in
|
|
||||||
// TODO: Use the `.deserialize` method directly.
|
|
||||||
let value = KeyChange(change: change, defaultValue: key.defaultValue).newValue
|
|
||||||
continuation.yield(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
observation.start(options: initial ? [.initial] : [])
|
|
||||||
|
|
||||||
continuation.onTermination = { _ in
|
|
||||||
observation.invalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@propertyWrapper
|
@propertyWrapper
|
||||||
private struct ViewStorage<Value>: DynamicProperty {
|
private struct ViewStorage<Value>: DynamicProperty {
|
||||||
private final class ValueBox {
|
private final class ValueBox {
|
||||||
|
|
|
@ -146,6 +146,7 @@ extension Sequence {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
extension Collection {
|
extension Collection {
|
||||||
subscript(safe index: Index) -> Element? {
|
subscript(safe index: Index) -> Element? {
|
||||||
indices.contains(index) ? self[index] : nil
|
indices.contains(index) ? self[index] : nil
|
||||||
|
@ -153,6 +154,13 @@ extension Collection {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension Collection {
|
||||||
|
func indexed() -> some Sequence<(Index, Element)> {
|
||||||
|
zip(indices, self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
extension Defaults.Serializable {
|
extension Defaults.Serializable {
|
||||||
/**
|
/**
|
||||||
Cast a `Serializable` value to `Self`.
|
Cast a `Serializable` value to `Self`.
|
||||||
|
|
|
@ -48,7 +48,7 @@ extension PlainHourMinuteTimeRange: Defaults.Serializable {
|
||||||
typealias Value = PlainHourMinuteTimeRange
|
typealias Value = PlainHourMinuteTimeRange
|
||||||
typealias Serializable = [PlainHourMinuteTime]
|
typealias Serializable = [PlainHourMinuteTime]
|
||||||
|
|
||||||
public func serialize(_ value: Value?) -> Serializable? {
|
func serialize(_ value: Value?) -> Serializable? {
|
||||||
guard let value = value else {
|
guard let value = value else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ extension PlainHourMinuteTimeRange: Defaults.Serializable {
|
||||||
return [value.start, value.end]
|
return [value.start, value.end]
|
||||||
}
|
}
|
||||||
|
|
||||||
public func deserialize(_ object: Serializable?) -> Value? {
|
func deserialize(_ object: Serializable?) -> Value? {
|
||||||
guard
|
guard
|
||||||
let array = object,
|
let array = object,
|
||||||
let start = array[safe: 0],
|
let start = array[safe: 0],
|
||||||
|
|
|
@ -19,18 +19,18 @@ struct DefaultsSetAlgebra<Element: Defaults.Serializable & Hashable>: SetAlgebra
|
||||||
store.contains(member)
|
store.contains(member)
|
||||||
}
|
}
|
||||||
|
|
||||||
func union(_ other: DefaultsSetAlgebra) -> DefaultsSetAlgebra {
|
func union(_ other: Self) -> Self {
|
||||||
DefaultsSetAlgebra(store.union(other.store))
|
Self(store.union(other.store))
|
||||||
}
|
}
|
||||||
|
|
||||||
func intersection(_ other: DefaultsSetAlgebra) -> DefaultsSetAlgebra {
|
func intersection(_ other: Self) -> Self {
|
||||||
var defaultsSetAlgebra = DefaultsSetAlgebra()
|
var defaultsSetAlgebra = Self()
|
||||||
defaultsSetAlgebra.store = store.intersection(other.store)
|
defaultsSetAlgebra.store = store.intersection(other.store)
|
||||||
return defaultsSetAlgebra
|
return defaultsSetAlgebra
|
||||||
}
|
}
|
||||||
|
|
||||||
func symmetricDifference(_ other: DefaultsSetAlgebra) -> DefaultsSetAlgebra {
|
func symmetricDifference(_ other: Self) -> Self {
|
||||||
var defaultedSetAlgebra = DefaultsSetAlgebra()
|
var defaultedSetAlgebra = Self()
|
||||||
defaultedSetAlgebra.store = store.symmetricDifference(other.store)
|
defaultedSetAlgebra.store = store.symmetricDifference(other.store)
|
||||||
return defaultedSetAlgebra
|
return defaultedSetAlgebra
|
||||||
}
|
}
|
||||||
|
|
|
@ -731,4 +731,64 @@ final class DefaultsTests: XCTestCase {
|
||||||
func testKeyHashable() {
|
func testKeyHashable() {
|
||||||
_ = Set([Defaults.Key<Bool>("hashableKeyTest", default: false)])
|
_ = Set([Defaults.Key<Bool>("hashableKeyTest", default: false)])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testUpdates() async {
|
||||||
|
let key = Defaults.Key<Bool>("updatesKey", default: false)
|
||||||
|
|
||||||
|
async let waiter = Defaults.updates(key, initial: false).first { $0 }
|
||||||
|
|
||||||
|
try? await Task.sleep(seconds: 0.1)
|
||||||
|
|
||||||
|
Defaults[key] = true
|
||||||
|
|
||||||
|
guard let result = await waiter else {
|
||||||
|
XCTFail()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertTrue(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUpdatesMultipleKeys() async {
|
||||||
|
let key1 = Defaults.Key<Bool>("updatesMultipleKey1", default: false)
|
||||||
|
let key2 = Defaults.Key<Bool>("updatesMultipleKey2", default: false)
|
||||||
|
let counter = Counter()
|
||||||
|
|
||||||
|
async let waiter: Void = {
|
||||||
|
for await _ in Defaults.updates([key1, key2], initial: false) {
|
||||||
|
await counter.increment()
|
||||||
|
|
||||||
|
if await counter.count == 2 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
try? await Task.sleep(seconds: 0.1)
|
||||||
|
|
||||||
|
Defaults[key1] = true
|
||||||
|
Defaults[key2] = true
|
||||||
|
|
||||||
|
await waiter
|
||||||
|
|
||||||
|
let count = await counter.count
|
||||||
|
XCTAssertEqual(count, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actor Counter {
|
||||||
|
private var _count = 0
|
||||||
|
|
||||||
|
var count: Int { _count }
|
||||||
|
|
||||||
|
func increment() {
|
||||||
|
_count += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove when testing on macOS 13.
|
||||||
|
extension Task<Never, Never> {
|
||||||
|
static func sleep(seconds: TimeInterval) async throws {
|
||||||
|
try await sleep(nanoseconds: UInt64(seconds * Double(NSEC_PER_SEC)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
170
readme.md
170
readme.md
|
@ -6,17 +6,14 @@ Store key-value pairs persistently across launches of your app.
|
||||||
|
|
||||||
It uses `UserDefaults` underneath but exposes a type-safe facade with lots of nice conveniences.
|
It uses `UserDefaults` underneath but exposes a type-safe facade with lots of nice conveniences.
|
||||||
|
|
||||||
It's used in production by apps like [Gifski](https://github.com/sindresorhus/Gifski), [Dato](https://sindresorhus.com/dato), [Lungo](https://sindresorhus.com/lungo), [Battery Indicator](https://sindresorhus.com/battery-indicator), and [HEIC Converter](https://sindresorhus.com/heic-converter).
|
It's used in production by [all my apps](https://sindresorhus.com/apps) (1 million+ users).
|
||||||
|
|
||||||
For a real-world example, see the [Plash app](https://github.com/sindresorhus/Plash/blob/533dbc888d8ba3bd9581e60320af282a22c53f85/Plash/Constants.swift#L9-L18).
|
|
||||||
|
|
||||||
## Highlights
|
## Highlights
|
||||||
|
|
||||||
- **Strongly typed:** You declare the type and default value upfront.
|
- **Strongly typed:** You declare the type and default value upfront.
|
||||||
|
- **SwiftUI:** Property wrapper that updates the view when the `UserDefaults` value changes.
|
||||||
- **Codable support:** You can store any [Codable](https://developer.apple.com/documentation/swift/codable) value, like an enum.
|
- **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.
|
- **NSSecureCoding support:** You can store any [NSSecureCoding](https://developer.apple.com/documentation/foundation/nssecurecoding) value.
|
||||||
- **SwiftUI:** Property wrapper that updates the view when the `UserDefaults` value changes.
|
|
||||||
- **Publishers:** Combine publishers built-in.
|
|
||||||
- **Observation:** Observe changes to keys.
|
- **Observation:** Observe changes to keys.
|
||||||
- **Debuggable:** The data is stored as JSON-serialized values.
|
- **Debuggable:** The data is stored as JSON-serialized values.
|
||||||
- **Customizable:** You can serialize and deserialize your own type in your own way.
|
- **Customizable:** You can serialize and deserialize your own type in your own way.
|
||||||
|
@ -26,7 +23,7 @@ For a real-world example, see the [Plash app](https://github.com/sindresorhus/Pl
|
||||||
- You define strongly-typed identifiers in a single place and can use them everywhere.
|
- You define strongly-typed identifiers in a single place and can use them everywhere.
|
||||||
- You also define the default values in a single place instead of having to remember what default value you used in other places.
|
- You also define the default values in a single place instead of having to remember what default value you used in other places.
|
||||||
- You can use it outside of SwiftUI.
|
- You can use it outside of SwiftUI.
|
||||||
- Comes with Combine publisher.
|
- You can observe value updates.
|
||||||
- Supports many more types, even `Codable`.
|
- Supports many more types, even `Codable`.
|
||||||
- Easy to add support for your own custom types.
|
- Easy to add support for your own custom types.
|
||||||
- Comes with a convenience SwiftUI `Toggle` component.
|
- Comes with a convenience SwiftUI `Toggle` component.
|
||||||
|
@ -38,15 +35,11 @@ For a real-world example, see the [Plash app](https://github.com/sindresorhus/Pl
|
||||||
- tvOS 13+
|
- tvOS 13+
|
||||||
- watchOS 6+
|
- watchOS 6+
|
||||||
|
|
||||||
## Migration Guides
|
|
||||||
|
|
||||||
#### [From v4 to v5](./migration.md)
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
Add `https://github.com/sindresorhus/Defaults` in the [“Swift Package Manager” tab in Xcode](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app).
|
Add `https://github.com/sindresorhus/Defaults` in the [“Swift Package Manager” tab in Xcode](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app).
|
||||||
|
|
||||||
**Requires Xcode 14 or later**
|
**Requires Xcode 14.1 or later**
|
||||||
|
|
||||||
## Support types
|
## Support types
|
||||||
|
|
||||||
|
@ -247,71 +240,17 @@ extension Defaults.Keys {
|
||||||
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
|
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
let observer = Defaults.observe(.isUnicornMode) { change in
|
// …
|
||||||
// Initial event
|
|
||||||
print(change.oldValue)
|
|
||||||
//=> false
|
|
||||||
print(change.newValue)
|
|
||||||
//=> false
|
|
||||||
|
|
||||||
// First actual event
|
Task {
|
||||||
print(change.oldValue)
|
for await value in Defaults.updates(.isUnicornMode) {
|
||||||
//=> false
|
print("Value:", value)
|
||||||
print(change.newValue)
|
}
|
||||||
//=> true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Defaults[.isUnicornMode] = true
|
|
||||||
```
|
```
|
||||||
|
|
||||||
In contrast to the native `UserDefaults` key observation, here you receive a strongly-typed change object.
|
In contrast to the native `UserDefaults` key observation, here you receive a strongly-typed change object.
|
||||||
|
|
||||||
There is also an observation API using the [Combine](https://developer.apple.com/documentation/combine) framework, exposing a [Publisher](https://developer.apple.com/documentation/combine/publisher) for key changes:
|
|
||||||
|
|
||||||
```swift
|
|
||||||
let publisher = Defaults.publisher(.isUnicornMode)
|
|
||||||
|
|
||||||
let cancellable = publisher.sink { change in
|
|
||||||
// Initial event
|
|
||||||
print(change.oldValue)
|
|
||||||
//=> false
|
|
||||||
print(change.newValue)
|
|
||||||
//=> false
|
|
||||||
|
|
||||||
// First actual event
|
|
||||||
print(change.oldValue)
|
|
||||||
//=> false
|
|
||||||
print(change.newValue)
|
|
||||||
//=> true
|
|
||||||
}
|
|
||||||
|
|
||||||
Defaults[.isUnicornMode] = true
|
|
||||||
|
|
||||||
// To invalidate the observation.
|
|
||||||
cancellable.cancel()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Invalidate observations automatically
|
|
||||||
|
|
||||||
```swift
|
|
||||||
extension Defaults.Keys {
|
|
||||||
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
final class Foo {
|
|
||||||
init() {
|
|
||||||
Defaults.observe(.isUnicornMode) { change in
|
|
||||||
print(change.oldValue)
|
|
||||||
print(change.newValue)
|
|
||||||
}.tieToLifetime(of: self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Defaults[.isUnicornMode] = true
|
|
||||||
```
|
|
||||||
|
|
||||||
The observation will be valid until `self` is deinitialized.
|
|
||||||
|
|
||||||
### Reset keys to their default values
|
### Reset keys to their default values
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
|
@ -480,49 +419,6 @@ Reset the given keys back to their default values.
|
||||||
|
|
||||||
You can also specify string keys, which can be useful if you need to store some keys in a collection, as it's not possible to store `Defaults.Key` in a collection because it's generic.
|
You can also specify string keys, which can be useful if you need to store some keys in a collection, as it's not possible to store `Defaults.Key` in a collection because it's generic.
|
||||||
|
|
||||||
#### `Defaults.observe`
|
|
||||||
|
|
||||||
```swift
|
|
||||||
Defaults.observe<T: Codable>(
|
|
||||||
_ key: Defaults.Key<T>,
|
|
||||||
options: ObservationOptions = [.initial],
|
|
||||||
handler: @escaping (KeyChange<T>) -> Void
|
|
||||||
) -> Defaults.Observation
|
|
||||||
```
|
|
||||||
|
|
||||||
Type: `func`
|
|
||||||
|
|
||||||
Observe changes to a key or an optional key.
|
|
||||||
|
|
||||||
By default, it will also trigger an initial event on creation. This can be useful for setting default values on controls. You can override this behavior with the `options` argument.
|
|
||||||
|
|
||||||
#### `Defaults.observe(keys: keys..., options:)`
|
|
||||||
|
|
||||||
Type: `func`
|
|
||||||
|
|
||||||
Observe multiple keys of any type, but without any information about the changes.
|
|
||||||
|
|
||||||
Options are the same as in `.observe(…)` for a single key.
|
|
||||||
|
|
||||||
#### `Defaults.publisher(_ key:, options:)`
|
|
||||||
|
|
||||||
```swift
|
|
||||||
Defaults.publisher<T: Codable>(
|
|
||||||
_ key: Defaults.Key<T>,
|
|
||||||
options: ObservationOptions = [.initial]
|
|
||||||
) -> AnyPublisher<KeyChange<T>, Never>
|
|
||||||
```
|
|
||||||
|
|
||||||
Type: `func`
|
|
||||||
|
|
||||||
Observation API using [Publisher](https://developer.apple.com/documentation/combine/publisher) from the [Combine](https://developer.apple.com/documentation/combine) framework.
|
|
||||||
|
|
||||||
#### `Defaults.publisher(keys: keys…, options:)`
|
|
||||||
|
|
||||||
Type: `func`
|
|
||||||
|
|
||||||
[Combine](https://developer.apple.com/documentation/combine) observation API for multiple key observation, but without specific information about changes.
|
|
||||||
|
|
||||||
#### `Defaults.removeAll`
|
#### `Defaults.removeAll`
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
|
@ -533,47 +429,6 @@ Type: `func`
|
||||||
|
|
||||||
Remove all entries from the given `UserDefaults` suite.
|
Remove all entries from the given `UserDefaults` suite.
|
||||||
|
|
||||||
### `Defaults.Observation`
|
|
||||||
|
|
||||||
Type: `protocol`
|
|
||||||
|
|
||||||
Represents an observation of a defaults key.
|
|
||||||
|
|
||||||
#### `Defaults.Observation#invalidate`
|
|
||||||
|
|
||||||
```swift
|
|
||||||
Defaults.Observation#invalidate()
|
|
||||||
```
|
|
||||||
|
|
||||||
Type: `func`
|
|
||||||
|
|
||||||
Invalidate the observation.
|
|
||||||
|
|
||||||
#### `Defaults.Observation#tieToLifetime`
|
|
||||||
|
|
||||||
```swift
|
|
||||||
@discardableResult
|
|
||||||
Defaults.Observation#tieToLifetime(of weaklyHeldObject: AnyObject) -> Self
|
|
||||||
```
|
|
||||||
|
|
||||||
Type: `func`
|
|
||||||
|
|
||||||
Keep the observation alive for as long as, and no longer than, another object exists.
|
|
||||||
|
|
||||||
When `weaklyHeldObject` is deinitialized, the observation is invalidated automatically.
|
|
||||||
|
|
||||||
#### `Defaults.Observation.removeLifetimeTie`
|
|
||||||
|
|
||||||
```swift
|
|
||||||
Defaults.Observation#removeLifetimeTie()
|
|
||||||
```
|
|
||||||
|
|
||||||
Type: `func`
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
#### `Defaults.withoutPropagation(_ closure:)`
|
#### `Defaults.withoutPropagation(_ closure:)`
|
||||||
|
|
||||||
Execute the closure without triggering change events.
|
Execute the closure without triggering change events.
|
||||||
|
@ -948,15 +803,16 @@ It's inspired by that package and other solutions. The main difference is that t
|
||||||
## Maintainers
|
## Maintainers
|
||||||
|
|
||||||
- [Sindre Sorhus](https://github.com/sindresorhus)
|
- [Sindre Sorhus](https://github.com/sindresorhus)
|
||||||
- [Kacper Rączy](https://github.com/fredyshox)
|
|
||||||
- [@hank121314](https://github.com/hank121314)
|
- [@hank121314](https://github.com/hank121314)
|
||||||
|
|
||||||
|
**Former**
|
||||||
|
|
||||||
|
- [Kacper Rączy](https://github.com/fredyshox)
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [Preferences](https://github.com/sindresorhus/Preferences) - Add a preferences window to your macOS app
|
|
||||||
- [KeyboardShortcuts](https://github.com/sindresorhus/KeyboardShortcuts) - Add user-customizable global keyboard shortcuts to your macOS app
|
- [KeyboardShortcuts](https://github.com/sindresorhus/KeyboardShortcuts) - Add user-customizable global keyboard shortcuts to your macOS app
|
||||||
- [LaunchAtLogin](https://github.com/sindresorhus/LaunchAtLogin) - Add "Launch at Login" functionality to your macOS app
|
- [LaunchAtLogin](https://github.com/sindresorhus/LaunchAtLogin) - Add "Launch at Login" functionality to your macOS app
|
||||||
- [Regex](https://github.com/sindresorhus/Regex) - Swifty regular expressions
|
|
||||||
- [DockProgress](https://github.com/sindresorhus/DockProgress) - Show progress in your app's Dock icon
|
- [DockProgress](https://github.com/sindresorhus/DockProgress) - Show progress in your app's Dock icon
|
||||||
- [Gifski](https://github.com/sindresorhus/Gifski) - Convert videos to high-quality GIFs on your Mac
|
- [Gifski](https://github.com/sindresorhus/Gifski) - Convert videos to high-quality GIFs on your Mac
|
||||||
- [More…](https://github.com/search?q=user%3Asindresorhus+language%3Aswift)
|
- [More…](https://github.com/search?q=user%3Asindresorhus+language%3Aswift)
|
||||||
|
|
Loading…
Reference in New Issue