简体   繁体   中英

Programmatically navigate to new view in SwiftUI

Descriptive example:

login screen, user taps "Login" button, request is performed, UI shows waiting indicator, then after successful response I'd like to automatically navigate user to the next screen.

How can I achieve such automatic transition in SwiftUI?

You can replace the next view with your login view after a successful login. For example:

struct LoginView: View {
    var body: some View {
        ...
    }
}

struct NextView: View {
    var body: some View {
        ...
    }
}

// Your starting view
struct ContentView: View {

    @EnvironmentObject var userAuth: UserAuth 

    var body: some View {
        if !userAuth.isLoggedin {
            LoginView()
        } else {
            NextView()
        }

    }
}

You should handle your login process in your data model and use bindings such as @EnvironmentObject to pass isLoggedin to your view.

Note: In Xcode Version 11.0 beta 4 , to conform to protocol 'BindableObject' the willChange property has to be added

import Combine

class UserAuth: ObservableObject {

  let didChange = PassthroughSubject<UserAuth,Never>()

  // required to conform to protocol 'ObservableObject' 
  let willChange = PassthroughSubject<UserAuth,Never>()

  func login() {
    // login request... on success:
    self.isLoggedin = true
  }

  var isLoggedin = false {
    didSet {
      didChange.send(self)
    }

    // willSet {
    //       willChange.send(self)
    // }
  }
}

For future reference, as a number of users have reported getting the error "Function declares an opaque return type", to implement the above code from @MoRezaFarahani requires the following syntax:

struct ContentView: View {

    @EnvironmentObject var userAuth: UserAuth 

    var body: some View {
        if !userAuth.isLoggedin {
            return AnyView(LoginView())
        } else {
            return AnyView(NextView())
        }

    }
}

This is working with Xcode 11.4 and Swift 5

struct LoginView: View {
    
    @State var isActive = false
    @State var attemptingLogin = false
    
    var body: some View {
        ZStack {
            NavigationLink(destination: HomePage(), isActive: $isActive) {
                Button(action: {
                    attlempinglogin = true
                    // Your login function will most likely have a closure in 
                    // which you change the state of isActive to true in order 
                    // to trigger a transition
                    loginFunction() { response in
                        if response == .success {
                            self.isActive = true
                        } else {
                            self.attemptingLogin = false
                        }
                    }
                }) {
                    Text("login")
                }
            }
            
            WaitingIndicator()
                .opacity(attemptingLogin ? 1.0 : 0.0)
        }
    }
}

Use Navigation link with the $isActive binding variable

To expound what others have elaborated above based on changes on combine as of Swift Version 5.2 it could be simplified using publishers.

  1. Create a class names UserAuth as shown below don't forget to import import Combine .
class UserAuth: ObservableObject {
        @Published var isLoggedin:Bool = false

        func login() {
            self.isLoggedin = true
        }
    }
  1. Update SceneDelegate.Swift with

    let contentView = ContentView().environmentObject(UserAuth())

  2. Your authentication view

     struct LoginView: View { @EnvironmentObject var userAuth: UserAuth var body: some View { ... if ... { self.userAuth.login() } else { ... } } }
  3. Your dashboard after successful authentication, if the authentication userAuth.isLoggedin = true then it will be loaded.

     struct NextView: View { var body: some View { ... } }
  4. Lastly, the initial view to be loaded once the application is launched.

struct ContentView: View {
    @EnvironmentObject var userAuth: UserAuth 
    var body: some View {
        if !userAuth.isLoggedin {
                LoginView()
            } else {
                NextView()
            }
    }
  }

Here is an extension on UINavigationController that has simple push/pop with SwiftUI views that gets the right animations. The problem I had with most custom navigations above was that the push/pop animations were off. Using NavigationLink with an isActive binding is the correct way of doing it, but it's not flexible or scalable. So below extension did the trick for me:

/**
 * Since SwiftUI doesn't have a scalable programmatic navigation, this could be used as
 * replacement. It just adds push/pop methods that host SwiftUI views in UIHostingController.
 */
extension UINavigationController: UINavigationControllerDelegate {

    convenience init(rootView: AnyView) {
        let hostingView = UIHostingController(rootView: rootView)
        self.init(rootViewController: hostingView)

        // Doing this to hide the nav bar since I am expecting SwiftUI
        // views to be wrapped in NavigationViews in case they need nav.
        self.delegate = self
    }

    public func pushView(view:AnyView) {
        let hostingView = UIHostingController(rootView: view)
        self.pushViewController(hostingView, animated: true)
    }

    public func popView() {
        self.popViewController(animated: true)
    }

    public func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
        navigationController.navigationBar.isHidden = true
    }
}

Here is one quick example using this for the window.rootViewController .

var appNavigationController = UINavigationController.init(rootView: rootView)
window.rootViewController = appNavigationController
window.makeKeyAndVisible()

// Now you can use appNavigationController like any UINavigationController, but with SwiftUI views i.e. 
appNavigationController.pushView(view: AnyView(MySwiftUILoginView()))

Now you need to just simply create an instance of the new View you want to navigate to and put that in NavigationButton:

NavigationButton(destination: NextView(), isDetail: true, onTrigger: { () -> Bool in
    return self.done
}) {
    Text("Login")
}

If you return true onTrigger means you successfully signed user in.

I followed Gene's answer but there are two issues with it that I fixed below. The first is that the variable isLoggedIn must have the property @Published in order to work as intended. The second is how to actually use environmental objects.

For the first, update UserAuth.isLoggedIn to the below:

@Published var isLoggedin = false {
didSet {
  didChange.send(self)
}

The second is how to actually use Environmental objects. This isn't really wrong in Gene's answer, I just noticed a lot of questions about it in the comments and I don't have enough karma to respond to them. Add this to your SceneDelegate view:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
    // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
    // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
    var userAuth = UserAuth()
    
    // Create the SwiftUI view that provides the window contents.
    let contentView = ContentView().environmentObject(userAuth)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM