31

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?

0

7 Answers 7

32

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)
    // }
  }
}
Sign up to request clarification or add additional context in comments.

10 Comments

@zgorawski That's right. I've not found a way to manually push a new view with SwiftUI yet. This way also makes sure the user cannot navigate back to the login screen unless you set isLoggedin to false.
@MoRezaFarahani I'm using beta 7 and UserAuth is now a ObservableObject. In my ContentView I'm getting the error "Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type" in the line "var body: some View"
I'm new to SwiftUI, but I followed this exactly and get this error: Fatal error: No ObservableObject of type UserAuth found. A View.environmentObject(_:) for UserAuth may be missing as an ancestor of this view.: file SwiftUI, line 0. This error only shows up at run-time.
I'm also getting the No ObservableObject of type found error. Where exactly in the above code was AuthUser first instantiated? And is this one version of the class accessible in any view without the need to pass it around?
Same No ObservableObject of type found error like others are facing what is the soln can someone elaborate instead of bits of pieces
|
7

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

1 Comment

Does this animate correctly?I would want a push animation between the two views.
6
struct LoginView: View {
    
    @State var isActive = false
    @State var attemptingLogin = false
    
    var body: some View {
        ZStack {
            NavigationLink(destination: HomePage(), isActive: $isActive) {
                Button(action: {
                    attemptinglogin = 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

2 Comments

Thanks @David Rozmajzl, after that I just stored in UserDefaults if the user isLogged in or not , and from sceneDelegate I choosed which View depending on that bool.
init(destination:isActive:label:)' was deprecated in iOS 16.0
4

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()
            }
    }
  }

2 Comments

This one works. I followed the accepted answer but it did not work first. What is the purpose of @Published ?
@aldoblack This is correct way! @Published is intended for use with SwiftUI.
2

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()))

Comments

1

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)

Comments

-1

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.

1 Comment

NavigationButton does not exist in SwiftUI 2. Better to delete an answer.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.