Skip to content

Commit 6409a03

Browse files
authored
Perception checking in iOS 17 and a bug fix (pointfreeco#10)
* Check perception even in iOS 17. * Fix implicit closures. * wip * wip * Update WithPerceptionTracking.swift * fix * wip * More tests and some performance improvements. * more performance * wip
1 parent 14ac2d3 commit 6409a03

File tree

3 files changed

+119
-25
lines changed

3 files changed

+119
-25
lines changed

Sources/Perception/PerceptionRegistrar.swift

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ extension PerceptionRegistrar {
8080
_ subject: Subject,
8181
keyPath: KeyPath<Subject, Member>
8282
) {
83+
perceptionCheck()
84+
8385
#if canImport(Observation)
8486
if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) {
8587
func `open`<T: Observable>(_ subject: T) {
@@ -92,7 +94,6 @@ extension PerceptionRegistrar {
9294
open(subject)
9395
}
9496
} else {
95-
perceptionCheck()
9697
self.perceptionRegistrar.access(subject, keyPath: keyPath)
9798
}
9899
#endif
@@ -194,7 +195,7 @@ extension PerceptionRegistrar: Hashable {
194195

195196
#if DEBUG
196197
private func perceptionCheck() {
197-
if #unavailable(iOS 17, macOS 14, tvOS 17, watchOS 10),
198+
if
198199
!_PerceptionLocals.isInPerceptionTracking,
199200
!_PerceptionLocals.skipPerceptionChecking,
200201
isInSwiftUIBody()
@@ -214,8 +215,8 @@ extension PerceptionRegistrar: Hashable {
214215
.drop(while: { $0 != .init(ascii: "$") })
215216
.prefix(while: { $0 != .init(ascii: " ") })
216217
guard
218+
mangledSymbol.isMangledViewBodyGetter,
217219
let demangled = String(Substring(mangledSymbol)).demangled,
218-
demangled.contains("body.getter : "),
219220
!demangled.isActionClosure
220221
else {
221222
continue
@@ -228,11 +229,12 @@ extension PerceptionRegistrar: Hashable {
228229
extension String {
229230
fileprivate var isActionClosure: Bool {
230231
var view = self[...].utf8
231-
guard view.starts(with: "closure #".utf8) else { return false }
232+
guard
233+
view.starts(with: "closure #".utf8) || view.starts(with: "implicit closure #".utf8)
234+
else { return false }
232235
view = view.drop(while: { $0 != .init(ascii: "-") })
233236
return view.starts(with: "-> () in ".utf8)
234237
}
235-
236238
fileprivate var demangled: String? {
237239
return self.utf8CString.withUnsafeBufferPointer { mangledNameUTF8CStr in
238240
let demangledNamePtr = swift_demangle(
@@ -291,3 +293,26 @@ extension PerceptionRegistrar: Hashable {
291293
apply()
292294
}
293295
#endif
296+
297+
extension Substring.UTF8View {
298+
fileprivate var isMangledViewBodyGetter: Bool {
299+
self._contains("V4bodyQrvg".utf8)
300+
}
301+
fileprivate func _contains(_ other: String.UTF8View) -> Bool {
302+
guard let first = other.first
303+
else { return false }
304+
let otherCount = other.count
305+
var input = self
306+
while let index = input.firstIndex(where: { first == $0 }) {
307+
input = input[index...]
308+
if
309+
input.count >= otherCount,
310+
zip(input, other).allSatisfy(==)
311+
{
312+
return true
313+
}
314+
input.removeFirst()
315+
}
316+
return false
317+
}
318+
}

Sources/Perception/WithPerceptionTracking.swift

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,5 @@
11
import SwiftUI
22

3-
@available(iOS, deprecated: 17)
4-
@available(macOS, deprecated: 14)
5-
@available(tvOS, deprecated: 17)
6-
@available(watchOS, deprecated: 10)
7-
public enum _PerceptionLocals {
8-
@TaskLocal public static var isInPerceptionTracking = false
9-
@TaskLocal public static var skipPerceptionChecking = false
10-
}
11-
123
/// Observes changes to perceptible models.
134
///
145
/// Use this view to automatically subscribe to the changes of any fields in ``Perceptible()``
@@ -58,21 +49,31 @@ public struct WithPerceptionTracking<Content> {
5849

5950
public var body: Content {
6051
if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) {
61-
return self.content()
52+
return self.instrumentedBody()
6253
} else {
6354
// NB: View will not re-render when 'id' changes unless we access it in the view.
6455
let _ = self.id
6556
return withPerceptionTracking {
66-
_PerceptionLocals.$isInPerceptionTracking.withValue(true) {
67-
self.content()
68-
}
57+
self.instrumentedBody()
6958
} onChange: {
7059
Task { @MainActor in
7160
self.id += 1
7261
}
7362
}
7463
}
7564
}
65+
66+
@_transparent
67+
@inline(__always)
68+
private func instrumentedBody() -> Content {
69+
#if DEBUG
70+
return _PerceptionLocals.$isInPerceptionTracking.withValue(true) {
71+
self.content()
72+
}
73+
#else
74+
return self.content()
75+
#endif
76+
}
7677
}
7778

7879
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *)
@@ -157,3 +158,12 @@ extension WithPerceptionTracking: View where Content: View {
157158
self.content = content
158159
}
159160
}
161+
162+
@available(iOS, deprecated: 17)
163+
@available(macOS, deprecated: 14)
164+
@available(tvOS, deprecated: 17)
165+
@available(watchOS, deprecated: 10)
166+
public enum _PerceptionLocals {
167+
@TaskLocal public static var isInPerceptionTracking = false
168+
@TaskLocal public static var skipPerceptionChecking = false
169+
}

Tests/PerceptionTests/RuntimeWarningTests.swift

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Combine
12
import Perception
23
import SwiftUI
34
import XCTest
@@ -268,15 +269,73 @@ final class RuntimeWarningTests: XCTestCase {
268269
self.render(FeatureView())
269270
}
270271

271-
private func expectFailure() {
272-
if #unavailable(iOS 17, macOS 14, tvOS 17, watchOS 10) {
273-
XCTExpectFailure {
274-
$0.compactDescription == """
275-
Perceptible state was accessed but is not being tracked. Track changes to state by \
276-
wrapping your view in a 'WithPerceptionTracking' view.
277-
"""
272+
func testActionClosure_CallMethodWithArguments() {
273+
struct FeatureView: View {
274+
@State var model = Model()
275+
var body: some View {
276+
Text("Hi")
277+
.onAppear { _ = foo(42) }
278+
}
279+
func foo(_: Int) -> Bool {
280+
_ = self.model.count
281+
return true
278282
}
279283
}
284+
285+
self.render(FeatureView())
286+
}
287+
288+
func testActionClosure_WithArguments() {
289+
struct FeatureView: View {
290+
@State var model = Model()
291+
var body: some View {
292+
Text("Hi")
293+
.onReceive(Just(1)) { _ in
294+
_ = self.model.count
295+
}
296+
}
297+
}
298+
299+
self.render(FeatureView())
300+
}
301+
302+
func testActionClosure_WithArguments_ImplicitClosure() {
303+
struct FeatureView: View {
304+
@State var model = Model()
305+
var body: some View {
306+
Text("Hi")
307+
.onReceive(Just(1), perform: self.foo)
308+
}
309+
func foo(_: Int) {
310+
_ = self.model.count
311+
}
312+
}
313+
314+
self.render(FeatureView())
315+
}
316+
317+
func testImplicitActionClosure() {
318+
struct FeatureView: View {
319+
@State var model = Model()
320+
var body: some View {
321+
Text("Hi")
322+
.onAppear(perform: foo)
323+
}
324+
func foo() {
325+
_ = self.model.count
326+
}
327+
}
328+
329+
self.render(FeatureView())
330+
}
331+
332+
private func expectFailure() {
333+
XCTExpectFailure {
334+
$0.compactDescription == """
335+
Perceptible state was accessed but is not being tracked. Track changes to state by \
336+
wrapping your view in a 'WithPerceptionTracking' view.
337+
"""
338+
}
280339
}
281340

282341
private func render(_ view: some View) {

0 commit comments

Comments
 (0)