Push View programmatically in callback, SwiftUI
It seems to me that Apple is encouraging us to give up using UIViewController
in SwiftUI, but without using view controllers, I feel a little bit powerless. What I would like is to be able to implement some sort of ViewModel
which will emit events to View
.
ViewModel:
public protocol LoginViewModel: ViewModel {
var onError: PassthroughSubject<Error, Never> { get }
var onSuccessLogin: PassthroughSubject<Void, Never> { get }
}
View:
public struct LoginView: View {
fileprivate let viewModel: LoginViewModel
public init(viewModel: LoginViewModel) {
self.viewModel = viewModel
}
public var body: some View {
NavigationView {
MasterView()
.onReceive(self.viewModel.onError, perform: self.handleError)
.onReceive(self.viewModel.onSuccessLogin, perform: self.handleSuccessfullLogin)
}
}
func handleSuccessfullLogin() {
//push next screen
}
func handleError(_ error: Error) {
//show alert
}
}
Using SwiftUI, I don't know how to push another controller if login is successful
Also, I would appreciate any advice about how to implement what I want in a better way. Thanks.
Solution 1:
I've found the answer. If you want to show another view on callback you should
-
Create state
@State var pushActive = false
-
When ViewModel notifies that login is successful set
pushActive
totrue
func handleSuccessfullLogin() { self.pushActive = true print("handleSuccessfullLogin") }
-
Create hidden
NavigationLink
and bind to that stateNavigationLink(destination: ProfileView(viewModel: ProfileViewModelImpl()), isActive: self.$pushActive) { EmptyView() }.hidden()
Solution 2:
I'm adding some snippets here because I think it simplifies some things and makes reusing navigation links easier:
1. Add View Navigation Extensions
extension View {
func navigatePush(whenTrue toggle: Binding<Bool>) -> some View {
NavigationLink(
destination: self,
isActive: toggle
) { EmptyView() }
}
func navigatePush<H: Hashable>(when binding: Binding<H>,
matches: H) -> some View {
NavigationLink(
destination: self,
tag: matches,
selection: Binding<H?>(binding)
) { EmptyView() }
}
func navigatePush<H: Hashable>(when binding: Binding<H?>,
matches: H) -> some View {
NavigationLink(
destination: self,
tag: matches,
selection: binding
) { EmptyView() }
}
}
Now, you can call on any view (make sure they (or a parent) are in a navigation view)
2. Use at leisure
struct Example: View {
@State var toggle = false
@State var tag = 0
var body: some View {
NavigationView {
VStack(alignment: .center, spacing: 24) {
Text("toggle pushed me")
.navigatePush(whenTrue: $toggle)
Text("tag pushed me (2)")
.navigatePush(when: $tag, matches: 2)
Text("tag pushed me (4)")
.navigatePush(when: $tag, matches: 4)
Button("toggle") {
self.toggle = true
}
Button("set tag 2") {
self.tag = 2
}
Button("set tag 4") {
self.tag = 4
}
}
}
}
}