Skip to content

Commit 7cbc103

Browse files
committed
Fix "Accessing StateObject's object without being installed on a View" warning
Fixes #190
1 parent 752bcb9 commit 7cbc103

File tree

2 files changed

+93
-3
lines changed

2 files changed

+93
-3
lines changed

Sources/Defaults/SwiftUI.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,20 @@ public struct Default<Value: Defaults.Serializable>: @preconcurrency DynamicProp
114114
}
115115

116116
public var wrappedValue: Value {
117-
get { observable.value }
117+
get {
118+
syncObservableKeyIfNeeded()
119+
return observable.value
120+
}
118121
nonmutating set {
122+
syncObservableKeyIfNeeded()
119123
observable.value = newValue
120124
}
121125
}
122126

123-
public var projectedValue: Binding<Value> { $observable.value }
127+
public var projectedValue: Binding<Value> {
128+
syncObservableKeyIfNeeded()
129+
return $observable.value
130+
}
124131

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

135142
@_documentation(visibility: private)
136143
public mutating func update() {
137-
observable.key = key
138144
_observable.update()
139145
}
140146

147+
private func syncObservableKeyIfNeeded() {
148+
guard observable.key != key else {
149+
return
150+
}
151+
152+
// Avoid touching `@StateObject` in `update()`, which triggers Xcode 16 warnings.
153+
observable.key = key
154+
}
155+
141156
/**
142157
Reset the key back to its default value.
143158

Tests/DefaultsTests/DefaultsSwiftUITests.swift

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import Foundation
22
import SwiftUI
3+
#if canImport(AppKit)
4+
import AppKit
5+
#elseif canImport(UIKit)
6+
import UIKit
7+
#endif
38
import Testing
49
import Defaults
10+
import Observation
511

612
private let suite_ = createSuite()
713

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

2129
struct ContentView: View {
@@ -31,6 +39,39 @@ struct ContentView: View {
3139
}
3240
}
3341

42+
@Observable
43+
final class DefaultKeySwitchingState {
44+
var useSecondaryKey = false
45+
}
46+
47+
struct KeySwitchingHostView: View {
48+
@Bindable var state: DefaultKeySwitchingState
49+
50+
var body: some View {
51+
KeySwitchingDefaultView(
52+
key: state.useSecondaryKey ? .secondarySwitchValue : .primarySwitchValue,
53+
marker: state.useSecondaryKey
54+
)
55+
}
56+
}
57+
58+
struct KeySwitchingDefaultView: View {
59+
@Default private var value: Bool
60+
private let marker: Bool
61+
62+
init(key: Defaults.Key<Bool>, marker: Bool) {
63+
self.marker = marker
64+
_value = Default(key)
65+
}
66+
67+
var body: some View {
68+
Color.clear
69+
.task(id: marker) {
70+
value = marker
71+
}
72+
}
73+
}
74+
3475
@Suite(.serialized)
3576
final class DefaultsSwiftUITests {
3677
init() {
@@ -63,4 +104,38 @@ final class DefaultsSwiftUITests {
63104
#expect(XColor(view.color) != XColor(Color.black))
64105
#expect(XColor(view.color) == XColor(Color(.sRGB, red: 100, green: 100, blue: 100, opacity: 1)))
65106
}
107+
108+
@MainActor
109+
@Test
110+
func testSwiftUIDefaultUpdatesKeyOnReinit() async throws {
111+
Defaults[.primarySwitchValue] = false
112+
Defaults[.secondarySwitchValue] = false
113+
114+
let state = DefaultKeySwitchingState()
115+
116+
#if os(macOS)
117+
let hostingController = NSHostingController(rootView: KeySwitchingHostView(state: state))
118+
let window = NSWindow(contentViewController: hostingController)
119+
window.makeKeyAndOrderFront(nil)
120+
#else
121+
let hostingController = UIHostingController(rootView: KeySwitchingHostView(state: state))
122+
let window = UIWindow(frame: UIScreen.main.bounds)
123+
window.rootViewController = hostingController
124+
window.makeKeyAndVisible()
125+
#endif
126+
127+
_ = hostingController.view
128+
_ = window
129+
130+
try await Task.sleep(for: .milliseconds(100))
131+
132+
#expect(!Defaults[.primarySwitchValue])
133+
#expect(!Defaults[.secondarySwitchValue])
134+
135+
state.useSecondaryKey = true
136+
try await Task.sleep(for: .milliseconds(100))
137+
138+
#expect(!Defaults[.primarySwitchValue])
139+
#expect(Defaults[.secondarySwitchValue])
140+
}
66141
}

0 commit comments

Comments
 (0)