Skip to content

Commit 7f254ea

Browse files
gfreezyclaude
andcommitted
Fix external change handler to only notify on actual value changes
When UserDefaults.didChangeNotification fires, the handler now reads the current value and compares it with the cached value using shouldSetValue() before triggering the mutation notification. This prevents unnecessary SwiftUI view re-evaluations when unrelated properties change in UserDefaults. Also add test to verify unrelated properties are not notified on external changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c8b021c commit 7f254ea

2 files changed

Lines changed: 37 additions & 2 deletions

File tree

Sources/ObservableDefaultsMacros/Macros/ObservableDefaultsMacro.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,21 @@ extension ObservableDefaultsMacros: MemberMacro {
182182
return """
183183
\(caseIndent)case prefix + "\(meta.userDefaultsKey)":
184184
\(caseIndent) MainActor.assumeIsolated {
185-
\(caseIndent) host._$observationRegistrar.withMutation(of: host, keyPath: \\.\(meta.propertyID)) {}
185+
\(caseIndent) let newValue = UserDefaultsWrapper.getValue(fullKey, host._\(meta.propertyID), host._userDefaults)
186+
\(caseIndent) if host.shouldSetValue(newValue, host._\(meta.propertyID)) {
187+
\(caseIndent) host._\(meta.propertyID) = newValue
188+
\(caseIndent) host._$observationRegistrar.withMutation(of: host, keyPath: \\.\(meta.propertyID)) {}
189+
\(caseIndent) }
186190
\(caseIndent) }
187191
"""
188192
} else {
189193
return """
190-
\(caseIndent)case prefix + "\(meta.userDefaultsKey)": host._$observationRegistrar.withMutation(of: host, keyPath: \\.\(meta.propertyID)) {}
194+
\(caseIndent)case prefix + "\(meta.userDefaultsKey)":
195+
\(caseIndent) let newValue = UserDefaultsWrapper.getValue(fullKey, host._\(meta.propertyID), host._userDefaults)
196+
\(caseIndent) if host.shouldSetValue(newValue, host._\(meta.propertyID)) {
197+
\(caseIndent) host._\(meta.propertyID) = newValue
198+
\(caseIndent) host._$observationRegistrar.withMutation(of: host, keyPath: \\.\(meta.propertyID)) {}
199+
\(caseIndent) }
191200
"""
192201
}
193202
// swiftformat:enable all
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
// ExternalChangeEqualityTests.swift
3+
// ObservableDefaults
4+
//
5+
// Tests that the external UserDefaults change notification handler
6+
// only triggers observation mutations when values actually change.
7+
//
8+
9+
import Foundation
10+
import ObservableDefaults
11+
import Observation
12+
import Testing
13+
14+
@Suite("External Change Equality Check")
15+
struct ExternalChangeEqualityTests {
16+
17+
@Test("Unrelated property not notified when another property changes externally")
18+
func unrelatedPropertyNotNotified() {
19+
let userDefaults = UserDefaults.getTestInstance(suiteName: #function)
20+
let model = MockModel(userDefaults: userDefaults)
21+
22+
// Track age - expect NO mutation when only name changes
23+
tracking(model, \.age, .userDefaults, false)
24+
userDefaults.set("NewName", forKey: "name")
25+
}
26+
}

0 commit comments

Comments
 (0)