How to trigger side effects on a UIViewController

I've inherited a UIKit based app and want to introduce SCA to extract the business logic before migrating over to SwiftUI. Triggering actions and subscribing to state changes looks straight forward. One area I'm not clear on though is how best to trigger side effects on a UIViewController. For instance if I want to call dismiss(animated:completion:) | Apple Developer Documentation a side effect seems appropriate, but I'm not sure how to package that function up as a side effect with SCA. How should I approach this?

Hey @opsb ! Duno which TCA version you're using, but have you ever taken a look, for a chance, at DismissEffect ?

Well I haven't even added it to the project yet so it'll be the latest :grin: Does @Dependency work with UIKit or is that SwiftUI thing? I'm also surprised to see self referenced in the reducer, what's it referring to there?

nvm, I see I need to read the manual :sweat_smile: That appears to be what I need then, I can use dependencies to make the UIViewController available for side effects. Now I'm not sure how I'll inject it at the appropriate time but I'll take a read through Documentation. Thanks for the pointer, it's set me on the right path.

hmm, so having reading the docs some more I don't see how a UIViewController could be made available as a dependency. Maybe I need to use a singleton which the UIViewController registers itself on which in turn is exposed as a dependency? (That way the singleton would be injected and the UIViewController would be available when attached to a view)

Actually I've never used TCA + UIKit, so the following is pretty much a pseudo-code, but maybe you can go with something like:

import UIKit
import ComposableArchitecture

struct FooBarFeature: ReducerProtocol {
    
    @Dependency(\.dismiss) var dismiss
    
    struct State {
        init() {}
    }
    
    enum Action {
        case dismiss
    }
    
    var body: some ReducerProtocolOf<Self> {
        Reduce { state, action in
            switch {
            case .dismiss:
                return .run { _ in await self.dimiss() }
            }
        }
    }
}

class FooBarViewController: UIViewController {
    var store: StoreOf<FooBarFeature>?

    override func viewDidLoad() {
        super.viewDidLoad()
        
        store = Store(
            state: FooBarFeature.State(),
            reduce: FooBarFeature()
        )
    }
    
    ...
    
    func foobar() {
        WithViewStore(store, observe: { $0 }) { viewStore in
            dismiss(animated: true) {
                viewStore.send(.dismiss)
            }
        }
    }
}

Thanks for the suggestion @otondin. Unless I'm mistaken it looks like WithViewStore only works with SwiftUI unfortunately. I'm getting the impression SCA isn't used much with UIKit. I'm still keen to try and find a way to get this working though because it seems like a good way to migrate to SwiftUI.

I see... so maybe you can use ViewStore type directly, maybe this example could help you out.

TCA does work just fine with UIKit, you just have to do more work to integrate with it than you do with SwiftUI. But that's just the reality of working with UIKit in general.

To go back to your original question, you would not put view controllers in your dependencies and you would not interact with view controllers in reducers. Instead, all navigation should be driven off of state. When a piece of state becomes non-nil, a navigation event occurs (e.g. a controller is pushed onto a nav controller, or a modal is presented), and then when the state turns nil you would dismiss the controller. We do have some basic examples of using UIKit in the TCA repo.

And this works more generally. Anything you want to do in the view controller from the reducer should be communicated via state. The reducer mutates a piece of state, the view controller listens for changes to that state, and acts accordingly.

Thanks for the pointer Brandon, that makes sense. So in this case I'd have some state corresponding to whether or not the view should be visible then in the UIViewController I'd observe that state and dismiss the UIViewController if it changed to notVisible?

Eventually I'd like to have navigation driven by the state (I see SCA has some APIs for this) but to begin with I need to slowly shift business logic into SCA. Given that the store isn't driving navigation yet I think I'd have to notify the store that the view was currently visible when viewDidLoad is called on the UIViewController?

(I will take a look at the UIKit example you mention)

Yeah, that is correct. You can even do something like this in the controller:

viewStore.publisher.notVisible
  .sink { 
    if $0 { self.dismiss(animated: true) }
  }
  .store(in: …)

Glad to hear I'm on the right track. Thanks so much for the guidance, that gives me a clear path to start introducing SCA with UIKit and then eventually migrating to SwiftUI.