Skip to content

Commit b7fc720

Browse files
pyrtsastephencelis
andauthored
Mention accessed property key path in perception warnings (#167)
* Fix unit tests on macOS, where tests now build against macosx14.0 by default * Update CI matrix to include Xcode 26.0 * Update CI to use actions/checkout@v5 * Skip perception checking tests if ImageRenderer is not available * Re-enable several perception checking tests on macOS * Mention accessed property key path in perception warnings --------- Co-authored-by: Stephen Celis <stephen@stephencelis.com>
1 parent a27c2d4 commit b7fc720

File tree

5 files changed

+28
-28
lines changed

5 files changed

+28
-28
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ It's unfortunate to have to wrap your view's content in `WithPerceptionTracking`
6666
then you will helpfully get a runtime warning letting you know that observation is not set up
6767
correctly:
6868

69-
> 🟣 Runtime Warning: Perceptible state was accessed from a view but is not being tracked.
69+
> 🟣 Runtime Warning: Perceptible state '\FeatureModel.count' was accessed from a view but is not being tracked.
7070
7171
Finally, the `Observations` async sequence has been back-ported as `Perceptions`, which can be used
7272
to observe changes to perceptible and observable objects over time:

Sources/PerceptionCore/Documentation.docc/PerceptionCore.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ It's unfortunate to have to wrap your view's content in `WithPerceptionTracking`
5353
then you will helpfully get a runtime warning letting you know that observation is not set up
5454
correctly:
5555

56-
> 🟣 Runtime Warning: Perceptible state was accessed from a view but is not being tracked.
56+
> 🟣 Runtime Warning: Perceptible state '\FeatureModel.count' was accessed from a view but is not being tracked.
5757
5858
Finally, the `Observations` async sequence has been back-ported as `Perceptions`, which can be used
5959
to observe changes to perceptible and observable objects over time:

Sources/PerceptionCore/Perception/PerceptionRegistrar.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public struct PerceptionRegistrar: Sendable {
6262
keyPath: KeyPath<Subject, Member>
6363
) {
6464
#if DEBUG && canImport(SwiftUI)
65-
check()
65+
check(keyPath)
6666
#endif
6767
#if canImport(Observation)
6868
if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *),
@@ -254,7 +254,7 @@ extension PerceptionRegistrar: Hashable {
254254
extension PerceptionRegistrar {
255255
@_transparent
256256
@usableFromInline
257-
func check() {
257+
func check<Subject, Member>(_ keyPath: KeyPath<Subject, Member>) {
258258
if _isPerceptionCheckingEnabled,
259259
PerceptionCore.isPerceptionCheckingEnabled,
260260
!_PerceptionLocals.isInPerceptionTracking,
@@ -263,7 +263,7 @@ extension PerceptionRegistrar: Hashable {
263263
{
264264
reportIssue(
265265
"""
266-
Perceptible state was accessed from a view but is not being tracked.
266+
Perceptible state '\(keyPath)' was accessed from a view but is not being tracked.
267267
268268
Use this warning's stack trace to locate the view in question and wrap it with a \
269269
'WithPerceptionTracking' view. For example:

Sources/PerceptionCore/SwiftUI/WithPerceptionTracking.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
/// If a field of a `@Perceptible` model is accessed in a view while _not_ inside
4141
/// ``WithPerceptionTracking``, then a runtime warning will helpfully be triggered:
4242
///
43-
/// > Warning: Perceptible state was accessed but is not being tracked.
43+
/// > Warning: Perceptible state '\FeatureModel.count' was accessed but is not being tracked.
4444
///
4545
/// To debug this, expand the warning in the Issue Navigator of Xcode (cmd+5), and click through
4646
/// the stack frames displayed to find the line in your view where you are accessing state without

Tests/PerceptionTests/PerceptionCheckingTests.swift

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
struct FeatureView: View {
3636
let model = Model()
3737
var body: some View {
38-
Text(expectRuntimeWarning { model.count }.description)
38+
Text(expectRuntimeWarning(on: #"\Model.count"#) { model.count }.description)
3939
}
4040
}
4141
try await render(FeatureView())
@@ -47,7 +47,7 @@
4747
let model = Model()
4848
var body: some View {
4949
Wrapper {
50-
Text(expectRuntimeWarning { model.count }.description)
50+
Text(expectRuntimeWarning(on: #"\Model.count"#) { model.count }.description)
5151
}
5252
}
5353
}
@@ -88,13 +88,13 @@
8888
@Perception.Bindable var model: Model
8989
var body: some View {
9090
Form {
91-
TextField("", text: expectRuntimeWarning { $model.text })
91+
TextField("", text: expectRuntimeWarning(on: #"\Model.text"#) { $model.text })
9292
}
9393
}
9494
}
9595
#if os(macOS)
9696
// NB: This failure is triggered out-of-body by the binding.
97-
XCTExpectFailure { $0.compactDescription.contains("Perceptible state was accessed") }
97+
XCTExpectFailure { $0.compactDescription.contains(#"Perceptible state '\Model.text' was accessed"#) }
9898
#endif
9999
try await render(FeatureView(model: Model()))
100100
}
@@ -117,8 +117,8 @@
117117
struct FeatureView: View {
118118
let model: Model
119119
var body: some View {
120-
ForEach(expectRuntimeWarning { model.list }) { model in
121-
Text(expectRuntimeWarning { model.count }.description)
120+
ForEach(expectRuntimeWarning(on: #"\Model.list"#) { model.list }) { model in
121+
Text(expectRuntimeWarning(on: #"\Model.count"#) { model.count }.description)
122122
}
123123
}
124124
}
@@ -141,7 +141,7 @@
141141
struct FeatureView: View {
142142
let model: Model
143143
var body: some View {
144-
ForEach(expectRuntimeWarning { model.list }) { model in
144+
ForEach(expectRuntimeWarning(on: #"\Model.list"#) { model.list }) { model in
145145
WithPerceptionTracking {
146146
Text(model.count.description)
147147
}
@@ -169,7 +169,7 @@
169169
var body: some View {
170170
WithPerceptionTracking {
171171
ForEach(model.list) { model in
172-
Text(expectRuntimeWarning { model.count }.description)
172+
Text(expectRuntimeWarning(on: #"\Model.count"#) { model.count }.description)
173173
}
174174
}
175175
}
@@ -222,13 +222,13 @@
222222
@Perception.Bindable var model: Model
223223
var body: some View {
224224
Text("Parent")
225-
.sheet(item: expectRuntimeWarning { $model.child }) { child in
226-
Text(expectRuntimeWarning { child.count }.description)
225+
.sheet(item: expectRuntimeWarning(on: #"\Model.child"#) { $model.child }) { child in
226+
Text(expectRuntimeWarning(on: #"\Model.count"#) { child.count }.description)
227227
}
228228
}
229229
}
230230
// NB: This failure is triggered out-of-body by the binding.
231-
XCTExpectFailure { $0.compactDescription.contains("Perceptible state was accessed") }
231+
XCTExpectFailure { $0.compactDescription.contains(#"Perceptible state '\Model.child' was accessed"#) }
232232
try await render(FeatureView(model: Model(child: Model())))
233233
}
234234

@@ -238,15 +238,15 @@
238238
@Perception.Bindable var model: Model
239239
var body: some View {
240240
Text("Parent")
241-
.sheet(item: expectRuntimeWarning { $model.child }) { child in
241+
.sheet(item: expectRuntimeWarning(on: #"\Model.child"#) { $model.child }) { child in
242242
WithPerceptionTracking {
243243
Text(child.count.description)
244244
}
245245
}
246246
}
247247
}
248248
// NB: This failure is triggered out-of-body by the binding.
249-
XCTExpectFailure { $0.compactDescription.contains("Perceptible state was accessed") }
249+
XCTExpectFailure { $0.compactDescription.contains(#"Perceptible state '\Model.child' was accessed"#) }
250250
try await render(FeatureView(model: Model(child: Model())))
251251
}
252252

@@ -258,7 +258,7 @@
258258
WithPerceptionTracking {
259259
Text("Parent")
260260
.sheet(item: $model.child) { child in
261-
Text(expectRuntimeWarning { child.count }.description)
261+
Text(expectRuntimeWarning(on: #"\Model.count"#) { child.count }.description)
262262
}
263263
}
264264
}
@@ -384,7 +384,7 @@
384384
var body: some View {
385385
VStack {
386386
ChildView(model: self.childModel)
387-
Text(expectRuntimeWarning { childModel.count }.description)
387+
Text(expectRuntimeWarning(on: #"\Model.count"#) { childModel.count }.description)
388388
}
389389
.onAppear { let _ = childModel.count }
390390
}
@@ -398,7 +398,7 @@
398398
struct ChildView: View {
399399
let model: Model
400400
var body: some View {
401-
Text(expectRuntimeWarning { model.count }.description)
401+
Text(expectRuntimeWarning(on: #"\Model.count"#) { model.count }.description)
402402
.onAppear { let _ = model.count }
403403
}
404404
}
@@ -426,7 +426,7 @@
426426
struct ChildView: View {
427427
let model: Model
428428
var body: some View {
429-
Text(expectRuntimeWarning { model.count }.description)
429+
Text(expectRuntimeWarning(on: #"\Model.count"#) { model.count }.description)
430430
.onAppear { let _ = model.count }
431431
}
432432
}
@@ -440,7 +440,7 @@
440440
var body: some View {
441441
VStack {
442442
ChildView(model: self.childModel)
443-
Text(expectRuntimeWarning { childModel.count }.description)
443+
Text(expectRuntimeWarning(on: #"\Model.count"#) { childModel.count }.description)
444444
}
445445
.onAppear { let _ = childModel.count }
446446
}
@@ -535,7 +535,7 @@
535535
var body: some View {
536536
WithPerceptionTracking {
537537
GeometryReader { _ in
538-
Text(expectRuntimeWarning { model.count }.description)
538+
Text(expectRuntimeWarning(on: #"\Model.count"#) { model.count }.description)
539539
}
540540
}
541541
}
@@ -569,7 +569,7 @@
569569
}
570570
var content: some View {
571571
GeometryReader { _ in
572-
Text(expectRuntimeWarning { model.count }.description)
572+
Text(expectRuntimeWarning(on: #"\Model.count"#) { model.count }.description)
573573
}
574574
}
575575
}
@@ -613,9 +613,9 @@
613613
}
614614
}
615615

616-
private func expectRuntimeWarning<R>(failingBlock: () -> R) -> R {
616+
private func expectRuntimeWarning<R>(on keyPathString: String, failingBlock: () -> R) -> R {
617617
XCTExpectFailure(failingBlock: failingBlock) {
618-
$0.compactDescription.contains("Perceptible state was accessed")
618+
$0.compactDescription.contains("Perceptible state '\(keyPathString)' was accessed")
619619
}
620620
}
621621

0 commit comments

Comments
 (0)