Skip to content

Commit af60ed2

Browse files
rcarvermbrandonwstephencelis
authored
Add shouldNotifyObservers to ObservableState (#3751)
* add shouldNotifyObservers support to ObservableState * update tests * move shouldNotifyObservers logic into ObservationState * fix test for macro change * Fix test * wip * update xcode on ci * wip * wip * wip * wip --------- Co-authored-by: Brandon Williams <[email protected]> Co-authored-by: Stephen Celis <[email protected]>
1 parent 27db3df commit af60ed2

File tree

8 files changed

+272
-57
lines changed

8 files changed

+272
-57
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
matrix:
2222
command: [test, '']
2323
platform: [IOS, MACOS]
24-
xcode: ['16.2']
24+
xcode: ['16.4']
2525
steps:
2626
- uses: actions/checkout@v4
2727
- name: Select Xcode ${{ matrix.xcode }}
@@ -49,6 +49,9 @@ jobs:
4949
examples:
5050
name: Examples
5151
runs-on: macos-15
52+
strategy:
53+
matrix:
54+
xcode: ['16.4']
5255
steps:
5356
- uses: actions/checkout@v4
5457
- name: Cache derived data
@@ -60,7 +63,7 @@ jobs:
6063
restore-keys: |
6164
deriveddata-examples-
6265
- name: Select Xcode 16
63-
run: sudo xcode-select -s /Applications/Xcode_16.2.app
66+
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
6467
- name: Update xcbeautify
6568
run: brew update && brew upgrade xcbeautify
6669
- name: Set IgnoreFileSystemDeviceInodeChanges flag

Sources/ComposableArchitecture/Macros.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public macro ReducerCaseIgnored() =
128128

129129
/// Defines and implements conformance of the Observable protocol.
130130
@attached(extension, conformances: Observable, ObservableState)
131-
@attached(member, names: named(_$id), named(_$observationRegistrar), named(_$willModify))
131+
@attached(member, names: named(_$id), named(_$observationRegistrar), named(_$willModify), named(shouldNotifyObservers))
132132
@attached(memberAttribute)
133133
public macro ObservableState() =
134134
#externalMacro(module: "ComposableArchitectureMacros", type: "ObservableStateMacro")

Sources/ComposableArchitecture/Observation/ObservationStateRegistrar.swift

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,10 @@ extension ObservationStateRegistrar: Equatable, Hashable, Codable {
5151
keyPath: KeyPath<Subject, Member>,
5252
_ value: inout Value,
5353
_ newValue: Value,
54-
_ isIdentityEqual: (Value, Value) -> Bool
54+
_ isIdentityEqual: (Value, Value) -> Bool,
55+
_ shouldNotifyObservers: (Value, Value) -> Bool = { _, _ in true }
5556
) {
56-
if isIdentityEqual(value, newValue) {
57+
if isIdentityEqual(value, newValue) || !shouldNotifyObservers(value, newValue) {
5758
value = newValue
5859
} else {
5960
self.registrar.withMutation(of: subject, keyPath: keyPath) {
@@ -101,12 +102,20 @@ extension ObservationStateRegistrar: Equatable, Hashable, Codable {
101102
keyPath: KeyPath<Subject, Member>,
102103
_ member: inout Member,
103104
_ oldValue: Member,
104-
_ isIdentityEqual: (Member, Member) -> Bool
105+
_ isIdentityEqual: (Member, Member) -> Bool,
106+
_ shouldNotifyObservers: (Member, Member) -> Bool = { _, _ in true }
105107
) {
106108
if !isIdentityEqual(oldValue, member) {
107109
let newValue = member
108110
member = oldValue
109-
self.mutate(subject, keyPath: keyPath, &member, newValue, isIdentityEqual)
111+
self.mutate(
112+
subject,
113+
keyPath: keyPath,
114+
&member,
115+
newValue,
116+
isIdentityEqual,
117+
shouldNotifyObservers
118+
)
110119
}
111120
}
112121
}
@@ -130,9 +139,10 @@ extension ObservationStateRegistrar: Equatable, Hashable, Codable {
130139
keyPath: KeyPath<Subject, Member>,
131140
_ value: inout Value,
132141
_ newValue: Value,
133-
_ isIdentityEqual: (Value, Value) -> Bool
142+
_ isIdentityEqual: (Value, Value) -> Bool,
143+
_ shouldNotifyObservers: (Value, Value) -> Bool = { _, _ in true }
134144
) {
135-
if isIdentityEqual(value, newValue) {
145+
if isIdentityEqual(value, newValue) || !shouldNotifyObservers(value, newValue) {
136146
value = newValue
137147
} else {
138148
self.registrar.withMutation(of: subject, keyPath: keyPath) {
@@ -169,12 +179,20 @@ extension ObservationStateRegistrar: Equatable, Hashable, Codable {
169179
keyPath: KeyPath<Subject, Member>,
170180
_ member: inout Member,
171181
_ oldValue: Member,
172-
_ isIdentityEqual: (Member, Member) -> Bool
182+
_ isIdentityEqual: (Member, Member) -> Bool,
183+
_ shouldNotifyObservers: (Member, Member) -> Bool = { _, _ in true }
173184
) {
174185
if !isIdentityEqual(oldValue, member) {
175186
let newValue = member
176187
member = oldValue
177-
self.mutate(subject, keyPath: keyPath, &member, newValue, isIdentityEqual)
188+
self.mutate(
189+
subject,
190+
keyPath: keyPath,
191+
&member,
192+
newValue,
193+
isIdentityEqual,
194+
shouldNotifyObservers
195+
)
178196
}
179197
}
180198
}

Sources/ComposableArchitectureMacros/ObservableStateMacro.swift

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,38 @@ public struct ObservableStateMacro {
7777
"""
7878
}
7979

80+
static func shouldNotifyObserversNonEquatableFunction(_ perceptibleType: TokenSyntax, context: some MacroExpansionContext) -> DeclSyntax {
81+
let memberGeneric = context.makeUniqueName("Member")
82+
return
83+
"""
84+
private nonisolated func shouldNotifyObservers<\(memberGeneric)>(_ lhs: \(memberGeneric), _ rhs: \(memberGeneric)) -> Bool { true }
85+
"""
86+
}
87+
88+
static func shouldNotifyObserversEquatableFunction(_ perceptibleType: TokenSyntax, context: some MacroExpansionContext) -> DeclSyntax {
89+
let memberGeneric = context.makeUniqueName("Member")
90+
return
91+
"""
92+
private nonisolated func shouldNotifyObservers<\(memberGeneric): Equatable>(_ lhs: \(memberGeneric), _ rhs: \(memberGeneric)) -> Bool { lhs != rhs }
93+
"""
94+
}
95+
96+
static func shouldNotifyObserversNonEquatableObjectFunction(_ perceptibleType: TokenSyntax, context: some MacroExpansionContext) -> DeclSyntax {
97+
let memberGeneric = context.makeUniqueName("Member")
98+
return
99+
"""
100+
private nonisolated func shouldNotifyObservers<\(memberGeneric): AnyObject>(_ lhs: \(memberGeneric), _ rhs: \(memberGeneric)) -> Bool { lhs !== rhs }
101+
"""
102+
}
103+
104+
static func shouldNotifyObserversEquatableObjectFunction(_ perceptibleType: TokenSyntax, context: some MacroExpansionContext) -> DeclSyntax {
105+
let memberGeneric = context.makeUniqueName("Member")
106+
return
107+
"""
108+
private nonisolated func shouldNotifyObservers<\(memberGeneric): Equatable & AnyObject>(_ lhs: \(memberGeneric), _ rhs: \(memberGeneric)) -> Bool { lhs != rhs }
109+
"""
110+
}
111+
80112
static var ignoredAttribute: AttributeSyntax {
81113
AttributeSyntax(
82114
leadingTrivia: .space,
@@ -298,6 +330,10 @@ extension ObservableStateMacro: MemberMacro {
298330
)
299331
declaration.addIfNeeded(ObservableStateMacro.idVariable(), to: &declarations)
300332
declaration.addIfNeeded(ObservableStateMacro.willModifyFunction(), to: &declarations)
333+
declaration.addIfNeeded(ObservableStateMacro.shouldNotifyObserversNonEquatableFunction(observableType, context: context), to: &declarations)
334+
declaration.addIfNeeded(ObservableStateMacro.shouldNotifyObserversEquatableFunction(observableType, context: context), to: &declarations)
335+
declaration.addIfNeeded(ObservableStateMacro.shouldNotifyObserversNonEquatableObjectFunction(observableType, context: context), to: &declarations)
336+
declaration.addIfNeeded(ObservableStateMacro.shouldNotifyObserversEquatableObjectFunction(observableType, context: context), to: &declarations)
301337

302338
return declarations
303339
}
@@ -611,7 +647,7 @@ public struct ObservationStateTrackedMacro: AccessorMacro {
611647
let setAccessor: AccessorDeclSyntax =
612648
"""
613649
set {
614-
\(raw: ObservableStateMacro.registrarVariableName).mutate(self, keyPath: \\.\(identifier), &_\(identifier), newValue, _$isIdentityEqual)
650+
\(raw: ObservableStateMacro.registrarVariableName).mutate(self, keyPath: \\.\(identifier), &_\(identifier), newValue, _$isIdentityEqual, shouldNotifyObservers)
615651
}
616652
"""
617653
let modifyAccessor: AccessorDeclSyntax = """

Tests/ComposableArchitectureMacrosTests/MacroBaseTestCase.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
class MacroBaseTestCase: XCTestCase {
99
override func invokeTest() {
1010
MacroTesting.withMacroTesting(
11-
//isRecording: true,
11+
record: .failed,
1212
macros: [
1313
ObservableStateMacro.self,
1414
ObservationStateTrackedMacro.self,

Tests/ComposableArchitectureMacrosTests/ObservableStateMacroTests.swift

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77

88
final class ObservableStateMacroTests: MacroBaseTestCase {
99
override func invokeTest() {
10-
withMacroTesting {
10+
withMacroTesting(
11+
// record: .failed,
12+
) {
1113
super.invokeTest()
1214
}
1315
}
@@ -35,7 +37,7 @@
3537
return _count
3638
}
3739
set {
38-
_$observationRegistrar.mutate(self, keyPath: \.count, &_count, newValue, _$isIdentityEqual)
40+
_$observationRegistrar.mutate(self, keyPath: \.count, &_count, newValue, _$isIdentityEqual, shouldNotifyObservers)
3941
}
4042
_modify {
4143
let oldValue = _$observationRegistrar.willModify(self, keyPath: \.count, &_count)
@@ -55,6 +57,22 @@
5557
public mutating func _$willModify() {
5658
_$observationRegistrar._$willModify()
5759
}
60+
61+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu_>(_ lhs: __macro_local_6MemberfMu_, _ rhs: __macro_local_6MemberfMu_) -> Bool {
62+
true
63+
}
64+
65+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu0_: Equatable>(_ lhs: __macro_local_6MemberfMu0_, _ rhs: __macro_local_6MemberfMu0_) -> Bool {
66+
lhs != rhs
67+
}
68+
69+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu1_: AnyObject>(_ lhs: __macro_local_6MemberfMu1_, _ rhs: __macro_local_6MemberfMu1_) -> Bool {
70+
lhs !== rhs
71+
}
72+
73+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu2_: Equatable & AnyObject>(_ lhs: __macro_local_6MemberfMu2_, _ rhs: __macro_local_6MemberfMu2_) -> Bool {
74+
lhs != rhs
75+
}
5876
}
5977
"""#
6078
}
@@ -81,7 +99,7 @@
8199
return _count
82100
}
83101
set {
84-
_$observationRegistrar.mutate(self, keyPath: \.count, &_count, newValue, _$isIdentityEqual)
102+
_$observationRegistrar.mutate(self, keyPath: \.count, &_count, newValue, _$isIdentityEqual, shouldNotifyObservers)
85103
}
86104
_modify {
87105
let oldValue = _$observationRegistrar.willModify(self, keyPath: \.count, &_count)
@@ -101,6 +119,22 @@
101119
public mutating func _$willModify() {
102120
_$observationRegistrar._$willModify()
103121
}
122+
123+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu_>(_ lhs: __macro_local_6MemberfMu_, _ rhs: __macro_local_6MemberfMu_) -> Bool {
124+
true
125+
}
126+
127+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu0_: Equatable>(_ lhs: __macro_local_6MemberfMu0_, _ rhs: __macro_local_6MemberfMu0_) -> Bool {
128+
lhs != rhs
129+
}
130+
131+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu1_: AnyObject>(_ lhs: __macro_local_6MemberfMu1_, _ rhs: __macro_local_6MemberfMu1_) -> Bool {
132+
lhs !== rhs
133+
}
134+
135+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu2_: Equatable & AnyObject>(_ lhs: __macro_local_6MemberfMu2_, _ rhs: __macro_local_6MemberfMu2_) -> Bool {
136+
lhs != rhs
137+
}
104138
}
105139
"""#
106140
}
@@ -127,7 +161,7 @@
127161
return _count
128162
}
129163
set {
130-
_$observationRegistrar.mutate(self, keyPath: \.count, &_count, newValue, _$isIdentityEqual)
164+
_$observationRegistrar.mutate(self, keyPath: \.count, &_count, newValue, _$isIdentityEqual, shouldNotifyObservers)
131165
}
132166
_modify {
133167
let oldValue = _$observationRegistrar.willModify(self, keyPath: \.count, &_count)
@@ -147,6 +181,22 @@
147181
public mutating func _$willModify() {
148182
_$observationRegistrar._$willModify()
149183
}
184+
185+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu_>(_ lhs: __macro_local_6MemberfMu_, _ rhs: __macro_local_6MemberfMu_) -> Bool {
186+
true
187+
}
188+
189+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu0_: Equatable>(_ lhs: __macro_local_6MemberfMu0_, _ rhs: __macro_local_6MemberfMu0_) -> Bool {
190+
lhs != rhs
191+
}
192+
193+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu1_: AnyObject>(_ lhs: __macro_local_6MemberfMu1_, _ rhs: __macro_local_6MemberfMu1_) -> Bool {
194+
lhs !== rhs
195+
}
196+
197+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu2_: Equatable & AnyObject>(_ lhs: __macro_local_6MemberfMu2_, _ rhs: __macro_local_6MemberfMu2_) -> Bool {
198+
lhs != rhs
199+
}
150200
}
151201
"""#
152202
}
@@ -170,7 +220,7 @@
170220
return _count
171221
}
172222
set {
173-
_$observationRegistrar.mutate(self, keyPath: \.count, &_count, newValue, _$isIdentityEqual)
223+
_$observationRegistrar.mutate(self, keyPath: \.count, &_count, newValue, _$isIdentityEqual, shouldNotifyObservers)
174224
}
175225
_modify {
176226
let oldValue = _$observationRegistrar.willModify(self, keyPath: \.count, &_count)
@@ -190,6 +240,22 @@
190240
public mutating func _$willModify() {
191241
_$observationRegistrar._$willModify()
192242
}
243+
244+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu_>(_ lhs: __macro_local_6MemberfMu_, _ rhs: __macro_local_6MemberfMu_) -> Bool {
245+
true
246+
}
247+
248+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu0_: Equatable>(_ lhs: __macro_local_6MemberfMu0_, _ rhs: __macro_local_6MemberfMu0_) -> Bool {
249+
lhs != rhs
250+
}
251+
252+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu1_: AnyObject>(_ lhs: __macro_local_6MemberfMu1_, _ rhs: __macro_local_6MemberfMu1_) -> Bool {
253+
lhs !== rhs
254+
}
255+
256+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu2_: Equatable & AnyObject>(_ lhs: __macro_local_6MemberfMu2_, _ rhs: __macro_local_6MemberfMu2_) -> Bool {
257+
lhs != rhs
258+
}
193259
}
194260
"""#
195261
}
@@ -218,6 +284,22 @@
218284
public mutating func _$willModify() {
219285
_$observationRegistrar._$willModify()
220286
}
287+
288+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu_>(_ lhs: __macro_local_6MemberfMu_, _ rhs: __macro_local_6MemberfMu_) -> Bool {
289+
true
290+
}
291+
292+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu0_: Equatable>(_ lhs: __macro_local_6MemberfMu0_, _ rhs: __macro_local_6MemberfMu0_) -> Bool {
293+
lhs != rhs
294+
}
295+
296+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu1_: AnyObject>(_ lhs: __macro_local_6MemberfMu1_, _ rhs: __macro_local_6MemberfMu1_) -> Bool {
297+
lhs !== rhs
298+
}
299+
300+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu2_: Equatable & AnyObject>(_ lhs: __macro_local_6MemberfMu2_, _ rhs: __macro_local_6MemberfMu2_) -> Bool {
301+
lhs != rhs
302+
}
221303
}
222304
"""
223305
}
@@ -673,6 +755,22 @@
673755
public mutating func _$willModify() {
674756
_$observationRegistrar._$willModify()
675757
}
758+
759+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu_>(_ lhs: __macro_local_6MemberfMu_, _ rhs: __macro_local_6MemberfMu_) -> Bool {
760+
true
761+
}
762+
763+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu0_: Equatable>(_ lhs: __macro_local_6MemberfMu0_, _ rhs: __macro_local_6MemberfMu0_) -> Bool {
764+
lhs != rhs
765+
}
766+
767+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu1_: AnyObject>(_ lhs: __macro_local_6MemberfMu1_, _ rhs: __macro_local_6MemberfMu1_) -> Bool {
768+
lhs !== rhs
769+
}
770+
771+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu2_: Equatable & AnyObject>(_ lhs: __macro_local_6MemberfMu2_, _ rhs: __macro_local_6MemberfMu2_) -> Bool {
772+
lhs != rhs
773+
}
676774
}
677775
"""
678776
}

Tests/ComposableArchitectureMacrosTests/PresentsMacroTests.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
final class PresentsMacroTests: XCTestCase {
99
override func invokeTest() {
1010
withMacroTesting(
11-
// isRecording: true,
11+
// record: .failed,
1212
macros: [PresentsMacro.self]
1313
) {
1414
super.invokeTest()
@@ -200,6 +200,22 @@
200200
public mutating func _$willModify() {
201201
_$observationRegistrar._$willModify()
202202
}
203+
204+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu_>(_ lhs: __macro_local_6MemberfMu_, _ rhs: __macro_local_6MemberfMu_) -> Bool {
205+
true
206+
}
207+
208+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu0_: Equatable>(_ lhs: __macro_local_6MemberfMu0_, _ rhs: __macro_local_6MemberfMu0_) -> Bool {
209+
lhs != rhs
210+
}
211+
212+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu1_: AnyObject>(_ lhs: __macro_local_6MemberfMu1_, _ rhs: __macro_local_6MemberfMu1_) -> Bool {
213+
lhs !== rhs
214+
}
215+
216+
private nonisolated func shouldNotifyObservers<__macro_local_6MemberfMu2_: Equatable & AnyObject>(_ lhs: __macro_local_6MemberfMu2_, _ rhs: __macro_local_6MemberfMu2_) -> Bool {
217+
lhs != rhs
218+
}
203219
}
204220
"""#
205221
}

0 commit comments

Comments
 (0)