Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 8 additions & 0 deletions Sources/ComposableArchitecture/Internal/NavigationID.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ struct NavigationID: Hashable, @unchecked Sendable {
private let identifier: AnyHashable?
private let tag: UInt32?



enum Kind: Hashable {
case casePath(root: Any.Type, value: Any.Type)
case keyPath(AnyKeyPath)
Expand Down Expand Up @@ -111,6 +113,12 @@ struct NavigationID: Hashable, @unchecked Sendable {
}
}

init(kind: Kind, identifier: AnyHashable?, tag: UInt32?) {
self.kind = kind
self.identifier = identifier
self.tag = tag
}

static func == (lhs: Self, rhs: Self) -> Bool {
lhs.kind == rhs.kind
&& lhs.identifier == rhs.identifier
Expand Down
15 changes: 13 additions & 2 deletions Sources/ComposableArchitecture/Store.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,17 @@ public final class Store<State, Action> {
@ReducerBuilder<State, Action> reducer: () -> R,
withDependencies prepareDependencies: ((inout DependencyValues) -> Void)? = nil
) {
let (initialState, reducer, dependencies) = withDependencies(prepareDependencies ?? { _ in }) {
let prepDeps: (inout DependencyValues) -> Void = {
$0.navigationIDPath = NavigationIDPath(path: [
NavigationID(
kind: .keyPath(\State.self),
identifier: UUID(),
tag: nil
)
])
prepareDependencies?(&$0)
}
let (initialState, reducer, dependencies) = withDependencies(prepDeps) {
@Dependency(\.self) var dependencies
return (initialState(), reducer(), dependencies)
}
Expand Down Expand Up @@ -329,7 +339,8 @@ public final class Store<State, Action> {
}

@available(
*, deprecated,
*,
deprecated,
message:
"Pass 'state' a key path to child state and 'action' a case key path to child action, instead. For more information see the following migration guide: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.5#Store-scoping-with-key-paths"
)
Expand Down
52 changes: 52 additions & 0 deletions Tests/ComposableArchitectureTests/StoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,58 @@ final class StoreTests: BaseTCATestCase {
}
}
}

@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
@MainActor func testRootStoreCancellationIsolation() async throws {
let clock = TestClock()
let store1 = Store(initialState: RootStoreCancellationIsolation.State()) {
RootStoreCancellationIsolation()
} withDependencies: {
$0.continuousClock = clock
}
let store2 = Store(initialState: RootStoreCancellationIsolation.State()) {
RootStoreCancellationIsolation()
} withDependencies: {
$0.continuousClock = clock
}
store1.send(.tap)
store2.send(.tap)
try await Task.sleep(nanoseconds: 100_000_000)
store2.send(.cancelButtonTapped)
await clock.run()
XCTAssertEqual(store1.count, 42)
XCTAssertEqual(store2.count, 0)
}
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
@Reducer struct RootStoreCancellationIsolation {
@ObservableState struct State: Equatable {
var count = 0
}
enum Action {
case cancelButtonTapped
case response(Int)
case tap
}
@Dependency(\.continuousClock) var clock
enum CancelID { case effect }
var body: some ReducerOf<Self> {
Reduce<State, Action> { state, action in
switch action {
case .cancelButtonTapped:
return .cancel(id: CancelID.effect)
case .response(let value):
state.count = value
return .none
case .tap:
return .run { send in
try await clock.sleep(for: .seconds(1))
await send(.response(42))
}
.cancellable(id: CancelID.effect)
}
}
}
}
}

#if canImport(Testing)
Expand Down
Loading