Skip to content

Commit

Permalink
Minor Strict Concurrency fix (#41)
Browse files Browse the repository at this point in the history
# Minor Strict Concurrency fix

## ♻️ Current situation & Problem
This PR adds a missing Sendable conformance.


## ⚙️ Release Notes 
* Add missing Sendable conformance.
* Allow arbitrary Regex Output types with ValidationRules (e.g., allows
to pass Regex instances built with RegexBuilder).
* Don't require a `ValidationEngine` in the environment with
`VerifiableTextField` for greater flexibility.


## 📚 Documentation
--


## ✅ Testing
--


## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
Supereg authored Aug 6, 2024
1 parent ff61e65 commit 4a8245e
Show file tree
Hide file tree
Showing 27 changed files with 103 additions and 54 deletions.
23 changes: 22 additions & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ jobs:
scheme: SpeziViews-Package
resultBundle: SpeziViews-iOS.xcresult
artifactname: SpeziViews-iOS.xcresult
buildandtest_ios_latest:
name: Build and Test Swift Package iOS Latest
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
with:
runsonlabels: '["macOS", "self-hosted"]'
scheme: SpeziViews-Package
xcodeversion: latest
swiftVersion: 6
resultBundle: SpeziViews-iOS-Latest.xcresult
artifactname: SpeziViews-iOS-Latest.xcresult
buildandtest_watchos:
name: Build and Test Swift Package watchOS
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
Expand Down Expand Up @@ -69,14 +79,25 @@ jobs:
scheme: TestApp
resultBundle: TestApp-iOS.xcresult
artifactname: TestApp-iOS.xcresult
buildandtestuitests_ios_latest:
name: Build and Test UI Tests iOS Latest
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
with:
runsonlabels: '["macOS", "self-hosted"]'
path: Tests/UITests
scheme: TestApp
xcodeversion: latest
swiftVersion: 6
resultBundle: TestApp-iOS-Latest.xcresult
artifactname: TestApp-iOS-Latest.xcresult
buildandtestuitests_ipad:
name: Build and Test UI Tests iPadOS
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
with:
runsonlabels: '["macOS", "self-hosted"]'
path: Tests/UITests
scheme: TestApp
destination: 'platform=iOS Simulator,name=iPad Air (5th generation)'
destination: 'platform=iOS Simulator,name=iPad Pro 11-inch (M4)'
resultBundle: TestApp-iPad.xcresult
artifactname: TestApp-iPad.xcresult
buildandtestuitests_visionos:
Expand Down
6 changes: 3 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import PackageDescription


#if swift(<6)
let swiftConcurrency: SwiftSetting = .enableExperimentalFeature("SwiftConcurrency")
let swiftConcurrency: SwiftSetting = .enableExperimentalFeature("StrictConcurrency")
#else
let swiftConcurrency: SwiftSetting = .enableUpcomingFeature("SwiftConcurrency")
let swiftConcurrency: SwiftSetting = .enableUpcomingFeature("StrictConcurrency")
#endif


Expand All @@ -37,7 +37,7 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/StanfordSpezi/Spezi.git", from: "1.3.0"),
.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.15.3")
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.17.0")
] + swiftLintPackage(),
targets: [
.target(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ extension ValidationEngine {
}


extension ValidationEngine.Configuration: Sendable {}


extension EnvironmentValues {
/// Access the ``ValidationEngine/Configuration-swift.struct`` of a ValidationEngine through the environment.
///
Expand Down
1 change: 1 addition & 0 deletions Sources/SpeziValidation/ValidationEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ public class ValidationEngine: Identifiable {
computeValidation(input: input, source: .manual)
}

@MainActor
private func debounce(_ task: @escaping () -> Void) {
debounceTask = Task {
try? await Task.sleep(for: debounceDuration)
Expand Down
20 changes: 12 additions & 8 deletions Sources/SpeziValidation/ValidationRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,11 @@ enum CascadingValidationEffect {
/// - ``minimalPassword``
/// - ``mediumPassword``
/// - ``strongPassword``
public struct ValidationRule: Identifiable, @unchecked Sendable, Equatable {
public struct ValidationRule: Identifiable, Sendable, Equatable {
// we guarantee that the closure is only executed on the main thread
/// A unique identifier for the ``ValidationRule``. Can be used to, e.g., match a ``FailedValidationResult`` to the ValidationRule.
public let id: UUID
private let rule: (String) -> Bool
private let rule: @Sendable (String) -> Bool
/// A localized message that describes a recovery suggestion if the validation rule fails.
public let message: LocalizedStringResource

Check warning on line 72 in Sources/SpeziValidation/ValidationRule.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package visionOS / Test using xcodebuild or run fastlane

stored property 'message' of 'Sendable'-conforming struct 'ValidationRule' has non-sendable type 'LocalizedStringResource'

Check warning on line 72 in Sources/SpeziValidation/ValidationRule.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package macOS / Test using xcodebuild or run fastlane

stored property 'message' of 'Sendable'-conforming struct 'ValidationRule' has non-sendable type 'LocalizedStringResource'

Check warning on line 72 in Sources/SpeziValidation/ValidationRule.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package macOS / Test using xcodebuild or run fastlane

stored property 'message' of 'Sendable'-conforming struct 'ValidationRule' has non-sendable type 'LocalizedStringResource'

Check warning on line 72 in Sources/SpeziValidation/ValidationRule.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package watchOS / Test using xcodebuild or run fastlane

stored property 'message' of 'Sendable'-conforming struct 'ValidationRule' has non-sendable type 'LocalizedStringResource'

Check warning on line 72 in Sources/SpeziValidation/ValidationRule.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package watchOS / Test using xcodebuild or run fastlane

stored property 'message' of 'Sendable'-conforming struct 'ValidationRule' has non-sendable type 'LocalizedStringResource'

Check warning on line 72 in Sources/SpeziValidation/ValidationRule.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package tvOS / Test using xcodebuild or run fastlane

stored property 'message' of 'Sendable'-conforming struct 'ValidationRule' has non-sendable type 'LocalizedStringResource'

Check warning on line 72 in Sources/SpeziValidation/ValidationRule.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package tvOS / Test using xcodebuild or run fastlane

stored property 'message' of 'Sendable'-conforming struct 'ValidationRule' has non-sendable type 'LocalizedStringResource'

Check warning on line 72 in Sources/SpeziValidation/ValidationRule.swift

View workflow job for this annotation

GitHub Actions / Build and Test UI Tests visionOS / Test using xcodebuild or run fastlane

stored property 'message' of 'Sendable'-conforming struct 'ValidationRule' has non-sendable type 'LocalizedStringResource'
let effect: CascadingValidationEffect
Expand All @@ -76,7 +76,7 @@ public struct ValidationRule: Identifiable, @unchecked Sendable, Equatable {
// swiftlint:disable:next function_default_parameter_at_end
init(
id: UUID = UUID(),
ruleClosure: @escaping (String) -> Bool,
ruleClosure: @escaping @Sendable (String) -> Bool,
message: LocalizedStringResource,
effect: CascadingValidationEffect = .continue
) {
Expand All @@ -92,7 +92,7 @@ public struct ValidationRule: Identifiable, @unchecked Sendable, Equatable {
/// - Parameters:
/// - rule: An escaping closure that validates a `String` and returns a boolean result.
/// - message: A `String` message to display if validation fails.
public init(rule: @escaping (String) -> Bool, message: LocalizedStringResource) {
public init(rule: @escaping @Sendable (String) -> Bool, message: LocalizedStringResource) {
self.init(ruleClosure: rule, message: message)
}

Expand All @@ -102,7 +102,7 @@ public struct ValidationRule: Identifiable, @unchecked Sendable, Equatable {
/// - rule: An escaping closure that validates a `String` and returns a boolean result.
/// - message: A `String` message to display if validation fails.
/// - bundle: The Bundle to localize for.
public init(rule: @escaping (String) -> Bool, message: String.LocalizationValue, bundle: Bundle) {
public init(rule: @escaping @Sendable (String) -> Bool, message: String.LocalizationValue, bundle: Bundle) {
self.init(ruleClosure: rule, message: LocalizedStringResource(message, bundle: .atURL(from: bundle)))
}

Expand All @@ -111,8 +111,12 @@ public struct ValidationRule: Identifiable, @unchecked Sendable, Equatable {
/// - Parameters:
/// - regex: A `Regex` regular expression to match for validating text. Note, the `wholeMatch` operation is used.
/// - message: A `LocalizedStringResource` message to display if validation fails.
public init(regex: Regex<AnyRegexOutput>, message: LocalizedStringResource) {
self.init(ruleClosure: { (try? regex.wholeMatch(in: $0) != nil) ?? false }, message: message)
public init<Output>(regex: Regex<Output>, message: LocalizedStringResource) {
// Regex might not be Sendable, depending how it was constructed (e.g., might capture a non-Sendable transform closure).
// This is still an issue that is actively discussed https://forums.swift.org/t/should-regex-be-sendable/69529
// so we are ignoring it for now.
nonisolated(unsafe) let regexTmp = regex
self.init(ruleClosure: { ( try? regexTmp.wholeMatch(in: $0) != nil) ?? false }, message: message)

Check warning on line 119 in Sources/SpeziValidation/ValidationRule.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package macOS / Test using xcodebuild or run fastlane

capture of 'regexTmp' with non-sendable type 'Regex<Output>' in a `@Sendable` closure

Check warning on line 119 in Sources/SpeziValidation/ValidationRule.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package watchOS / Test using xcodebuild or run fastlane

capture of 'regexTmp' with non-sendable type 'Regex<Output>' in a `@Sendable` closure

Check warning on line 119 in Sources/SpeziValidation/ValidationRule.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package tvOS / Test using xcodebuild or run fastlane

capture of 'regexTmp' with non-sendable type 'Regex<Output>' in a `@Sendable` closure
}

/// Creates a validation rule from a regular expression.
Expand All @@ -121,7 +125,7 @@ public struct ValidationRule: Identifiable, @unchecked Sendable, Equatable {
/// - regex: A `Regex` regular expression to match for validating text. Note, the `wholeMatch` operation is used.
/// - message: A `String` message to display if validation fails.
/// - bundle: The Bundle to localize for.
public init(regex: Regex<AnyRegexOutput>, message: String.LocalizationValue, bundle: Bundle) {
public init<Output>(regex: Regex<Output>, message: String.LocalizationValue, bundle: Bundle) {
self.init(regex: regex, message: LocalizedStringResource(message, bundle: .atURL(from: bundle)))
}

Expand Down
8 changes: 5 additions & 3 deletions Sources/SpeziValidation/Views/VerifiableTextField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public struct VerifiableTextField<FieldLabel: View, FieldFooter: View>: View {
@Binding private var text: String

@Environment(ValidationEngine.self)
var validationEngine
var validationEngine: ValidationEngine?

public var body: some View {
VStack {
Expand All @@ -44,9 +44,11 @@ public struct VerifiableTextField<FieldLabel: View, FieldFooter: View>: View {
}

HStack {
ValidationResultsView(results: validationEngine.displayedValidationResults)
if let validationEngine {
ValidationResultsView(results: validationEngine.displayedValidationResults)

Spacer()
Spacer()
}

textFieldFooter
}
Expand Down
14 changes: 7 additions & 7 deletions Sources/SpeziViews/Views/Button/AsyncButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ enum AsyncButtonState {
@MainActor
public struct AsyncButton<Label: View>: View {
private let role: ButtonRole?
private let action: () async throws -> Void
private let label: () -> Label
private let action: @MainActor () async throws -> Void
private let label: Label

@Environment(\.defaultErrorDescription)
var defaultErrorDescription
Expand All @@ -58,7 +58,7 @@ public struct AsyncButton<Label: View>: View {

public var body: some View {
Button(role: role, action: submitAction) {
label()
label
.processingOverlay(isProcessing: buttonState == .disabledAndProcessing || externallyProcessing)
}
.disabled(buttonState != .idle || externallyProcessing)
Expand Down Expand Up @@ -106,11 +106,11 @@ public struct AsyncButton<Label: View>: View {
public init(
role: ButtonRole? = nil,
action: @escaping () async -> Void,
@ViewBuilder label: @escaping () -> Label
@ViewBuilder label: () -> Label
) {
self.role = role
self.action = action
self.label = label
self.label = label()
self._viewState = .constant(.idle)
}

Expand Down Expand Up @@ -162,12 +162,12 @@ public struct AsyncButton<Label: View>: View {
role: ButtonRole? = nil,
state: Binding<ViewState>,
action: @escaping () async throws -> Void,
@ViewBuilder label: @escaping () -> Label
@ViewBuilder label: () -> Label
) {
self.role = role
self._viewState = state
self.action = action
self.label = label
self.label = label()
}


Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziViews/Views/Drawing/CanvasView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ public struct CanvasView: View {
/// The ``CanvasSizePreferenceKey`` enables outer views to get access to the current canvas size of the ``CanvasView``
/// using the SwiftUI preference mechanisms.
public struct CanvasSizePreferenceKey: PreferenceKey, Equatable {
public static var defaultValue: CGSize = .zero
public static let defaultValue: CGSize = .zero


public static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import SwiftUI
/// Enables outer views to get access to the current width calculated by the ``HorizontalGeometryReader``
/// using the SwiftUI preference mechanisms.
public struct WidthPreferenceKey: PreferenceKey, Equatable {
public static var defaultValue: CGFloat = 0
public static let defaultValue: CGFloat = 0


public static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { }
Expand Down
4 changes: 4 additions & 0 deletions Tests/SpeziViewsTests/SnapshotTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import XCTest


final class SnapshotTests: XCTestCase {
@MainActor
func testListRow() {
let row = List {
ListRow(verbatim: "San Francisco") {
Expand All @@ -33,6 +34,7 @@ final class SnapshotTests: XCTestCase {
#endif
}

@MainActor
func testReverseLabelStyle() {
let label = SwiftUI.Label("100 %", image: "battery.100")
.labelStyle(.reverse)
Expand All @@ -43,9 +45,11 @@ final class SnapshotTests: XCTestCase {
#endif
}

@MainActor
func testDismissButton() {
let dismissButton = DismissButton()


#if os(iOS)
assertSnapshot(of: dismissButton, as: .image(layout: .device(config: .iPhone13Pro)), named: "iphone-regular")
assertSnapshot(of: dismissButton, as: .image(layout: .device(config: .iPadPro11)), named: "ipad-regular")
Expand Down
4 changes: 2 additions & 2 deletions Tests/SpeziViewsTests/SpeziValidationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import XCTest

final class SpeziValidationTests: XCTestCase {
@MainActor
func testValidationDebounce() {
func testValidationDebounce() async throws {
let engine = ValidationEngine(rules: .nonEmpty)

engine.submit(input: "Valid")
Expand All @@ -23,7 +23,7 @@ final class SpeziValidationTests: XCTestCase {
XCTAssertTrue(engine.inputValid)
XCTAssertEqual(engine.validationResults, [])

sleep(1)
try await Task.sleep(for: .seconds(1))
XCTAssertFalse(engine.inputValid)
XCTAssertEqual(engine.validationResults.count, 1)

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ enum SpeziPersonalInfoTests: String, TestAppTests {
case userProfile = "User Profile"

@ViewBuilder
@MainActor
private var nameFields: some View {
NameFieldsTestView()
}

@MainActor
@ViewBuilder
private var userProfile: some View {
UserProfileView(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ struct OperationStateTestView: View {
}
.task {
operationState = .someOperationStep
try? await Task.sleep(for: .seconds(10))
try? await Task.sleep(for: .seconds(2))
operationState = .error(
AnyLocalizedError(
error: testError,
Expand Down
12 changes: 12 additions & 0 deletions Tests/UITests/TestApp/ViewsTests/SpeziViewsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,21 @@ enum SpeziViewsTests: String, TestAppTests {

#if canImport(PencilKit) && !os(macOS)
@ViewBuilder
@MainActor
private var canvas: some View {
CanvasTestView()
}
#endif

@ViewBuilder
@MainActor
private var geometryReader: some View {
GeometryReaderTestView()
}

#if !os(macOS)
@ViewBuilder
@MainActor
private var label: some View {
Label(
"LABEL_TEXT",
Expand All @@ -65,11 +68,13 @@ enum SpeziViewsTests: String, TestAppTests {
#endif

@ViewBuilder
@MainActor
private var markdownView: some View {
MarkdownViewTestView()
}

@ViewBuilder
@MainActor
private var lazyText: some View {
ScrollView {
LazyText(
Expand All @@ -86,36 +91,43 @@ enum SpeziViewsTests: String, TestAppTests {
}

@ViewBuilder
@MainActor
private var viewState: some View {
ViewStateTestView()
}

@ViewBuilder
@MainActor
private var operationState: some View {
OperationStateTestView()
}

@ViewBuilder
@MainActor
private var viewStateMapper: some View {
ViewStateMapperTestView()
}

@ViewBuilder
@MainActor
private var conditionalModifier: some View {
ConditionalModifierTestView()
}

@ViewBuilder
@MainActor
private var defaultErrorOnly: some View {
ViewStateTestView(testError: .init(errorDescription: "Some error occurred!"))
}

@ViewBuilder
@MainActor
private var defaultErrorDescription: some View {
DefaultErrorDescriptionTestView()
}

@ViewBuilder
@MainActor
private var asyncButton: some View {
AsyncButtonTestView()
}
Expand Down
2 changes: 1 addition & 1 deletion Tests/UITests/TestApp/ViewsTests/ViewStateMapperView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ struct ViewStateMapperTestView: View {
}
.task {
operationState = .someOperationStep
try? await Task.sleep(for: .seconds(10))
try? await Task.sleep(for: .seconds(2))
operationState = .error(
AnyLocalizedError(
error: testError,
Expand Down
Loading

0 comments on commit 4a8245e

Please sign in to comment.