Skip to content

Commit

Permalink
Support Strict Concurrency (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
Supereg authored Aug 29, 2024
1 parent 08ab029 commit bb32d10
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 34 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ concurrency:
cancel-in-progress: true

on:
push:
branches:
- main
pull_request:
workflow_dispatch:
workflow_call:
Expand Down
11 changes: 10 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,14 @@ DerivedData

.DS_Store
*.pyc
.swiftpm

# Swift Package Manager
Package.resolved
*.xcodeproj
.swiftpm
.build
.swiftpm
.xcodebuild
.derivedData
coverage.lcov
*.xcresult
35 changes: 34 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -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: [
Expand All @@ -14,6 +23,7 @@ let package = Package(
.library(name: "ResearchKitActiveTask", targets: ["ResearchKitActiveTask"]),
.library(name: "ResearchKitSwiftUI", targets: ["ResearchKitSwiftUI"])
],
dependencies: [] + swiftLintPackage(),
targets: [
.binaryTarget(
name: "ResearchKit",
Expand All @@ -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 {
[]
}
}
3 changes: 3 additions & 0 deletions Sources/ResearchKitSwiftUI/CancelBehavior.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ public enum CancelBehavior {
/// Cancel button without a confirmation dialog.
case cancel
}


extension CancelBehavior: Sendable, Hashable {}
12 changes: 9 additions & 3 deletions Sources/ResearchKitSwiftUI/ORKLifecycle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}


Expand Down
69 changes: 40 additions & 29 deletions Sources/ResearchKitSwiftUI/ORKOrderedTaskView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Check warning on line 58 in Sources/ResearchKitSwiftUI/ORKOrderedTaskView.swift

View workflow job for this annotation

GitHub Actions / Build and Test iOS / Test using xcodebuild or run fastlane

stored property 'result' within struct cannot have a global actor; this is an error in Swift 6

Check warning on line 58 in Sources/ResearchKitSwiftUI/ORKOrderedTaskView.swift

View workflow job for this annotation

GitHub Actions / Build and Test iOS / Test using xcodebuild or run fastlane

stored property 'result' within struct cannot have a global actor; this is an error in Swift 6
}
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)
Expand All @@ -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)")
}
}
}
}
Expand Down Expand Up @@ -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)
}
Expand Down
1 change: 1 addition & 0 deletions Tests/UITests/TestAppUITests/ORKOrderedTaskViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class ORKOrderedTaskViewTests: XCTestCase {
}


@MainActor
func testStroopTest() throws {
let app = XCUIApplication()
app.launch()
Expand Down
3 changes: 3 additions & 0 deletions Tests/UITests/UITests.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit bb32d10

Please sign in to comment.