diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c14dbb..f791862 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,11 +19,11 @@ jobs: strategy: matrix: xcode: ['15.1'] - config: ['debug'] + config: ['debug', 'release'] runs-on: macos-13 steps: - uses: actions/checkout@v3 - name: Select Xcode ${{ matrix.xcode }} run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app - name: Run ${{ matrix.config }} tests - run: swift test + run: swift test -c ${{ matrix.config }} diff --git a/Example/Example/ContentView.swift b/Example/Example/ContentView.swift index 32d8559..cb8b97d 100644 --- a/Example/Example/ContentView.swift +++ b/Example/Example/ContentView.swift @@ -6,6 +6,7 @@ class CounterModel { var count = 0 var isDisplayingCount = true var isPresentingSheet = false + var text = "" func decrementButtonTapped() { count -= 1 } @@ -24,6 +25,7 @@ struct ContentView: View { WithPerceptionTracking { let _ = print("\(Self.self): tracked change.") Form { + TextField("Text", text: $model.text) if model.isDisplayingCount { Text(model.count.description) } else { diff --git a/Sources/Perception/Internal/Memoization.swift b/Sources/Perception/Internal/Memoization.swift deleted file mode 100644 index 5a2d728..0000000 --- a/Sources/Perception/Internal/Memoization.swift +++ /dev/null @@ -1,45 +0,0 @@ -#if DEBUG - import Foundation - import OrderedCollections - - func memoize( - maxCapacity: Int = 500, - _ apply: @escaping () -> Result - ) -> () -> Result { - let cache = Cache<[NSNumber], Result>(maxCapacity: maxCapacity) - return { - let callStack = Thread.callStackReturnAddresses - guard let memoizedResult = cache[callStack] - else { - let result = apply() - defer { cache[callStack] = result } - return result - } - return memoizedResult - } - } - - private final class Cache: @unchecked Sendable { - var dictionary = OrderedDictionary() - var lock = NSLock() - let maxCapacity: Int - init(maxCapacity: Int = 500) { - self.maxCapacity = maxCapacity - } - subscript(key: Key) -> Value? { - get { - self.lock.sync { - self.dictionary[key] - } - } - set { - self.lock.sync { - self.dictionary[key] = newValue - if self.dictionary.count > self.maxCapacity { - self.dictionary.removeFirst() - } - } - } - } - } -#endif diff --git a/Sources/Perception/PerceptionRegistrar.swift b/Sources/Perception/PerceptionRegistrar.swift index be238d2..0a35848 100644 --- a/Sources/Perception/PerceptionRegistrar.swift +++ b/Sources/Perception/PerceptionRegistrar.swift @@ -14,6 +14,10 @@ import Foundation @available(watchOS, deprecated: 10, renamed: "ObservationRegistrar") public struct PerceptionRegistrar: Sendable { private let _rawValue: AnySendable + #if DEBUG + private let isPerceptionCheckingEnabled: Bool + fileprivate let perceptionChecks = LockIsolated<[Location: Bool]>([:]) + #endif /// Creates an instance of the observation registrar. /// @@ -21,7 +25,7 @@ public struct PerceptionRegistrar: Sendable { /// ``PerceptionRegistrar`` when using the /// ``Perception/Perceptible()`` macro to indicate observably /// of a type. - public init() { + public init(isPerceptionCheckingEnabled: Bool = Perception.isPerceptionCheckingEnabled) { if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { #if canImport(Observation) self._rawValue = AnySendable(ObservationRegistrar()) @@ -31,6 +35,9 @@ public struct PerceptionRegistrar: Sendable { } else { self._rawValue = AnySendable(_PerceptionRegistrar()) } + #if DEBUG + self.isPerceptionCheckingEnabled = isPerceptionCheckingEnabled + #endif } #if canImport(Observation) @@ -78,10 +85,13 @@ extension PerceptionRegistrar { @_disfavoredOverload public func access( _ subject: Subject, - keyPath: KeyPath + keyPath: KeyPath, + file: StaticString = #file, + line: UInt = #line ) { - perceptionCheck() - + #if DEBUG + self.perceptionCheck(file: file, line: line) + #endif #if canImport(Observation) if #available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) { func `open`(_ subject: T) { @@ -194,37 +204,46 @@ extension PerceptionRegistrar: Hashable { } #if DEBUG - private func perceptionCheck() { - if - isPerceptionCheckingEnabled, - !_PerceptionLocals.isInPerceptionTracking, - !_PerceptionLocals.skipPerceptionChecking, - isInSwiftUIBody() - { - runtimeWarn( - """ - Perceptible state was accessed but is not being tracked. Track changes to state by \ - wrapping your view in a 'WithPerceptionTracking' view. - """ - ) + extension PerceptionRegistrar { + fileprivate func perceptionCheck(file: StaticString, line: UInt) { + if self.isPerceptionCheckingEnabled, + Perception.isPerceptionCheckingEnabled, + !_PerceptionLocals.isInPerceptionTracking, + !_PerceptionLocals.skipPerceptionChecking, + self.isInSwiftUIBody(file: file, line: line) + { + runtimeWarn( + """ + Perceptible state was accessed but is not being tracked. Track changes to state by \ + wrapping your view in a 'WithPerceptionTracking' view. + """ + ) + } } - } - private let isInSwiftUIBody: () -> Bool = memoize { - for callStackSymbol in Thread.callStackSymbols { - let mangledSymbol = callStackSymbol.utf8 - .drop(while: { $0 != .init(ascii: "$") }) - .prefix(while: { $0 != .init(ascii: " ") }) - guard - mangledSymbol.isMangledViewBodyGetter, - let demangled = String(Substring(mangledSymbol)).demangled, - !demangled.isActionClosure - else { - continue + fileprivate func isInSwiftUIBody(file: StaticString, line: UInt) -> Bool { + self.perceptionChecks.withValue { perceptionChecks in + if let result = perceptionChecks[Location(file: file, line: line)] { + return result + } + for callStackSymbol in Thread.callStackSymbols { + let mangledSymbol = callStackSymbol.utf8 + .drop(while: { $0 != .init(ascii: "$") }) + .prefix(while: { $0 != .init(ascii: " ") }) + + guard + mangledSymbol.isMangledViewBodyGetter, + let demangled = String(Substring(mangledSymbol)).demangled, + !demangled.isActionClosure + else { + continue + } + return true + } + perceptionChecks[Location(file: file, line: line)] = false + return false } - return true } - return false } extension String { @@ -263,10 +282,6 @@ extension PerceptionRegistrar: Hashable { outputBufferSize: UnsafeMutablePointer?, flags: UInt32 ) -> UnsafeMutablePointer? -#else - @_transparent - @inline(__always) - private func perceptionCheck() {} #endif #if DEBUG @@ -295,25 +310,52 @@ extension PerceptionRegistrar: Hashable { } #endif -extension Substring.UTF8View { - fileprivate var isMangledViewBodyGetter: Bool { - self._contains("V4bodyQrvg".utf8) +#if DEBUG + extension Substring.UTF8View { + fileprivate var isMangledViewBodyGetter: Bool { + self._contains("V4bodyQrvg".utf8) + } + fileprivate func _contains(_ other: String.UTF8View) -> Bool { + guard let first = other.first + else { return false } + let otherCount = other.count + var input = self + while let index = input.firstIndex(where: { first == $0 }) { + input = input[index...] + if input.count >= otherCount, + zip(input, other).allSatisfy(==) + { + return true + } + input.removeFirst() + } + return false + } } - fileprivate func _contains(_ other: String.UTF8View) -> Bool { - guard let first = other.first - else { return false } - let otherCount = other.count - var input = self - while let index = input.firstIndex(where: { first == $0 }) { - input = input[index...] - if - input.count >= otherCount, - zip(input, other).allSatisfy(==) - { - return true + + private final class LockIsolated: @unchecked Sendable { + private var _value: Value + private let lock = NSRecursiveLock() + init(_ value: @autoclosure @Sendable () throws -> Value) rethrows { + self._value = try value() + } + func withValue( + _ operation: @Sendable (inout Value) throws -> T + ) rethrows -> T { + try self.lock.withLock { + var value = self._value + defer { self._value = value } + return try operation(&value) } - input.removeFirst() } - return false } -} + + private struct Location: Hashable { + let file: String + let line: UInt + init(file: StaticString, line: UInt) { + self.file = file.description + self.line = line + } + } +#endif diff --git a/Sources/PerceptionMacros/PerceptibleMacro.swift b/Sources/PerceptionMacros/PerceptibleMacro.swift index 5953431..4f15c00 100644 --- a/Sources/PerceptionMacros/PerceptibleMacro.swift +++ b/Sources/PerceptionMacros/PerceptibleMacro.swift @@ -48,9 +48,11 @@ public struct PerceptibleMacro { return """ internal nonisolated func access( - keyPath: KeyPath<\(perceptibleType), Member> + keyPath: KeyPath<\(perceptibleType), Member>, + file: StaticString = #file, + line: UInt = #line ) { - \(raw: registrarVariableName).access(self, keyPath: keyPath) + \(raw: registrarVariableName).access(self, keyPath: keyPath, file: file, line: line) } """ } diff --git a/Tests/PerceptionMacrosTests/PerceptionMacrosTests.swift b/Tests/PerceptionMacrosTests/PerceptionMacrosTests.swift index bfc43fc..afb8b59 100644 --- a/Tests/PerceptionMacrosTests/PerceptionMacrosTests.swift +++ b/Tests/PerceptionMacrosTests/PerceptionMacrosTests.swift @@ -47,9 +47,11 @@ private let _$perceptionRegistrar = Perception.PerceptionRegistrar() internal nonisolated func access( - keyPath: KeyPath + keyPath: KeyPath, + file: StaticString = #file, + line: UInt = #line ) { - _$perceptionRegistrar.access(self, keyPath: keyPath) + _$perceptionRegistrar.access(self, keyPath: keyPath, file: file, line: line) } internal nonisolated func withMutation( diff --git a/Tests/PerceptionTests/RuntimeWarningTests.swift b/Tests/PerceptionTests/RuntimeWarningTests.swift index 5bf1f08..66aba0d 100644 --- a/Tests/PerceptionTests/RuntimeWarningTests.swift +++ b/Tests/PerceptionTests/RuntimeWarningTests.swift @@ -1,336 +1,463 @@ -import Combine -import Perception -import SwiftUI -import XCTest - -@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) -@MainActor -final class RuntimeWarningTests: XCTestCase { - func testNotInPerceptionBody() { - let model = Model() - model.count += 1 - XCTAssertEqual(model.count, 1) - } - - func testInPerceptionBody_NotInSwiftUIBody() { - let model = Model() - _PerceptionLocals.$isInPerceptionTracking.withValue(true) { - _ = model.count +#if DEBUG + import Combine + import Perception + import SwiftUI + import XCTest + + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) + @MainActor + final class RuntimeWarningTests: XCTestCase { + func testNotInPerceptionBody() { + let model = Model() + model.count += 1 + XCTAssertEqual(model.count, 1) } - } - - func testNotInPerceptionBody_InSwiftUIBody() { - self.expectFailure() - struct FeatureView: View { + func testInPerceptionBody_NotInSwiftUIBody() { let model = Model() - var body: some View { - Text(self.model.count.description) + _PerceptionLocals.$isInPerceptionTracking.withValue(true) { + _ = model.count } } - self.render(FeatureView()) - } - - func testNotInPerceptionBody_InSwiftUIBody_Wrapper() { - self.expectFailure() - struct FeatureView: View { - let model = Model() - var body: some View { - Wrapper { - Text(self.model.count.description) + func testNotInPerceptionBody_InSwiftUIBody() { + struct FeatureView: View { + let model = Model() + var body: some View { + Text(expectRuntimeWarning { self.model.count }.description) } } + self.render(FeatureView()) } - self.render(FeatureView()) - } - func testInPerceptionBody_InSwiftUIBody_Wrapper() { - struct FeatureView: View { - let model = Model() - var body: some View { - WithPerceptionTracking { + func testNotInPerceptionBody_InSwiftUIBody_Wrapper() { + struct FeatureView: View { + let model = Model() + var body: some View { Wrapper { - Text(self.model.count.description) + Text(expectRuntimeWarning { self.model.count }.description) } } } + self.render(FeatureView()) } - self.render(FeatureView()) - } - - func testInPerceptionBody_InSwiftUIBody() { - struct FeatureView: View { - let model = Model() - var body: some View { - WithPerceptionTracking { - Text(self.model.count.description) - } - } - } - self.render(FeatureView()) - } - - func testNotInPerceptionBody_SwiftUIBinding() { - self.expectFailure() - struct FeatureView: View { - @State var model = Model() - var body: some View { - VStack { - TextField("", text: self.$model.text) + func testInPerceptionBody_InSwiftUIBody_Wrapper() { + struct FeatureView: View { + let model = Model() + var body: some View { + WithPerceptionTracking { + Wrapper { + Text(self.model.count.description) + } + } } } + self.render(FeatureView()) } - self.render(FeatureView()) - } - func testInPerceptionBody_SwiftUIBinding() { - struct FeatureView: View { - @State var model = Model() - var body: some View { - WithPerceptionTracking { - TextField("", text: self.$model.text) + func testInPerceptionBody_InSwiftUIBody() { + struct FeatureView: View { + let model = Model() + var body: some View { + WithPerceptionTracking { + Text(self.model.count.description) + } } } + self.render(FeatureView()) } - self.render(FeatureView()) - } - func testNotInPerceptionBody_ForEach() { - self.expectFailure() - - struct FeatureView: View { - @State var model = Model( - list: [ - Model(count: 1), - Model(count: 2), - Model(count: 3), - ] - ) - var body: some View { - ForEach(model.list) { model in - Text(model.count.description) + func testNotInPerceptionBody_SwiftUIBinding() { + struct FeatureView: View { + @State var model = Model() + var body: some View { + Form { + TextField("", text: expectRuntimeWarning { self.$model.text }) + } } } + self.render(FeatureView()) } - self.render(FeatureView()) - } - - func testInnerInPerceptionBody_ForEach() { - self.expectFailure() - - struct FeatureView: View { - @State var model = Model( - list: [ - Model(count: 1), - Model(count: 2), - Model(count: 3), - ] - ) - var body: some View { - ForEach(model.list) { model in + func testInPerceptionBody_SwiftUIBinding() { + struct FeatureView: View { + @State var model = Model() + var body: some View { WithPerceptionTracking { - Text(model.count.description) + TextField("", text: self.$model.text) } } } + self.render(FeatureView()) } - self.render(FeatureView()) - } - - func testOuterInPerceptionBody_ForEach() { - self.expectFailure() - - struct FeatureView: View { - @State var model = Model( - list: [ - Model(count: 1), - Model(count: 2), - Model(count: 3), - ] - ) - var body: some View { - WithPerceptionTracking { - ForEach(model.list) { model in - Text(model.count.description) + func testNotInPerceptionBody_ForEach() { + struct FeatureView: View { + @State var model = Model( + list: [ + Model(count: 1), + Model(count: 2), + Model(count: 3), + ] + ) + var body: some View { + ForEach(expectRuntimeWarning { model.list }) { model in + Text(expectRuntimeWarning { model.count }.description) } } } - } - self.render(FeatureView()) - } + self.render(FeatureView()) + } - func testOuterAndInnerInPerceptionBody_ForEach() { - struct FeatureView: View { - @State var model = Model( - list: [ - Model(count: 1), - Model(count: 2), - Model(count: 3), - ] - ) - var body: some View { - WithPerceptionTracking { - ForEach(model.list) { model in + func testInnerInPerceptionBody_ForEach() { + struct FeatureView: View { + @State var model = Model( + list: [ + Model(count: 1), + Model(count: 2), + Model(count: 3), + ] + ) + var body: some View { + ForEach(expectRuntimeWarning { model.list }) { model in WithPerceptionTracking { Text(model.count.description) } } } } - } - self.render(FeatureView()) - } - - func testNotInPerceptionBody_Sheet() { - self.expectFailure() + self.render(FeatureView()) + } - struct FeatureView: View { - @State var model = Model(child: Model()) - var body: some View { - Text("Parent") - .sheet(item: $model.child) { child in - Text(child.count.description) + func testOuterInPerceptionBody_ForEach() { + struct FeatureView: View { + @State var model = Model( + list: [ + Model(count: 1), + Model(count: 2), + Model(count: 3), + ] + ) + var body: some View { + WithPerceptionTracking { + ForEach(model.list) { model in + Text(expectRuntimeWarning { model.count }.description) + } } + } } - } - self.render(FeatureView()) - } - - func testInnerInPerceptionBody_Sheet() { - self.expectFailure() + self.render(FeatureView()) + } - struct FeatureView: View { - @State var model = Model(child: Model()) - var body: some View { - Text("Parent") - .sheet(item: $model.child) { child in - WithPerceptionTracking { - Text(child.count.description) + func testOuterAndInnerInPerceptionBody_ForEach() { + struct FeatureView: View { + @State var model = Model( + list: [ + Model(count: 1), + Model(count: 2), + Model(count: 3), + ] + ) + var body: some View { + WithPerceptionTracking { + ForEach(model.list) { model in + WithPerceptionTracking { + Text(model.count.description) + } } } + } } - } - self.render(FeatureView()) - } - - func testOuterInPerceptionBody_Sheet() { - self.expectFailure() + self.render(FeatureView()) + } - struct FeatureView: View { - @State var model = Model(child: Model()) - var body: some View { - WithPerceptionTracking { + func testNotInPerceptionBody_Sheet() { + struct FeatureView: View { + @State var model = Model(child: Model()) + var body: some View { Text("Parent") - .sheet(item: $model.child) { child in - Text(child.count.description) + .sheet(item: expectRuntimeWarning { $model.child }) { child in + Text(expectRuntimeWarning { child.count }.description) } } } - } - self.render(FeatureView()) - } + self.render(FeatureView()) + } - func testOuterAndInnerInPerceptionBody_Sheet() { - struct FeatureView: View { - @State var model = Model(child: Model()) - var body: some View { - WithPerceptionTracking { + func testInnerInPerceptionBody_Sheet() { + struct FeatureView: View { + @State var model = Model(child: Model()) + var body: some View { Text("Parent") - .sheet(item: $model.child) { child in + .sheet(item: expectRuntimeWarning { $model.child }) { child in WithPerceptionTracking { Text(child.count.description) } } } } + + self.render(FeatureView()) } - self.render(FeatureView()) - } + func testOuterInPerceptionBody_Sheet() { + struct FeatureView: View { + @State var model = Model(child: Model()) + var body: some View { + WithPerceptionTracking { + Text("Parent") + .sheet(item: $model.child) { child in + Text(expectRuntimeWarning { child.count }.description) + } + } + } + } + + self.render(FeatureView()) + } + + func testOuterAndInnerInPerceptionBody_Sheet() { + struct FeatureView: View { + @State var model = Model(child: Model()) + var body: some View { + WithPerceptionTracking { + Text("Parent") + .sheet(item: $model.child) { child in + WithPerceptionTracking { + Text(child.count.description) + } + } + } + } + } - func testActionClosure() { - struct FeatureView: View { - @State var model = Model() - var body: some View { - Text("Hi") - .onAppear { _ = self.model.count } + self.render(FeatureView()) + } + + func testActionClosure() { + struct FeatureView: View { + @State var model = Model() + var body: some View { + Text("Hi") + .onAppear { _ = self.model.count } + } } + + self.render(FeatureView()) } - self.render(FeatureView()) - } + func testActionClosure_CallMethodWithArguments() { + struct FeatureView: View { + @State var model = Model() + var body: some View { + Text("Hi") + .onAppear { _ = foo(42) } + } + func foo(_: Int) -> Bool { + _ = self.model.count + return true + } + } - func testActionClosure_CallMethodWithArguments() { - struct FeatureView: View { - @State var model = Model() - var body: some View { - Text("Hi") - .onAppear { _ = foo(42) } + self.render(FeatureView()) + } + + func testActionClosure_WithArguments() { + struct FeatureView: View { + @State var model = Model() + var body: some View { + Text("Hi") + .onReceive(Just(1)) { _ in + _ = self.model.count + } + } } - func foo(_: Int) -> Bool { - _ = self.model.count - return true + + self.render(FeatureView()) + } + + func testActionClosure_WithArguments_ImplicitClosure() { + struct FeatureView: View { + @State var model = Model() + var body: some View { + Text("Hi") + .onReceive(Just(1), perform: self.foo) + } + func foo(_: Int) { + _ = self.model.count + } } + + self.render(FeatureView()) } - self.render(FeatureView()) - } + func testImplicitActionClosure() { + struct FeatureView: View { + @State var model = Model() + var body: some View { + Text("Hi") + .onAppear(perform: foo) + } + func foo() { + _ = self.model.count + } + } - func testActionClosure_WithArguments() { - struct FeatureView: View { - @State var model = Model() - var body: some View { - Text("Hi") - .onReceive(Just(1)) { _ in - _ = self.model.count - } + self.render(FeatureView()) + } + + func testRegistrarDisablePerceptionTracking() { + struct FeatureView: View { + let model = Model() + let registrar = PerceptionRegistrar(isPerceptionCheckingEnabled: false) + var body: some View { + let _ = registrar.access(model, keyPath: \.count) + Text("Hi") + } } + self.render(FeatureView()) } - self.render(FeatureView()) - } + func testGlobalDisablePerceptionTracking() { + let previous = Perception.isPerceptionCheckingEnabled + Perception.isPerceptionCheckingEnabled = false + defer { Perception.isPerceptionCheckingEnabled = previous } + + struct FeatureView: View { + let model = Model() + var body: some View { + Text(model.count.description) + } + } + self.render(FeatureView()) + } + + func testParentAccessingChildState_ParentNotObserving_ChildObserving() { + struct ChildView: View { + let model: Model + var body: some View { + WithPerceptionTracking { + Text(model.count.description) + .onAppear { let _ = model.count } + } + } + } + struct FeatureView: View { + let model: Model + let childModel: Model + init() { + self.childModel = Model() + self.model = Model(list: [self.childModel]) + } + var body: some View { + VStack { + ChildView(model: self.childModel) + Text(expectRuntimeWarning { childModel.count }.description) + } + .onAppear { let _ = childModel.count } + } + } - func testActionClosure_WithArguments_ImplicitClosure() { - struct FeatureView: View { - @State var model = Model() - var body: some View { - Text("Hi") - .onReceive(Just(1), perform: self.foo) + self.render(FeatureView()) + } + + func testParentAccessingChildState_ParentObserving_ChildNotObserving() { + struct ChildView: View { + let model: Model + var body: some View { + Text(expectRuntimeWarning { model.count }.description) + .onAppear { let _ = model.count } + } } - func foo(_: Int) { - _ = self.model.count + struct FeatureView: View { + let model: Model + let childModel: Model + init() { + self.childModel = Model() + self.model = Model(list: [self.childModel]) + } + var body: some View { + WithPerceptionTracking { + ChildView(model: self.childModel) + Text(childModel.count.description) + } + .onAppear { let _ = childModel.count } + } } + + self.render(FeatureView()) } - self.render(FeatureView()) - } + func testParentAccessingChildState_ParentNotObserving_ChildNotObserving() { + struct ChildView: View { + let model: Model + var body: some View { + Text(expectRuntimeWarning { model.count }.description) + .onAppear { let _ = model.count } + } + } + struct FeatureView: View { + let model: Model + let childModel: Model + init() { + self.childModel = Model() + self.model = Model(list: [self.childModel]) + } + var body: some View { + VStack { + ChildView(model: self.childModel) + Text(expectRuntimeWarning { childModel.count }.description) + } + .onAppear { let _ = childModel.count } + } + } + + self.render(FeatureView()) + } - func testImplicitActionClosure() { - struct FeatureView: View { - @State var model = Model() - var body: some View { - Text("Hi") - .onAppear(perform: foo) + func testParentAccessingChildState_ParentObserving_ChildObserving() { + struct ChildView: View { + let model: Model + var body: some View { + WithPerceptionTracking { + Text(model.count.description) + .onAppear { let _ = model.count } + } + } } - func foo() { - _ = self.model.count + struct FeatureView: View { + let model: Model + let childModel: Model + init() { + self.childModel = Model() + self.model = Model(list: [self.childModel]) + } + var body: some View { + WithPerceptionTracking { + ChildView(model: self.childModel) + Text(childModel.count.description) + } + .onAppear { let _ = childModel.count } + } } + + self.render(FeatureView()) } - self.render(FeatureView()) + private func render(_ view: some View) { + let image = ImageRenderer(content: view).cgImage + _ = image + } } - private func expectFailure() { - XCTExpectFailure { + private func expectRuntimeWarning(failingBlock: () -> R) -> R { + XCTExpectFailure(failingBlock: failingBlock) { $0.compactDescription == """ Perceptible state was accessed but is not being tracked. Track changes to state by \ wrapping your view in a 'WithPerceptionTracking' view. @@ -338,35 +465,30 @@ final class RuntimeWarningTests: XCTestCase { } } - private func render(_ view: some View) { - let image = ImageRenderer(content: view).cgImage - _ = image - } -} - -@Perceptible -private class Model: Identifiable { - var child: Model? - var count: Int - var list: [Model] - var text: String - - init( - child: Model? = nil, - count: Int = 0, - list: [Model] = [], - text: String = "" - ) { - self.child = child - self.count = count - self.list = list - self.text = text + @Perceptible + private class Model: Identifiable { + var child: Model? + var count: Int + var list: [Model] + var text: String + + init( + child: Model? = nil, + count: Int = 0, + list: [Model] = [], + text: String = "" + ) { + self.child = child + self.count = count + self.list = list + self.text = text + } } -} -struct Wrapper: View { - @ViewBuilder var content: Content - var body: some View { - self.content + struct Wrapper: View { + @ViewBuilder var content: Content + var body: some View { + self.content + } } -} +#endif