diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 5645dcd..049ad6d 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -11,12 +11,21 @@ on: jobs: build: - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - name: Build - run: swift build -v - - name: Run tests - run: swift test -v + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Swift + uses: swift-actions/setup-swift@v2 + with: + swift-version: "6" + + - name: Get swift version + run: swift --version + + - name: Build + run: swift build -v + + - name: Run tests + run: swift test -v diff --git a/Package.swift b/Package.swift index 904c20e..bcef3be 100644 --- a/Package.swift +++ b/Package.swift @@ -1,16 +1,9 @@ -// swift-tools-version: 5.5 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription -let swiftSettings: [SwiftSetting] = [ - // Use for development to catch concurrency issues. SPM packages cannot depend on other packages that use unsafeFlags. -// SwiftSetting.unsafeFlags([ -// "-Xfrontend", "-strict-concurrency=complete", -// "-Xfrontend", "-warn-concurrency", -// "-Xfrontend", "-enable-actor-data-race-checks", -// ]) -] +let swiftSettings: [SwiftSetting] = [ ] let package = Package( name: "FlowPilot", @@ -54,5 +47,6 @@ let package = Package( dependencies: ["FlowPilot"], swiftSettings: swiftSettings ), - ] + ], + swiftLanguageModes: [.v5, .v6] ) diff --git a/Package@swift-5.5.swift b/Package@swift-5.5.swift new file mode 100644 index 0000000..f81e6df --- /dev/null +++ b/Package@swift-5.5.swift @@ -0,0 +1,59 @@ +// swift-tools-version: 5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let swiftSettings: [SwiftSetting] = [ + // Use for development to catch concurrency issues. SPM packages cannot depend on other packages that use unsafeFlags. +// SwiftSetting.unsafeFlags([ +// "-Xfrontend", "-strict-concurrency=complete", +// "-Xfrontend", "-warn-concurrency", +// "-Xfrontend", "-enable-actor-data-race-checks", +// ]) +] + +let package = Package( + name: "FlowPilot", + platforms: [ + .iOS(.v13) + ], + products: [ + .library( + name: "FlowPilot", + targets: ["FlowPilot"]), + .library(name: "FlowPilotFloatingRouters", targets: ["FlowPilotFloatingRouters"]), + .library(name: "FlowPilotLegacyCombineCoordinators", targets: ["FlowPilotLegacyCombineCoordinators"]) + ], + dependencies: [ + .package(url: "https://github.com/cleevio/CleevioCore.git", .upToNextMajor(from: Version(2,0,0))), + .package(url: "https://github.com/scenee/FloatingPanel", .upToNextMajor(from: Version(2,6,1))), + .package(url: "https://github.com/apple/swift-collections", .upToNextMajor(from: Version(1,0,0))) + ], + targets: [ + .target( + name: "FlowPilot", + dependencies: [ + .product(name: "CleevioCore", package: "CleevioCore"), + .product(name: "OrderedCollections", package: "swift-collections") + ], + swiftSettings: swiftSettings + ), + .target(name: "FlowPilotFloatingRouters", dependencies: [ + "FlowPilot", + .product(name: "FloatingPanel", package: "FloatingPanel", condition: .when(platforms: [.iOS, .macCatalyst])) + ], + swiftSettings: swiftSettings + ), + .target(name: "FlowPilotLegacyCombineCoordinators", dependencies: [ + "FlowPilot", + .product(name: "CleevioCore", package: "CleevioCore") + ] + ), + .testTarget( + name: "FlowPilotTests", + dependencies: ["FlowPilot"], + swiftSettings: swiftSettings + ), + ], + swiftLanguageModes: [.v5, .v6] +) diff --git a/Sources/FlowPilot/Coordinators/Coordinator.swift b/Sources/FlowPilot/Coordinators/Coordinator.swift index 50650d9..c2bc0bd 100644 --- a/Sources/FlowPilot/Coordinators/Coordinator.swift +++ b/Sources/FlowPilot/Coordinators/Coordinator.swift @@ -80,7 +80,7 @@ open class Coordinator: CoordinatorEventDelegate { #if canImport(UIKit) final public var options: Options = .default - public struct Options: OptionSet { + public struct Options: OptionSet, Sendable { public let rawValue: UInt8 public init(rawValue: UInt8) { @@ -118,9 +118,8 @@ open class Coordinator: CoordinatorEventDelegate { deinit { let typeOfSelf = Self.self - let identifier = self.identifier - - Task { @MainActor [eventDelegate] in + + Task { @MainActor [eventDelegate, identifier] in eventDelegate?.onDeinit(of: typeOfSelf, identifier: identifier) } } diff --git a/Sources/FlowPilot/Coordinators/ResponseRouterCoordinator.swift b/Sources/FlowPilot/Coordinators/ResponseRouterCoordinator.swift index 65315c4..5da0a33 100644 --- a/Sources/FlowPilot/Coordinators/ResponseRouterCoordinator.swift +++ b/Sources/FlowPilot/Coordinators/ResponseRouterCoordinator.swift @@ -21,7 +21,7 @@ import Combine /// - `deinit` /// Sends a failure due to cancellation when the handler is deinitialized. @available(macOS 10.15, *) -open class ResponseHandler { +open class ResponseHandler: @unchecked Sendable { private let responseStream = PassthroughSubject() private var cancellable: AnyCancellable? @@ -33,16 +33,17 @@ open class ResponseHandler { } deinit { - self.handleResult(.failure(CancellationError())) + Task { @MainActor [responseStream] in + Self.handleResult(.failure(CancellationError()), on: responseStream) + } } /// Asynchronously retrieves a response value or throws an error if encountered. /// - Throws: An error if the response stream completes with a failure. /// - Returns: The response value upon successful completion. public func response() async throws -> Response { - try await withUnsafeThrowingContinuation { continuation in - cancellable = - responseStream + try await withUnsafeThrowingContinuation { [responseStream] continuation in + self.cancellable = responseStream .first() .sink { completion in guard case .failure(let error) = completion else { return } @@ -53,9 +54,13 @@ open class ResponseHandler { } } + public func handleResult(_ result: Result) { + Self.handleResult(result, on: responseStream) + } + /// Processes a result, either sending a success value or a failure completion to the stream. /// - Parameter result: A result value of type `Result`. - public func handleResult(_ result: Result) { + private static func handleResult(_ result: Result, on responseStream: PassthroughSubject) { switch result { case let .success(response): responseStream.send(response) @@ -128,10 +133,12 @@ public protocol ResponseRoutingDelegate: AnyObject { @MainActor open class ResponseRouterCoordinator: RouterCoordinator, ResponseRoutingDelegate { /// An optional closure that takes a `Result` to handle the result of a response. - public var onResponse: ((Result) -> Void)? + public var onResponse: (@Sendable @MainActor (Result) -> Void)? deinit { - onResponse?(.failure(CancellationError())) + Task { @MainActor [onResponse] in + onResponse?(.failure(CancellationError())) + } } /// Sends a successful response to the `onResponse` closure. diff --git a/Sources/FlowPilot/Coordinators/RouterCoordinator.swift b/Sources/FlowPilot/Coordinators/RouterCoordinator.swift index df1de03..91cd40a 100644 --- a/Sources/FlowPilot/Coordinators/RouterCoordinator.swift +++ b/Sources/FlowPilot/Coordinators/RouterCoordinator.swift @@ -32,13 +32,13 @@ open class RouterCoordinator: Coordinator { } @inlinable - open func coordinate(to coordinator: ResponseRouterCoordinator, animated: Bool = true) + open func coordinate(to coordinator: sending ResponseRouterCoordinator, animated: Bool = true) -> ResponseHandler { let responseHandler = ResponseHandler() - coordinator.onResponse = { result in - responseHandler.handleResult(result) + coordinator.onResponse = { [weak responseHandler] result in + responseHandler?.handleResult(result) } super.coordinate( diff --git a/Sources/FlowPilot/shouldAnimateTransition.swift b/Sources/FlowPilot/shouldAnimateTransition.swift index 2721f06..458d63d 100644 --- a/Sources/FlowPilot/shouldAnimateTransition.swift +++ b/Sources/FlowPilot/shouldAnimateTransition.swift @@ -8,6 +8,7 @@ #if canImport(UIKit) import UIKit @inlinable +@MainActor public func shouldAnimateTransition(preference: Bool, respectsUserReduceMotion: Bool) -> Bool { preference && (respectsUserReduceMotion ? !UIAccessibility.isReduceMotionEnabled : true) } diff --git a/Sources/FlowPilotFloatingRouters/FloatingPanelDelegate.swift b/Sources/FlowPilotFloatingRouters/FloatingPanelDelegate.swift index e7de34c..0a2dde2 100644 --- a/Sources/FlowPilotFloatingRouters/FloatingPanelDelegate.swift +++ b/Sources/FlowPilotFloatingRouters/FloatingPanelDelegate.swift @@ -33,7 +33,7 @@ open class FloatingPanelDelegate { } } -extension FloatingPanelDelegate: FloatingPanelControllerDelegate { +extension FloatingPanelDelegate: @preconcurrency FloatingPanelControllerDelegate { @MainActor public func floatingPanelDidChangeState(_ fpc: FloatingPanel.FloatingPanelController) { handleFloatingPanelState(fpc.state) } diff --git a/Sources/FlowPilotLegacyCombineCoordinators/Router+.swift b/Sources/FlowPilotLegacyCombineCoordinators/Router+.swift index b0c78bd..2027963 100644 --- a/Sources/FlowPilotLegacyCombineCoordinators/Router+.swift +++ b/Sources/FlowPilotLegacyCombineCoordinators/Router+.swift @@ -8,7 +8,7 @@ import UIKit import Combine import CleevioCore -import FlowPilot +@preconcurrency import FlowPilot @available(*, deprecated, message: "It is expected to use new coordinators from now on") public enum RouterResult { @@ -54,16 +54,17 @@ extension RouterResult: Equatable { @available(*, deprecated, message: "It is expected to use new coordinators from now on") public protocol LegacyRouter: AnyObject, DismissHandler { - func present(_ viewController: UIViewController, animated: Bool) - func dismiss(animated: Bool, completion: (() -> Void)?) - func dismiss(animated: Bool, returning result: RouterResult) -> AnyPublisher, Never> + @MainActor func present(_ viewController: UIViewController, animated: Bool) + @MainActor func dismiss(animated: Bool, completion: (() -> Void)?) + @MainActor func dismiss(animated: Bool, returning result: RouterResult) -> AnyPublisher, Never> } public extension LegacyRouter { + @MainActor func dismiss(animated: Bool, returning result: RouterResult) -> AnyPublisher, Never> { Future { [weak self] promise in - self?.dismiss(animated: animated) { - promise(.success(result)) + Task { @MainActor in + self?.dismiss(animated: animated) { promise(.success(result)) } } } .eraseToAnyPublisher()