Push View programmatically in callback, SwiftUI

2020-03-09 04:07发布

问题:

It seems to me that Apple is encouraging us to give up using UIViewController in SwiftUI, but without using view controlelrs, 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 implement the following:

  • Push another controller if login is successful
  • Show Alert if the error happened

Also, I would appreciate any advice about how to implement what I want in a better way. Thanks.

Update 1: I was able to show an alert, but still cannot find how to push another view in viewModel's callback

回答1:

I've found the answer. If you want to show another view on callback you should

1) Create state @State var pushActive = false

2) When ViewModel notifies that login is successful set pushActive to true

  func handleSuccessfullLogin() {
    self.pushActive = true
    print("handleSuccessfullLogin")
  }

3) Create hidden NavigationLink and bind to that state

  NavigationLink(destination: ProfileView(viewModel: ProfileViewModelImpl()), isActive: self.pushActive) {
    Text("")
  }.hidden()


回答2:

As of beta 5, NavigationLink is the mechanism used to programmatically push views. You can see an example of it here.



回答3:

Workaround without creating additional empty views.

You can use .disabled(true) or .allowsHitTesting(false) modifiers to disable taps on NavigationLink.

Disadvantage: You loose default button tap highlighting.

NavigationLink(destination: EnterVerificationCodeScreen(), isActive: self.$viewModel.verifyPinIsShowing) {
    Text("Create an account")
}
.allowsHitTesting(false) // or .disabled(true) 
.buttonStyle(ShadowRadiusButtonStyle(type: .dark, height: 38))