diff --git a/Sources/Perception/Bindable.swift b/Sources/Perception/Bindable.swift index 30210ef..40f76ed 100644 --- a/Sources/Perception/Bindable.swift +++ b/Sources/Perception/Bindable.swift @@ -5,11 +5,11 @@ /// perceptible objects. /// /// A backport of SwiftUI's `Bindable` property wrapper. - @available(iOS, introduced: 13, obsoleted: 17) - @available(macOS, introduced: 10.15, obsoleted: 14) - @available(tvOS, introduced: 13, obsoleted: 17) - @available(watchOS, introduced: 6, obsoleted: 10) - @available(visionOS, unavailable) + @available(iOS, introduced: 13, obsoleted: 17, message: "Use @Bindable without the 'Perception.' prefix.") + @available(macOS, introduced: 10.15, obsoleted: 14, message: "Use @Bindable without the 'Perception.' prefix.") + @available(tvOS, introduced: 13, obsoleted: 17, message: "Use @Bindable without the 'Perception.' prefix.") + @available(watchOS, introduced: 6, obsoleted: 10, message: "Use @Bindable without the 'Perception.' prefix.") + @available(visionOS, unavailable, message: "Use @Bindable without the 'Perception.' prefix.") @dynamicMemberLookup @propertyWrapper public struct Bindable { diff --git a/Sources/Perception/PerceptionRegistrar.swift b/Sources/Perception/PerceptionRegistrar.swift index 400fdaa..c552268 100644 --- a/Sources/Perception/PerceptionRegistrar.swift +++ b/Sources/Perception/PerceptionRegistrar.swift @@ -1,4 +1,5 @@ import Foundation +import SwiftUI #if canImport(Observation) import Observation @@ -230,10 +231,15 @@ extension PerceptionRegistrar: Hashable { let mangledSymbol = callStackSymbol.utf8 .drop(while: { $0 != .init(ascii: "$") }) .prefix(while: { $0 != .init(ascii: " ") }) - + guard let demangled = String(Substring(mangledSymbol)).demangled + else { + continue + } + if demangled.isGeometryTrailingClosure { + return true + } guard mangledSymbol.isMangledViewBodyGetter, - let demangled = String(Substring(mangledSymbol)).demangled, !demangled.isSuspendingClosure, !demangled.isActionClosure else { @@ -247,7 +253,12 @@ extension PerceptionRegistrar: Hashable { } } + extension String { + var isGeometryTrailingClosure: Bool { + self.contains("(SwiftUI.GeometryProxy) -> ") + } + fileprivate var isSuspendingClosure: Bool { let fragment = self.utf8.drop(while: { $0 != .init(ascii: ")") }).dropFirst() return fragment.starts( diff --git a/Tests/PerceptionTests/RuntimeWarningTests.swift b/Tests/PerceptionTests/RuntimeWarningTests.swift index 83c9f2c..9690ea1 100644 --- a/Tests/PerceptionTests/RuntimeWarningTests.swift +++ b/Tests/PerceptionTests/RuntimeWarningTests.swift @@ -499,6 +499,51 @@ try await Task.sleep(for: .milliseconds(100)) } + func testGeometryReader_WithoutPerceptionTracking() { + struct FeatureView: View { + let model = Model() + var body: some View { + WithPerceptionTracking { + GeometryReader { _ in + Text(expectRuntimeWarning { self.model.count }.description) + } + } + } + } + self.render(FeatureView()) + } + + func testGeometryReader_WithProperPerceptionTracking() { + struct FeatureView: View { + let model = Model() + var body: some View { + GeometryReader { _ in + WithPerceptionTracking { + Text(self.model.count.description) + } + } + } + } + self.render(FeatureView()) + } + + func testGeometryReader_ComputedProperty_ImproperPerceptionTracking() { + struct FeatureView: View { + let model = Model() + var body: some View { + WithPerceptionTracking { + content + } + } + var content: some View { + GeometryReader { _ in + Text(expectRuntimeWarning { self.model.count }.description) + } + } + } + self.render(FeatureView()) + } + private func render(_ view: some View) { let image = ImageRenderer(content: view).cgImage _ = image