Optional State for Sheet and onDisappear actions

How do you correctly call onDisappear action, when Sheet gets interactively dismissed (drag)?

Let's say I have parent state with Route enum, indicating whether I should be displaying sheet. This child (sheet) state has some kind of side effects, in this case running Camera session for scanning codes. When we call onDisappear on Camera view, it gets correctly deinitialized and camera stops running (green indicator in status bar disappears).

However if user dismisses entire sheet, before scanning any code, this camera session is still running and simply setting state.route = nil erases the child state and subsequently the onDisappear action is ignored and even throws a runtime error about state being nil.

Camera indicator stays on until either app is closed or user reenters the Camera view and scans a valid code, which triggers a NavigationLink and onDisappear on CameraView gets called

Parent state:

public struct ParentState: Equatable {
    public var route: Route?

    public enum Route: Equatable {
        ...
        case qrScannerSheet(SheetState)
        ...
    }
}

View code:

.sheet(isPresented: viewStore.binding(
    get: \.showingSheet,
    send: ParentAction.setShowingSheet
)) {
    IfLetStore(store.scope(
        state: { (/ParentState.Route.sheet).extract(from: $0.route) },
        action: ParentAction.sheetAction
    ), then: SheetView.init(store:))
}

Parent reducer:

case .setShowingSheet(true): return .none
case .setShowingSheet(false):
    state.route = nil
    return .none

EDIT:
Managed to solve it by changing the binding action on sheet to send a new child "clean" action on false case, which first clears all state inside child state and then react to this action in Parent Reducer to set route to nil.

send: { $0 ? .setShowingSheet(true) : .sheet(.clean) })
and react to it in reducer with

case .sheet(.clean):
    return .init(value: . setShowingSheet(false))

This feels like a hack tho, since the parent has to know about this "clean" action, and how to call and react to it.

1 Like

Hi @Berhtulf, this is a good question, and unfortunately it is a sharp edge. Currently the best way to handle is as you have done. The parent is the only domain that can see the "on disappear" event since by the time the child domain gets that action, the state has already been nil'd out.

We do have plans to improve this very soon. We are currently putting the finishing touches on adding more concurrency tools to the library (see here), and included in those tools is the ability to tie an effect to the lifetime of the view so that it is automatically cancelled when the view goes away.

And then in the near future we have plans to add navigation tools to the library (here's a sneak peek for stacks) that further ties an entire feature's domain to the lifecycle of a view.

In the meantime what you have done should be sufficient though!


Another option that comes to mind, if you really do want to get ahold of onDisappear in the view, is to make the child state non-optional in the parent, and instead drive navigation via a boolean. This is not ideal from a domain modeling perspective as it allows invalid states, but it does mean you don't have to worry about sending actions when state is nil.

Hope this helps!

3 Likes