Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ let package = Package(
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.3.0"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"),
.package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"603.0.0"),
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.14.0"),
],
targets: [
.target(
Expand All @@ -51,6 +52,13 @@ let package = Package(
.product(name: "Sharing", package: "swift-sharing"),
.product(name: "SwiftUINavigation", package: "swift-navigation"),
.product(name: "UIKitNavigation", package: "swift-navigation"),
.product(
name: "OpenCombine", package: "OpenCombine",
condition: .when(platforms: [
.android,
.linux,
])
),
],
resources: [
.process("Resources/PrivacyInfo.xcprivacy")
Expand Down
8 changes: 8 additions & 0 deletions Package@swift-6.0.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ let package = Package(
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.3.0"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"),
.package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"603.0.0"),
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.14.0"),
],
targets: [
.target(
Expand All @@ -51,6 +52,13 @@ let package = Package(
.product(name: "Sharing", package: "swift-sharing"),
.product(name: "SwiftUINavigation", package: "swift-navigation"),
.product(name: "UIKitNavigation", package: "swift-navigation"),
.product(
name: "OpenCombine", package: "OpenCombine",
condition: .when(platforms: [
.android,
.linux,
])
),
],
resources: [
.process("Resources/PrivacyInfo.xcprivacy")
Expand Down
15 changes: 11 additions & 4 deletions Sources/ComposableArchitecture/Core.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import Combine
import Foundation

#if canImport(Combine)
import Combine
#else
import OpenCombine
#endif

@MainActor
protocol Core<State, Action>: AnyObject, Sendable {
associatedtype State
Expand Down Expand Up @@ -101,12 +106,14 @@ final class RootCore<Root: Reducer>: Core {
switch effect.operation {
case .none:
break
case let .publisher(publisher):
case .publisher(let publisher):
var didComplete = false
let boxedTask = Box<Task<Void, Never>?>(wrappedValue: nil)
let effectCancellable = withEscapedDependencies { continuation in
publisher
.receive(on: UIScheduler.shared)
#if os(macOS) || os(iOS) || os(watchOS) || os(visionOS) || os(tvOS)
.receive(on: UIScheduler.shared)
#endif
.handleEvents(receiveCancel: { [weak self] in self?.effectCancellables[uuid] = nil })
.sink(
receiveCompletion: { [weak self] _ in
Expand Down Expand Up @@ -136,7 +143,7 @@ final class RootCore<Root: Reducer>: Core {
task.cancel()
}
}
case let .run(name, priority, operation):
case .run(let name, let priority, let operation):
withEscapedDependencies { continuation in
let task = Task(name: name, priority: priority) { @MainActor [weak self] in
let isCompleted = LockIsolated(false)
Expand Down
144 changes: 87 additions & 57 deletions Sources/ComposableArchitecture/Dependencies/Dismiss.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import SwiftUI

extension DependencyValues {
/// An effect that dismisses the current presentation.
///
Expand Down Expand Up @@ -78,70 +76,102 @@ extension DependencyValues {
public struct DismissEffect: Sendable {
var dismiss: (@MainActor @Sendable () -> Void)?

@MainActor
public func callAsFunction(
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) async {
await self.callAsFunction(
animation: nil,
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
}

@MainActor
public func callAsFunction(
animation: Animation?,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) async {
await callAsFunction(
transaction: Transaction(animation: animation),
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
}

@MainActor
public func callAsFunction(
transaction: Transaction,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) async {
guard let dismiss = self.dismiss
else {
reportIssue(
"""
A reducer requested dismissal at "\(fileID):\(line)", but couldn't be dismissed.
#if os(macOS) || os(iOS) || os(watchOS) || os(visionOS) || os(tvOS)
@MainActor
public func callAsFunction(
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) async {
await self.callAsFunction(
animation: nil,
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
}

This is generally considered an application logic error, and can happen when a reducer \
assumes it runs in a presentation context. If a reducer can run at both the root level \
of an application, as well as in a presentation destination, use \
@Dependency(\\.isPresented) to determine if the reducer is being presented before calling \
@Dependency(\\.dismiss).
""",
@MainActor
public func callAsFunction(
animation: Animation?,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) async {
await callAsFunction(
transaction: Transaction(animation: animation),
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
return
}
withTransaction(transaction) {

@MainActor
public func callAsFunction(
transaction: Transaction,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) async {
guard let dismiss = self.dismiss
else {
reportIssue(
"""
A reducer requested dismissal at "\(fileID):\(line)", but couldn't be dismissed.

This is generally considered an application logic error, and can happen when a reducer \
assumes it runs in a presentation context. If a reducer can run at both the root level \
of an application, as well as in a presentation destination, use \
@Dependency(\\.isPresented) to determine if the reducer is being presented before calling \
@Dependency(\\.dismiss).
""",
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
return
}
withTransaction(transaction) {
dismiss()
}
}
#else
@MainActor
public func callAsFunction(
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) async {
guard let dismiss = self.dismiss
else {
reportIssue(
"""
A reducer requested dismissal at "\(fileID):\(line)", but couldn't be dismissed.

This is generally considered an application logic error, and can happen when a reducer \
assumes it runs in a presentation context. If a reducer can run at both the root level \
of an application, as well as in a presentation destination, use \
@Dependency(\\.isPresented) to determine if the reducer is being presented before calling \
@Dependency(\\.dismiss).
""",
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
return
}

dismiss()
}
}
#endif
}

extension DismissEffect {
Expand Down
76 changes: 42 additions & 34 deletions Sources/ComposableArchitecture/Effect.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
@preconcurrency import Combine
import Foundation
import SwiftUI

#if canImport(Combine)
@preconcurrency import Combine
#else
@preconcurrency import OpenCombine
#endif

public struct Effect<Action>: Sendable {
@usableFromInline
Expand Down Expand Up @@ -148,20 +152,22 @@ extension Effect {
Self(operation: .publisher(Just(action).eraseToAnyPublisher()))
}

/// Initializes an effect that immediately emits the action passed in.
///
/// > Note: We do not recommend using `Effect.send` to share logic. Instead, limit usage to
/// > child-parent communication, where a child may want to emit a "delegate" action for a parent
/// > to listen to.
/// >
/// > For more information, see <doc:Performance#Sharing-logic-with-actions>.
///
/// - Parameters:
/// - action: The action that is immediately emitted by the effect.
/// - animation: An animation.
public static func send(_ action: Action, animation: Animation? = nil) -> Self {
.send(action).animation(animation)
}
#if os(macOS) || os(iOS) || os(watchOS) || os(visionOS) || os(tvOS)
/// Initializes an effect that immediately emits the action passed in.
///
/// > Note: We do not recommend using `Effect.send` to share logic. Instead, limit usage to
/// > child-parent communication, where a child may want to emit a "delegate" action for a parent
/// > to listen to.
/// >
/// > For more information, see <doc:Performance#Sharing-logic-with-actions>.
///
/// - Parameters:
/// - action: The action that is immediately emitted by the effect.
/// - animation: An animation.
public static func send(_ action: Action, animation: Animation? = nil) -> Self {
.send(action).animation(animation)
}
#endif
}

/// A type that can send actions back into the system when used from
Expand Down Expand Up @@ -208,26 +214,28 @@ public struct Send<Action>: Sendable {
self.send(action)
}

/// Sends an action back into the system from an effect with animation.
///
/// - Parameters:
/// - action: An action.
/// - animation: An animation.
public func callAsFunction(_ action: Action, animation: Animation?) {
callAsFunction(action, transaction: Transaction(animation: animation))
}
#if os(macOS) || os(iOS) || os(watchOS) || os(visionOS) || os(tvOS)
/// Sends an action back into the system from an effect with animation.
///
/// - Parameters:
/// - action: An action.
/// - animation: An animation.
public func callAsFunction(_ action: Action, animation: Animation?) {
callAsFunction(action, transaction: Transaction(animation: animation))
}

/// Sends an action back into the system from an effect with transaction.
///
/// - Parameters:
/// - action: An action.
/// - transaction: A transaction.
public func callAsFunction(_ action: Action, transaction: Transaction) {
guard !Task.isCancelled else { return }
withTransaction(transaction) {
self(action)
/// Sends an action back into the system from an effect with transaction.
///
/// - Parameters:
/// - action: An action.
/// - transaction: A transaction.
public func callAsFunction(_ action: Action, transaction: Transaction) {
guard !Task.isCancelled else { return }
withTransaction(transaction) {
self(action)
}
}
}
#endif
}

// MARK: - Composing Effects
Expand Down
Loading