Skip to content

Commit

Permalink
Add Defaults.Toggle (#69)
Browse files Browse the repository at this point in the history
  • Loading branch information
sindresorhus authored May 18, 2021
1 parent 64351c2 commit 63d93f9
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 3 deletions.
107 changes: 105 additions & 2 deletions Sources/Defaults/SwiftUI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import Combine
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension Defaults {
final class Observable<Value: Serializable>: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
private var observation: DefaultsObservation?
private let key: Defaults.Key<Value>

let objectWillChange = ObservableObjectPublisher()

var value: Value {
get { Defaults[key] }
set {
Expand Down Expand Up @@ -44,6 +45,8 @@ public struct Default<Value: Defaults.Serializable>: DynamicProperty {
public typealias Publisher = AnyPublisher<Defaults.KeyChange<Value>, Never>

private let key: Defaults.Key<Value>

// Intentionally using `@ObservedObjected` over `@StateObject` so that the key can be dynamicaly changed.
@ObservedObject private var observable: Defaults.Observable<Value>

/**
Expand All @@ -61,7 +64,10 @@ public struct Default<Value: Defaults.Serializable>: DynamicProperty {
var body: some View {
Text("Has Unicorn: \(hasUnicorn)")
Toggle("Toggle Unicorn", isOn: $hasUnicorn)
Toggle("Toggle", isOn: $hasUnicorn)
Button("Reset") {
_hasUnicorn.reset()
}
}
}
```
Expand Down Expand Up @@ -110,4 +116,101 @@ public struct Default<Value: Defaults.Serializable>: 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<Bool>("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<Label, Key>: View where Label: View, Key: Defaults.Key<Bool> {
@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<Bool>

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<S>(_ title: S, key: Defaults.Key<Bool>) 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<Value>: 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
39 changes: 38 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
}
}
}
```
Expand All @@ -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<Bool>("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
Expand Down

0 comments on commit 63d93f9

Please sign in to comment.