diff --git a/Package.swift b/Package.swift index 9b618ea..5c6c19c 100644 --- a/Package.swift +++ b/Package.swift @@ -29,6 +29,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/StanfordSpezi/Spezi.git", from: "1.8.0"), + .package(url: "https://github.com/StanfordSpezi/SpeziFoundation.git", from: "2.0.1"), .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"), .package(url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.17.0") ] + swiftLintPackage(), diff --git a/Sources/SpeziValidation/ValidationState/ReceiveValidationModifier.swift b/Sources/SpeziValidation/ValidationState/ReceiveValidationModifier.swift index 9dc3ee0..3d7bf05 100644 --- a/Sources/SpeziValidation/ValidationState/ReceiveValidationModifier.swift +++ b/Sources/SpeziValidation/ValidationState/ReceiveValidationModifier.swift @@ -6,19 +6,10 @@ // SPDX-License-Identifier: MIT // +import SpeziFoundation import SwiftUI -@MainActor -private struct IsolatedValidationBinding: Sendable { - let state: ValidationState.Binding - - init(_ state: ValidationState.Binding) { - self.state = state - } -} - - /// Provide access to validation state to the parent view. /// /// The internal preference key to provide parent views access to all configured ``ValidationEngine`` and input @@ -46,11 +37,9 @@ extension View { /// - Parameter state: The binding to the ``ValidationState``. /// - Returns: The modified view. public func receiveValidation(in state: ValidationState.Binding) -> some View { - let binding = IsolatedValidationBinding(state) - - return onPreferenceChange(CapturedValidationStateKey.self) { entries in - Task { @Sendable @MainActor in - binding.state.wrappedValue = ValidationContext(entries: entries) + onPreferenceChange(CapturedValidationStateKey.self) { entries in + runOrScheduleOnMainActor { + state.wrappedValue = ValidationContext(entries: entries) } } } diff --git a/Sources/SpeziValidation/ValidationState/ValidationContext.swift b/Sources/SpeziValidation/ValidationState/ValidationContext.swift index f90ac7e..a638790 100644 --- a/Sources/SpeziValidation/ValidationState/ValidationContext.swift +++ b/Sources/SpeziValidation/ValidationState/ValidationContext.swift @@ -17,7 +17,7 @@ import SwiftUI /// /// You can use this structure to retrieve the state of all ``ValidationEngine``s of a subview or manually /// initiate validation by calling ``validateSubviews(switchFocus:)``. E.g., when pressing on a submit button of a form. -public struct ValidationContext { +public struct ValidationContext: Sendable { private let entries: [CapturedValidationState] diff --git a/Sources/SpeziValidation/ValidationState/ValidationState.swift b/Sources/SpeziValidation/ValidationState/ValidationState.swift index 055aedf..65c3b6f 100644 --- a/Sources/SpeziValidation/ValidationState/ValidationState.swift +++ b/Sources/SpeziValidation/ValidationState/ValidationState.swift @@ -67,8 +67,8 @@ public struct ValidationState: DynamicProperty { extension ValidationState { /// A binding to a ``ValidationState``. @propertyWrapper - public struct Binding { - private let binding: SwiftUI.Binding + public struct Binding: Sendable { + @MainActor private let binding: SwiftUI.Binding /// The validation context. public var wrappedValue: ValidationContext { diff --git a/Sources/SpeziViews/Views/Layout/HorizontalGeometryReader.swift b/Sources/SpeziViews/Views/Layout/HorizontalGeometryReader.swift index 6f46ee1..ca6ad62 100644 --- a/Sources/SpeziViews/Views/Layout/HorizontalGeometryReader.swift +++ b/Sources/SpeziViews/Views/Layout/HorizontalGeometryReader.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import SpeziFoundation import SwiftUI @@ -39,18 +40,8 @@ public struct HorizontalGeometryReader: View { } ) .onPreferenceChange(WidthPreferenceKey.self) { width in - // The `onPreferenceChange` view modfier now takes a `@Sendable` closure, therefore we cannot capture `@MainActor` isolated properties - // on the `View` directly anymore: https://developer.apple.com/documentation/swiftui/view/onpreferencechange(_:perform:)?changes=latest_minor - // However, as the `@Sendable` closure is still run on the MainActor (at least in my testing on 18.2 RC SDKs), we can use `MainActor.assumeIsolated` - // to avoid scheduling a `MainActor` `Task`, which could delay execution and cause unexpected UI behavior. - if Thread.isMainThread { - MainActor.assumeIsolated { - self.width = width - } - } else { - Task { @MainActor in - self.width = width - } + runOrScheduleOnMainActor { + self.width = width } } } diff --git a/Tests/SpeziViewsTests/SnapshotTests.swift b/Tests/SpeziViewsTests/SnapshotTests.swift index ee69f18..61ad157 100644 --- a/Tests/SpeziViewsTests/SnapshotTests.swift +++ b/Tests/SpeziViewsTests/SnapshotTests.swift @@ -20,8 +20,7 @@ final class SnapshotTests: XCTestCase { Text(verbatim: "20 °C, Sunny") } } - - + let largeRow = row .dynamicTypeSize(.accessibility3) diff --git a/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testListRow.ipad-XA3.png b/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testListRow.ipad-XA3.png index 0e66f85..835cb4c 100644 Binary files a/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testListRow.ipad-XA3.png and b/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testListRow.ipad-XA3.png differ diff --git a/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testListRow.iphone-XA3.png b/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testListRow.iphone-XA3.png index ad0a6e3..a634320 100644 Binary files a/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testListRow.iphone-XA3.png and b/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testListRow.iphone-XA3.png differ diff --git a/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testListRow.iphone-regular.png b/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testListRow.iphone-regular.png index ed72d16..5a9b287 100644 Binary files a/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testListRow.iphone-regular.png and b/Tests/SpeziViewsTests/__Snapshots__/SnapshotTests/testListRow.iphone-regular.png differ diff --git a/Tests/UITests/TestApp/ViewsTests/CanvasTestView.swift b/Tests/UITests/TestApp/ViewsTests/CanvasTestView.swift index 4ebd6bc..b874bb8 100644 --- a/Tests/UITests/TestApp/ViewsTests/CanvasTestView.swift +++ b/Tests/UITests/TestApp/ViewsTests/CanvasTestView.swift @@ -8,6 +8,7 @@ #if canImport(PencilKit) && !os(macOS) import PencilKit +import SpeziFoundation import SpeziViews import SwiftUI @@ -47,15 +48,8 @@ struct CanvasTestView: View { } .navigationBarTitleDisplayMode(.inline) .onPreferenceChange(CanvasView.CanvasSizePreferenceKey.self) { size in - // See `HorizontalGeometryReader.swift` - if Thread.isMainThread { - MainActor.assumeIsolated { - self.receivedSize = size - } - } else { - Task { @MainActor in - self.receivedSize = size - } + runOrScheduleOnMainActor { + self.receivedSize = size } } } diff --git a/Tests/UITests/TestAppUITests/SpeziViews/ViewsTests.swift b/Tests/UITests/TestAppUITests/SpeziViews/ViewsTests.swift index f51926c..b7669be 100644 --- a/Tests/UITests/TestAppUITests/SpeziViews/ViewsTests.swift +++ b/Tests/UITests/TestAppUITests/SpeziViews/ViewsTests.swift @@ -34,9 +34,9 @@ final class ViewsTests: XCTestCase { #if os(visionOS) // visionOS doesn't have the image anymore, this should be enough to check - let paletteView = app.scrollViews.otherElements["Pen, black"] + let penView = app.scrollViews.otherElements["Pen, black"] #else - let paletteView = app.images["palette_tool_pencil_base"] + let penView = app.buttons["Pen"] #endif @@ -44,7 +44,7 @@ final class ViewsTests: XCTestCase { app.collectionViews.buttons["Canvas"].tap() XCTAssert(app.staticTexts["Did Draw Anything: false"].waitForExistence(timeout: 2)) - XCTAssertFalse(paletteView.exists) + XCTAssertFalse(penView.exists) let canvasView = app.scrollViews.firstMatch canvasView.swipeRight() @@ -55,7 +55,7 @@ final class ViewsTests: XCTestCase { XCTAssert(app.buttons["Show Tool Picker"].waitForExistence(timeout: 2)) app.buttons["Show Tool Picker"].tap() - XCTAssertTrue(paletteView.waitForExistence(timeout: 5)) + XCTAssertTrue(penView.waitForExistence(timeout: 5)) canvasView.swipeLeft() XCTAssertTrue(canvasView.waitForExistence(timeout: 2.0)) @@ -65,12 +65,7 @@ final class ViewsTests: XCTestCase { return // the pencilKit toolbar cannot be hidden anymore on visionOS #endif -#if compiler(>=6) - XCTAssertTrue(paletteView.waitForNonExistence(timeout: 15)) -#else - sleep(15) // waitForExistence will otherwise return immediately - XCTAssertFalse(paletteView.exists) -#endif + XCTAssertTrue(penView.waitForNonExistence(timeout: 15)) canvasView.swipeUp() }