Skip to content

Commit 48327bc

Browse files
authored
Only dismiss alert/dialog state when receiving a domain-specific action (pointfreeco#2468)
1 parent c0d8fba commit 48327bc

File tree

3 files changed

+145
-12
lines changed

3 files changed

+145
-12
lines changed

Sources/ComposableArchitecture/Internal/EphemeralState.swift

+28-10
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,39 @@
44
/// with they go away. Such features do not manage any behavior on the inside.
55
///
66
/// Alerts and confirmation dialogs are examples of this kind of state.
7-
public protocol _EphemeralState {}
7+
public protocol _EphemeralState {
8+
static var actionType: Any.Type { get }
9+
}
810

9-
extension AlertState: _EphemeralState {}
11+
extension AlertState: _EphemeralState {
12+
public static var actionType: Any.Type { Action.self }
13+
}
1014

1115
@available(iOS 13, macOS 12, tvOS 13, watchOS 6, *)
12-
extension ConfirmationDialogState: _EphemeralState {}
16+
extension ConfirmationDialogState: _EphemeralState {
17+
public static var actionType: Any.Type { Action.self }
18+
}
19+
20+
@usableFromInline
21+
func ephemeralType<State>(of state: State) -> (any _EphemeralState.Type)? {
22+
(State.self as? any _EphemeralState.Type)
23+
?? EnumMetadata(type(of: state)).flatMap { metadata in
24+
metadata.associatedValueType(forTag: metadata.tag(of: state))
25+
as? any _EphemeralState.Type
26+
}
27+
}
1328

1429
@usableFromInline
1530
func isEphemeral<State>(_ state: State) -> Bool {
16-
if State.self is _EphemeralState.Type {
17-
return true
18-
} else if let metadata = EnumMetadata(type(of: state)) {
19-
return metadata.associatedValueType(forTag: metadata.tag(of: state))
20-
is _EphemeralState.Type
21-
} else {
22-
return false
31+
ephemeralType(of: state) != nil
32+
}
33+
34+
extension _EphemeralState {
35+
@usableFromInline
36+
static func canSend<Action>(_ action: Action) -> Bool {
37+
return Action.self == Self.actionType
38+
|| EnumMetadata(Action.self).flatMap { metadata in
39+
metadata.associatedValueType(forTag: metadata.tag(of: action)) == Self.actionType
40+
} == true
2341
}
2442
}

Sources/ComposableArchitecture/Reducer/Reducers/PresentationReducer.swift

+3-2
Original file line numberDiff line numberDiff line change
@@ -353,9 +353,10 @@ public struct _PresentationReducer<Base: Reducer, Destination: Reducer>: Reducer
353353
.map { self.toPresentationAction.embed(.presented($0)) }
354354
._cancellable(navigationIDPath: destinationNavigationIDPath)
355355
baseEffects = self.base.reduce(into: &state, action: action)
356-
if isEphemeral(destinationState),
356+
if let ephemeralType = ephemeralType(of: destinationState),
357357
destinationNavigationIDPath
358-
== state[keyPath: self.toPresentationState].wrappedValue.map(self.navigationIDPath(for:))
358+
== state[keyPath: self.toPresentationState].wrappedValue.map(self.navigationIDPath(for:)),
359+
ephemeralType.canSend(destinationAction)
359360
{
360361
state[keyPath: self.toPresentationState].wrappedValue = nil
361362
}

Tests/ComposableArchitectureTests/Reducers/PresentationReducerTests.swift

+114
Original file line numberDiff line numberDiff line change
@@ -2427,4 +2427,118 @@ final class PresentationReducerTests: BaseTCATestCase {
24272427
// NB: Another action needs to come into the `ifLet` to cancel the child action
24282428
await store.send(.tapAfter)
24292429
}
2430+
2431+
func testPresentation_leaveAlertPresentedForNonAlertActions() async {
2432+
if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) {
2433+
struct Child: Reducer {
2434+
struct State: Equatable {
2435+
var count = 0
2436+
}
2437+
enum Action: Equatable {
2438+
case decrementButtonTapped
2439+
case incrementButtonTapped
2440+
}
2441+
func reduce(into state: inout State, action: Action) -> Effect<Action> {
2442+
switch action {
2443+
case .decrementButtonTapped:
2444+
state.count -= 1
2445+
return .none
2446+
case .incrementButtonTapped:
2447+
state.count += 1
2448+
return .none
2449+
}
2450+
}
2451+
}
2452+
2453+
struct Parent: Reducer {
2454+
struct State: Equatable {
2455+
@PresentationState var destination: Destination.State?
2456+
var isDeleted = false
2457+
}
2458+
enum Action: Equatable {
2459+
case destination(PresentationAction<Destination.Action>)
2460+
case presentAlert
2461+
case presentChild
2462+
}
2463+
2464+
var body: some ReducerOf<Self> {
2465+
Reduce { state, action in
2466+
switch action {
2467+
case .destination(.presented(.alert(.deleteButtonTapped))):
2468+
state.isDeleted = true
2469+
return .none
2470+
case .destination:
2471+
return .none
2472+
case .presentAlert:
2473+
state.destination = .alert(
2474+
AlertState {
2475+
TextState("Uh oh!")
2476+
} actions: {
2477+
ButtonState(role: .destructive, action: .deleteButtonTapped) {
2478+
TextState("Delete")
2479+
}
2480+
}
2481+
)
2482+
return .none
2483+
case .presentChild:
2484+
state.destination = .child(Child.State())
2485+
return .none
2486+
}
2487+
}
2488+
.ifLet(\.$destination, action: /Action.destination) {
2489+
Destination()
2490+
}
2491+
}
2492+
struct Destination: Reducer {
2493+
enum State: Equatable {
2494+
case alert(AlertState<Action.Alert>)
2495+
case child(Child.State)
2496+
}
2497+
enum Action: Equatable {
2498+
case alert(Alert)
2499+
case child(Child.Action)
2500+
2501+
enum Alert: Equatable {
2502+
case deleteButtonTapped
2503+
}
2504+
}
2505+
var body: some ReducerOf<Self> {
2506+
Scope(state: /State.alert, action: /Action.alert) {}
2507+
Scope(state: /State.child, action: /Action.child) {
2508+
Child()
2509+
}
2510+
}
2511+
}
2512+
}
2513+
let line = #line - 6
2514+
2515+
let store = TestStore(initialState: Parent.State()) {
2516+
Parent()
2517+
}
2518+
2519+
await store.send(.presentAlert) {
2520+
$0.destination = .alert(
2521+
AlertState {
2522+
TextState("Uh oh!")
2523+
} actions: {
2524+
ButtonState(role: .destructive, action: .deleteButtonTapped) {
2525+
TextState("Delete")
2526+
}
2527+
}
2528+
)
2529+
}
2530+
2531+
#if DEBUG
2532+
XCTExpectFailure {
2533+
$0.compactDescription.hasPrefix(
2534+
"""
2535+
A "Scope" at "\(#fileID):\(line)" received a child action when child state was set to a \
2536+
different case. …
2537+
"""
2538+
)
2539+
}
2540+
#endif
2541+
await store.send(.destination(.presented(.child(.decrementButtonTapped))))
2542+
}
2543+
}
24302544
}

0 commit comments

Comments
 (0)