Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions Sources/Defaults/SwiftUI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,20 @@ public struct Default<Value: Defaults.Serializable>: @preconcurrency DynamicProp
}

public var wrappedValue: Value {
get { observable.value }
get {
syncObservableKeyIfNeeded()
return observable.value
}
nonmutating set {
syncObservableKeyIfNeeded()
observable.value = newValue
}
}

public var projectedValue: Binding<Value> { $observable.value }
public var projectedValue: Binding<Value> {
syncObservableKeyIfNeeded()
return $observable.value
}

/**
The default value of the key.
Expand All @@ -134,10 +141,18 @@ public struct Default<Value: Defaults.Serializable>: @preconcurrency DynamicProp

@_documentation(visibility: private)
public mutating func update() {
observable.key = key
_observable.update()
}

private func syncObservableKeyIfNeeded() {
guard observable.key != key else {
return
}

// Avoid touching `@StateObject` in `update()`, which triggers Xcode 16 warnings.
observable.key = key
}

/**
Reset the key back to its default value.

Expand Down
75 changes: 75 additions & 0 deletions Tests/DefaultsTests/DefaultsSwiftUITests.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import Foundation
import SwiftUI
#if canImport(AppKit)
import AppKit
#elseif canImport(UIKit)
import UIKit
#endif
import Testing
import Defaults
import Observation

private let suite_ = createSuite()

Expand All @@ -16,6 +22,8 @@ extension Defaults.Keys {
fileprivate static let user = Key<User>("swiftui_user", default: User(username: "Hank", password: "123456"), suite: suite_)
fileprivate static let setInt = Key<Set<Int>>("swiftui_setInt", default: Set(1...3), suite: suite_)
fileprivate static let color = Key<Color>("swiftui_color", default: .black, suite: suite_)
fileprivate static let primarySwitchValue = Key<Bool>("swiftui_primarySwitchValue", default: false, suite: suite_)
fileprivate static let secondarySwitchValue = Key<Bool>("swiftui_secondarySwitchValue", default: false, suite: suite_)
}

struct ContentView: View {
Expand All @@ -31,6 +39,39 @@ struct ContentView: View {
}
}

@Observable
final class DefaultKeySwitchingState {
var useSecondaryKey = false
}

struct KeySwitchingHostView: View {
@Bindable var state: DefaultKeySwitchingState

var body: some View {
KeySwitchingDefaultView(
key: state.useSecondaryKey ? .secondarySwitchValue : .primarySwitchValue,
marker: state.useSecondaryKey
)
}
}

struct KeySwitchingDefaultView: View {
@Default private var value: Bool
private let marker: Bool

init(key: Defaults.Key<Bool>, marker: Bool) {
self.marker = marker
_value = Default(key)
}

var body: some View {
Color.clear
.task(id: marker) {
value = marker
}
}
}

@Suite(.serialized)
final class DefaultsSwiftUITests {
init() {
Expand Down Expand Up @@ -63,4 +104,38 @@ final class DefaultsSwiftUITests {
#expect(XColor(view.color) != XColor(Color.black))
#expect(XColor(view.color) == XColor(Color(.sRGB, red: 100, green: 100, blue: 100, opacity: 1)))
}

@MainActor
@Test
func testSwiftUIDefaultUpdatesKeyOnReinit() async throws {
Defaults[.primarySwitchValue] = false
Defaults[.secondarySwitchValue] = false

let state = DefaultKeySwitchingState()

#if os(macOS)
let hostingController = NSHostingController(rootView: KeySwitchingHostView(state: state))
let window = NSWindow(contentViewController: hostingController)
window.makeKeyAndOrderFront(nil)
#else
let hostingController = UIHostingController(rootView: KeySwitchingHostView(state: state))
let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = hostingController
window.makeKeyAndVisible()
#endif

_ = hostingController.view
_ = window

try await Task.sleep(for: .milliseconds(100))

#expect(!Defaults[.primarySwitchValue])
#expect(!Defaults[.secondarySwitchValue])

state.useSecondaryKey = true
try await Task.sleep(for: .milliseconds(100))

#expect(!Defaults[.primarySwitchValue])
#expect(Defaults[.secondarySwitchValue])
}
}
Loading