opsb
(Oliver Searle-Barnes)
1
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?
otondin
(Gabriel Tondin)
2
Hey @opsb ! Duno which TCA version you're using, but have you ever taken a look, for a chance, at DismissEffect ?
opsb
(Oliver Searle-Barnes)
3
Well I haven't even added it to the project yet so it'll be the latest
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?
opsb
(Oliver Searle-Barnes)
4
nvm, I see I need to read the manual
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.
opsb
(Oliver Searle-Barnes)
5
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)
otondin
(Gabriel Tondin)
6
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)
}
}
}
}
opsb
(Oliver Searle-Barnes)
7
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.
otondin
(Gabriel Tondin)
8
I see... so maybe you can use ViewStore type directly, maybe this example could help you out.
mbrandonw
(Brandon Williams)
9
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.
opsb
(Oliver Searle-Barnes)
10
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)
mbrandonw
(Brandon Williams)
11
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: …)
opsb
(Oliver Searle-Barnes)
12
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.