From 63d93f97ad545c8bceb125a8a36175ea705f7cf5 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Tue, 18 May 2021 19:56:55 +0700 Subject: [PATCH] Add `Defaults.Toggle` (#69) --- Sources/Defaults/SwiftUI.swift | 107 ++++++++++++++++++++++++++++++++- readme.md | 39 +++++++++++- 2 files changed, 143 insertions(+), 3 deletions(-) diff --git a/Sources/Defaults/SwiftUI.swift b/Sources/Defaults/SwiftUI.swift index 6cc7405..da98d93 100644 --- a/Sources/Defaults/SwiftUI.swift +++ b/Sources/Defaults/SwiftUI.swift @@ -5,10 +5,11 @@ import Combine @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) extension Defaults { final class Observable: ObservableObject { - let objectWillChange = ObservableObjectPublisher() private var observation: DefaultsObservation? private let key: Defaults.Key + let objectWillChange = ObservableObjectPublisher() + var value: Value { get { Defaults[key] } set { @@ -44,6 +45,8 @@ public struct Default: DynamicProperty { public typealias Publisher = AnyPublisher, Never> private let key: Defaults.Key + + // Intentionally using `@ObservedObjected` over `@StateObject` so that the key can be dynamicaly changed. @ObservedObject private var observable: Defaults.Observable /** @@ -61,7 +64,10 @@ public struct Default: DynamicProperty { var body: some View { Text("Has Unicorn: \(hasUnicorn)") - Toggle("Toggle Unicorn", isOn: $hasUnicorn) + Toggle("Toggle", isOn: $hasUnicorn) + Button("Reset") { + _hasUnicorn.reset() + } } } ``` @@ -110,4 +116,101 @@ public struct Default: DynamicProperty { key.reset() } } + +@available(macOS 11, iOS 14, tvOS 14, watchOS 7, *) +extension Defaults { + /** + Creates a SwiftUI `Toggle` view that is connected to a `Defaults` key with a `Bool` value. + + The toggle works exactly like the SwiftUI `Toggle`. + + ``` + extension Defaults.Keys { + static let showAllDayEvents = Key("showAllDayEvents", default: false) + } + + struct ShowAllDayEventsSetting: View { + var body: some View { + Defaults.Toggle("Show All-Day Events", key: .showAllDayEvents) + } + } + ``` + + You can also listen to changes: + + ``` + struct ShowAllDayEventsSetting: View { + var body: some View { + Defaults.Toggle("Show All-Day Events", key: .showAllDayEvents) + // Note that this has to be directly attached to `Defaults.Toggle`. It's not `View#onChange()`. + .onChange { + print("Value", $0) + } + } + } + ``` + */ + public struct Toggle: View where Label: View, Key: Defaults.Key { + @ViewStorage private var onChange: ((Bool) -> Void)? + + private let label: () -> Label + + // Intentionally using `@ObservedObjected` over `@StateObject` so that the key can be dynamicaly changed. + @ObservedObject private var observable: Defaults.Observable + + public init(key: Key, @ViewBuilder label: @escaping () -> Label) { + self.label = label + self.observable = Defaults.Observable(key) + } + + public var body: some View { + SwiftUI.Toggle(isOn: $observable.value, label: label) + .onChange(of: observable.value) { + onChange?($0) + } + } + } +} + +@available(macOS 11, iOS 14, tvOS 14, watchOS 7, *) +extension Defaults.Toggle where Label == Text { + public init(_ title: S, key: Defaults.Key) where S: StringProtocol { + self.label = { Text(title) } + self.observable = Defaults.Observable(key) + } +} + +@available(macOS 11, iOS 14, tvOS 14, watchOS 7, *) +extension Defaults.Toggle { + /// Do something when the value changes to a different value. + public func onChange(_ action: @escaping (Bool) -> Void) -> Self { + onChange = action + return self + } +} + +@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +@propertyWrapper +private struct ViewStorage: DynamicProperty { + private final class ValueBox { + var value: Value + + init(_ value: Value) { + self.value = value + } + } + + @State private var valueBox: ValueBox + + var wrappedValue: Value { + get { valueBox.value } + nonmutating set { + valueBox.value = newValue + } + } + + init(wrappedValue value: @autoclosure @escaping () -> Value) { + self._valueBox = .init(wrappedValue: ValueBox(value())) + } +} #endif diff --git a/readme.md b/readme.md index 9bb1039..f878f2c 100644 --- a/readme.md +++ b/readme.md @@ -153,6 +153,8 @@ Defaults[isUnicorn] ### SwiftUI support +#### `@Default` + 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 @@ -165,7 +167,10 @@ struct ContentView: View { var body: some View { Text("Has Unicorn: \(hasUnicorn)") - Toggle("Toggle Unicorn", isOn: $hasUnicorn) + Toggle("Toggle", isOn: $hasUnicorn) + Button("Reset") { + _hasUnicorn.reset() + } } } ``` @@ -174,6 +179,38 @@ Note that it's `@Default`, not `@Defaults`. You cannot use `@Default` in an `ObservableObject`. It's meant to be used in a `View`. +#### `Toggle` + +There's also a `SwiftUI.Toggle` wrapper that makes it easier to create a toggle based on a `Defaults` key with a `Bool` value. + +```swift +extension Defaults.Keys { + static let showAllDayEvents = Key("showAllDayEvents", default: false) +} + +struct ShowAllDayEventsSetting: View { + var body: some View { + Defaults.Toggle("Show All-Day Events", key: .showAllDayEvents) + } +} +``` + +You can also listen to changes: + +```swift +struct ShowAllDayEventsSetting: View { + var body: some View { + Defaults.Toggle("Show All-Day Events", key: .showAllDayEvents) + // Note that this has to be directly attached to `Defaults.Toggle`. It's not `View#onChange()`. + .onChange { + print("Value", $0) + } + } +} +``` + +*Requires at least macOS 11, iOS 14, tvOS 14, watchOS 7.* + ### Observe changes to a key ```swift