UIKit and racing actions

Hi!

We use SCA pretty heavy in our project, which currently is UIKit and Swift. The navigation is partly driven by state changes using the built-in IfLetUIKit-helper. It's mostly working well.

However one issue we have is that if we present a view controller using a state change, and that view controller in turn sends an action when it's displayed, we get the erroneous message Fatal error: The store was sent the action ... while it was already processing another action.

Now my question is, how are we supposed to send actions without interfering with in-flight ones when the presented view controller knows nothing about the context?

I've made an example project which looks like this:

struct AppState: Equatable {
    var showView = false
}

enum AppAction {
    case showView
    case fetchSomeNecessaryData
}

let reducer = Reducer<AppState, AppAction, Void> { state, action, _ in
    switch action {
        case .showView:
            state.showView = true
            return .none

        case .fetchSomeNecessaryData:
            return .none
    }
}

And two view controllers:

final class ViewController: UIViewController {
    let store: Store<AppState, AppAction>
    let viewStore: ViewStore<AppState, AppAction>

    var cancellable: AnyCancellable?

    init(store: Store<AppState, AppAction>) {
        self.store = store
        self.viewStore = ViewStore(store)
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError() }

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .gray

        cancellable = viewStore.publisher.showView.sink { showView in
            guard showView else { return }
            self.present(
                DetailViewController(store: self.store),
                animated: true,
                completion: nil
            )
        }
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        viewStore.send(.showView)
    }
}

final class DetailViewController: UIViewController {
    let viewStore: ViewStore<AppState, AppAction>

    init(store: Store<AppState, AppAction>) {
        self.viewStore = ViewStore(store)
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError() }

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .white

        viewStore.send(.fetchSomeNecessaryData)
    }
}

This is all it takes to make SCA angry, and this is a very common navigation pattern IMHO.

What happens if you move viewStore.send(.fetchSomeNecessaryData) into viewWillAppear? Just curious.

Hey @Simon_Westerlund, thanks for sharing this. The reason for this precondition is that if send is implemented naively it can allow actions to come in and mutate state + send side effects while the store is processing an action, and that can lead to unexpected behavior. So it may seem strict, but we do it for correctness.

However, having said that, Stephen and I think we can remove this precondition while still retaining the correctness. We will be opening a PR soon.

Thank you for your response, @mbrandonw! I fully understand the reason behind the stricter approach, it most certainly have helped me previously spot issues with actions running on different threads, however that was just developer errors.

I'm looking forward to the PR, thanks for the great work!

we just merged this: https://github.com/pointfreeco/swift-composable-architecture/pull/287. Let us know how it works for you!

1 Like

It works just as well as I hoped, thank you!

This is great, I've been fighting similar issues myself!
Thanks for bringing it up @Simon_Westerlund and thanks for fishing @mbrandonw and @stephencelis :)

1 Like
Terms of Service

Privacy Policy

Cookie Policy