Skip to content
23 changes: 16 additions & 7 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 4 additions & 10 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -54,5 +47,6 @@ let package = Package(
dependencies: ["FlowPilot"],
swiftSettings: swiftSettings
),
]
],
swiftLanguageModes: [.v5, .v6]
)
59 changes: 59 additions & 0 deletions Package@swift-5.5.swift
Original file line number Diff line number Diff line change
@@ -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]
)
7 changes: 3 additions & 4 deletions Sources/FlowPilot/Coordinators/Coordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
}
Expand Down
23 changes: 15 additions & 8 deletions Sources/FlowPilot/Coordinators/ResponseRouterCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Response> {
open class ResponseHandler<Response: Sendable>: @unchecked Sendable {
private let responseStream = PassthroughSubject<Response, Error>()
private var cancellable: AnyCancellable?

Expand All @@ -33,16 +33,17 @@ open class ResponseHandler<Response> {
}

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 }
Expand All @@ -53,9 +54,13 @@ open class ResponseHandler<Response> {
}
}

public func handleResult(_ result: Result<Response, Error>) {
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<Response, Error>`.
public func handleResult(_ result: Result<Response, Error>) {
private static func handleResult(_ result: Result<Response, Error>, on responseStream: PassthroughSubject<Response, Error>) {
switch result {
case let .success(response):
responseStream.send(response)
Expand Down Expand Up @@ -128,10 +133,12 @@ public protocol ResponseRoutingDelegate<Response>: AnyObject {
@MainActor
open class ResponseRouterCoordinator<Response>: RouterCoordinator, ResponseRoutingDelegate {
/// An optional closure that takes a `Result<Response, Error>` to handle the result of a response.
public var onResponse: ((Result<Response, Error>) -> Void)?
public var onResponse: (@Sendable @MainActor (Result<Response, Error>) -> Void)?

deinit {
onResponse?(.failure(CancellationError()))
Task { @MainActor [onResponse] in
onResponse?(.failure(CancellationError()))
}
}

/// Sends a successful response to the `onResponse` closure.
Expand Down
6 changes: 3 additions & 3 deletions Sources/FlowPilot/Coordinators/RouterCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ open class RouterCoordinator: Coordinator {
}

@inlinable
open func coordinate<Response>(to coordinator: ResponseRouterCoordinator<Response>, animated: Bool = true)
open func coordinate<Response>(to coordinator: sending ResponseRouterCoordinator<Response>, animated: Bool = true)
-> ResponseHandler<Response>
{
let responseHandler = ResponseHandler<Response>()

coordinator.onResponse = { result in
responseHandler.handleResult(result)
coordinator.onResponse = { [weak responseHandler] result in
responseHandler?.handleResult(result)
}

super.coordinate(
Expand Down
1 change: 1 addition & 0 deletions Sources/FlowPilot/shouldAnimateTransition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#if canImport(UIKit)
import UIKit
@inlinable
@MainActor
public func shouldAnimateTransition(preference: Bool, respectsUserReduceMotion: Bool) -> Bool {
preference && (respectsUserReduceMotion ? !UIAccessibility.isReduceMotionEnabled : true)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ open class FloatingPanelDelegate {
}
}

extension FloatingPanelDelegate: FloatingPanelControllerDelegate {
extension FloatingPanelDelegate: @preconcurrency FloatingPanelControllerDelegate {
@MainActor public func floatingPanelDidChangeState(_ fpc: FloatingPanel.FloatingPanelController) {
handleFloatingPanelState(fpc.state)
}
Expand Down
13 changes: 7 additions & 6 deletions Sources/FlowPilotLegacyCombineCoordinators/Router+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
Expand Down Expand Up @@ -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<T>(animated: Bool, returning result: RouterResult<T>) -> AnyPublisher<RouterResult<T>, Never>
@MainActor func present(_ viewController: UIViewController, animated: Bool)
@MainActor func dismiss(animated: Bool, completion: (() -> Void)?)
@MainActor func dismiss<T>(animated: Bool, returning result: RouterResult<T>) -> AnyPublisher<RouterResult<T>, Never>
}

public extension LegacyRouter {
@MainActor
func dismiss<T>(animated: Bool, returning result: RouterResult<T>) -> AnyPublisher<RouterResult<T>, Never> {
Future { [weak self] promise in
self?.dismiss(animated: animated) {
promise(.success(result))
Task { @MainActor in
self?.dismiss(animated: animated) { promise(.success(result)) }
}
}
.eraseToAnyPublisher()
Expand Down