From bb32d100eee6da94e07f30bc32d4457da87a4028 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Thu, 29 Aug 2024 17:15:26 +0200 Subject: [PATCH] Support Strict Concurrency (#30) --- .github/workflows/build_and_test.yml | 3 + .gitignore | 11 ++- Package.swift | 35 +++++++++- .../ResearchKitSwiftUI/CancelBehavior.swift | 3 + Sources/ResearchKitSwiftUI/ORKLifecycle.swift | 12 +++- .../ORKOrderedTaskView.swift | 69 +++++++++++-------- .../ORKOrderedTaskViewTests.swift | 1 + .../UITests/UITests.xcodeproj/project.pbxproj | 3 + 8 files changed, 103 insertions(+), 34 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index ec00b46a6..28103957b 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -13,6 +13,9 @@ concurrency: cancel-in-progress: true on: + push: + branches: + - main pull_request: workflow_dispatch: workflow_call: diff --git a/.gitignore b/.gitignore index 80f309da4..67633896a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,5 +19,14 @@ DerivedData .DS_Store *.pyc +.swiftpm + +# Swift Package Manager +Package.resolved +*.xcodeproj +.swiftpm .build -.swiftpm \ No newline at end of file +.xcodebuild +.derivedData +coverage.lcov +*.xcresult diff --git a/Package.swift b/Package.swift index f7d199dd7..ae57bb6f3 100644 --- a/Package.swift +++ b/Package.swift @@ -1,7 +1,16 @@ // swift-tools-version:5.10 + +import class Foundation.ProcessInfo import PackageDescription +#if swift(<6) +let swiftConcurrency: SwiftSetting = .enableExperimentalFeature("StrictConcurrency") +#else +let swiftConcurrency: SwiftSetting = .enableUpcomingFeature("StrictConcurrency") +#endif + + let package = Package( name: "ResearchKit", platforms: [ @@ -14,6 +23,7 @@ let package = Package( .library(name: "ResearchKitActiveTask", targets: ["ResearchKitActiveTask"]), .library(name: "ResearchKitSwiftUI", targets: ["ResearchKitSwiftUI"]) ], + dependencies: [] + swiftLintPackage(), targets: [ .binaryTarget( name: "ResearchKit", @@ -33,7 +43,30 @@ let package = Package( .target(name: "ResearchKit"), .target(name: "ResearchKitUI"), .target(name: "ResearchKitActiveTask", condition: .when(platforms: [.iOS])) - ] + ], + swiftSettings: [ + swiftConcurrency + ], + plugins: [] + swiftLintPlugin() ) ] ) + + +func swiftLintPlugin() -> [Target.PluginUsage] { + // Fully quit Xcode and open again with `open --env SPEZI_DEVELOPMENT_SWIFTLINT /Applications/Xcode.app` + if ProcessInfo.processInfo.environment["SPEZI_DEVELOPMENT_SWIFTLINT"] != nil { + [.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLint")] + } else { + [] + } +} + + +func swiftLintPackage() -> [PackageDescription.Package.Dependency] { + if ProcessInfo.processInfo.environment["SPEZI_DEVELOPMENT_SWIFTLINT"] != nil { + [.package(url: "https://github.com/realm/SwiftLint.git", from: "0.55.1")] + } else { + [] + } +} diff --git a/Sources/ResearchKitSwiftUI/CancelBehavior.swift b/Sources/ResearchKitSwiftUI/CancelBehavior.swift index 21b0dbfb0..002522f02 100644 --- a/Sources/ResearchKitSwiftUI/CancelBehavior.swift +++ b/Sources/ResearchKitSwiftUI/CancelBehavior.swift @@ -16,3 +16,6 @@ public enum CancelBehavior { /// Cancel button without a confirmation dialog. case cancel } + + +extension CancelBehavior: Sendable, Hashable {} diff --git a/Sources/ResearchKitSwiftUI/ORKLifecycle.swift b/Sources/ResearchKitSwiftUI/ORKLifecycle.swift index 245c08e23..40a13bd28 100644 --- a/Sources/ResearchKitSwiftUI/ORKLifecycle.swift +++ b/Sources/ResearchKitSwiftUI/ORKLifecycle.swift @@ -13,15 +13,21 @@ import SwiftUI private struct ORKWillAppearMethod: EnvironmentKey { - static let defaultValue: ((ORKTaskViewController, ORKStepViewController) -> Void)? = nil + static var defaultValue: ((ORKTaskViewController, ORKStepViewController) -> Void)? { + nil + } } private struct ORKWillDisappearMethod: EnvironmentKey { - static let defaultValue: ((ORKTaskViewController, ORKStepViewController, ORKStepViewControllerNavigationDirection) -> Void)? = nil + static var defaultValue: ((ORKTaskViewController, ORKStepViewController, ORKStepViewControllerNavigationDirection) -> Void)? { + nil + } } private struct ORKShouldPresentMethod: EnvironmentKey { - static let defaultValue: ((ORKTaskViewController, ORKStep) -> Bool)? = nil + static var defaultValue: ((ORKTaskViewController, ORKStep) -> Bool)? { + nil + } } diff --git a/Sources/ResearchKitSwiftUI/ORKOrderedTaskView.swift b/Sources/ResearchKitSwiftUI/ORKOrderedTaskView.swift index 5c4a34af5..7de3e8c88 100644 --- a/Sources/ResearchKitSwiftUI/ORKOrderedTaskView.swift +++ b/Sources/ResearchKitSwiftUI/ORKOrderedTaskView.swift @@ -52,33 +52,39 @@ import SwiftUI /// You can check the results of a `taskResult` if it contains an object of type `ORKFileResult` and save its corresponding content. /// After your async closure returns the temporary directory will be automatically deleted otherwise. public struct ORKOrderedTaskView: UIViewControllerRepresentable { - public class Coordinator: NSObject, ORKTaskViewControllerDelegate { - fileprivate var result: @MainActor (TaskResult) async -> Void + public final class Coordinator: NSObject, ORKTaskViewControllerDelegate { + @MainActor + fileprivate struct Closure { + @MainActor fileprivate var result: @MainActor (TaskResult) async -> Void + } + fileprivate var closure: Closure fileprivate var cancelBehavior: CancelBehavior fileprivate var stepWillAppear: ((ORKTaskViewController, ORKStepViewController) -> Void)? fileprivate var stepWillDisappear: ((ORKTaskViewController, ORKStepViewController, ORKStepViewControllerNavigationDirection) -> Void)? fileprivate var shouldPresentStep: ((ORKTaskViewController, ORKStep) -> Bool)? - + @MainActor init( result: @escaping @MainActor (TaskResult) async -> Void, cancelBehavior: CancelBehavior ) { - self.result = result + self.closure = Closure(result: result) self.cancelBehavior = cancelBehavior } public func taskViewControllerShouldConfirmCancel(_ taskViewController: ORKTaskViewController) -> Bool { cancelBehavior == .shouldConfirmCancel } - + public func taskViewController( _ taskViewController: ORKTaskViewController, stepViewControllerWillAppear stepViewController: ORKStepViewController ) { if cancelBehavior == .disabled { - stepViewController.cancelButtonItem = nil + MainActor.assumeIsolated { + stepViewController.cancelButtonItem = nil + } } stepWillAppear?(taskViewController, stepViewController) @@ -101,30 +107,35 @@ public struct ORKOrderedTaskView: UIViewControllerRepresentable { didFinishWith reason: ORKTaskFinishReason, error: Error? ) { - let taskResult = taskViewController.result - - _Concurrency.Task { @MainActor in - switch reason { - case .completed: - await result(.completed(taskResult)) - case .discarded, .earlyTermination: - await result(.cancelled) - case .failed: - guard let error else { - preconditionFailure("ResearchKit broke API contract. Didn't supply error when indicating task failure.") + let closure = closure // avoid self Sendable warning + + MainActor.assumeIsolated { + let taskResult = taskViewController.result + let result = closure.result + + _Concurrency.Task { @MainActor in + switch reason { + case .completed: + await result(.completed(taskResult)) + case .discarded, .earlyTermination: + await result(.cancelled) + case .failed: + guard let error else { + preconditionFailure("ResearchKit broke API contract. Didn't supply error when indicating task failure.") + } + await result(.failed(error)) + case .saved: + break // we don't support that currently + @unknown default: + break } - await result(.failed(error)) - case .saved: - break // we don't support that currently - @unknown default: - break - } - if let outputDirectory = taskViewController.outputDirectory { - do { - try FileManager.default.removeItem(at: outputDirectory) - } catch { - logger.error("Failed to delete the temporary output directory: \(error)") + if let outputDirectory = taskViewController.outputDirectory { + do { + try FileManager.default.removeItem(at: outputDirectory) + } catch { + logger.error("Failed to delete the temporary output directory: \(error)") + } } } } @@ -224,7 +235,7 @@ public struct ORKOrderedTaskView: UIViewControllerRepresentable { uiViewController.view.tintColor = UIColor(tintColor) uiViewController.delegate = context.coordinator - context.coordinator.result = result + context.coordinator.closure.result = result context.coordinator.cancelBehavior = cancelBehavior updateClosures(for: context.coordinator) } diff --git a/Tests/UITests/TestAppUITests/ORKOrderedTaskViewTests.swift b/Tests/UITests/TestAppUITests/ORKOrderedTaskViewTests.swift index cba984b1f..7ec03be54 100644 --- a/Tests/UITests/TestAppUITests/ORKOrderedTaskViewTests.swift +++ b/Tests/UITests/TestAppUITests/ORKOrderedTaskViewTests.swift @@ -17,6 +17,7 @@ class ORKOrderedTaskViewTests: XCTestCase { } + @MainActor func testStroopTest() throws { let app = XCUIApplication() app.launch() diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index 3e48a4436..dad3e9d69 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -303,6 +303,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; TVOS_DEPLOYMENT_TARGET = 17.0; WATCHOS_DEPLOYMENT_TARGET = 10.0; XROS_DEPLOYMENT_TARGET = 1.0; @@ -362,6 +363,7 @@ SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_STRICT_CONCURRENCY = complete; TVOS_DEPLOYMENT_TARGET = 17.0; VALIDATE_PRODUCT = YES; WATCHOS_DEPLOYMENT_TARGET = 10.0; @@ -547,6 +549,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = TEST; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; TVOS_DEPLOYMENT_TARGET = 17.0; WATCHOS_DEPLOYMENT_TARGET = 10.0; XROS_DEPLOYMENT_TARGET = 1.0;