skip to Main Content

Im working on a new social media app and Im having and issue with my navigation code.

Once a user fills out the registration form I want them to be prompted to upload the profile picture. The issue I am having is that it shows the intended view for a half second then moves right back to the registration view.

I have a RegistrationView that handles the UI and and a AuthViewModel that is taking care of the server side communications. Essentially when the user finishes entering the information and hits the button. The AuthViewModel takes over and send the info to firebase then triggers a Bool to be true.

I then had a NagivationLink on the RegistrationView that listens for that bool and when true, changes the view on the UI. Here is the code for that.

NavigationLink(destination: ProfilePhotoSelectorView(), isActive: $viewModel.didAuthenticateUser, label:{} )

XCode is spitting out that its been deprecated in iOS 16 and to move to the NavigationStack system they developed. But, with every guide I can see I cant get this to work. The only time I can get it to work is though the code above and returns this UI glitch.

Here is the full code for the RegistrationView

import SwiftUI

struct RegistrationView: View {
    @State private var email = ""
    @State private var username = ""
    @State private var fullName = ""
    @State private var password = ""
    @State private var isVerified = false

    @Environment(.presentationMode) var presentationMode
    @EnvironmentObject var viewModel: AuthViewModel
    
    var body: some View {
        VStack{

                NavigationLink(destination: ProfilePhotoSelectorView(), isActive: $viewModel.didAuthenticateUser, label:{} )
            
            AuthHeaderView(title1: "Get Started.", title2: "Create Your Account.")
            VStack(spacing: 40) {
                CustomInputFields(imageName: "envelope", placeholderText: "Email", isSecureField: false, text: $email)
                
                CustomInputFields(imageName: "person", placeholderText: "Username", isSecureField: false, text: $username)
                
                CustomInputFields(imageName: "person", placeholderText: "Full Name", isSecureField: false, text: $fullName)
                
                CustomInputFields(imageName: "lock", placeholderText: "Password", isSecureField: true, text: $password)
            }
            .padding(32)
            
            
            Button {
                viewModel.register(withEmail: email, password: password, fullname: fullName, username: username, isVerified: isVerified)
            } label: {
                Text("Sign Up")
                    .font(.headline)
                    .foregroundColor(.white)
                    .frame(width: 340, height: 50)
                    .background(Color("AppGreen"))
                    .clipShape(Capsule())
                    .padding()
            }
            .shadow(color: .gray.opacity(0.5), radius: 10, x:0, y:0)
            
            Spacer()
            
            Button {
                presentationMode.wrappedValue.dismiss()
            } label: {
                HStack {
                    Text("Already Have And Account?")
                        .font(.caption)
                    
                    Text("Sign In")
                        .font(.footnote)
                        .fontWeight(.semibold)
                }
            }
            .padding(.bottom, 32)
            .foregroundColor(Color("AppGreen"))

        }
        .ignoresSafeArea()
        .preferredColorScheme(.dark)
    }
}

struct RegistrationView_Previews: PreviewProvider {
    static var previews: some View {
        RegistrationView()
    }
}

And here is the full code for the AuthViewModel

import SwiftUI
import Firebase

class AuthViewModel: ObservableObject {
    @Published var userSession: Firebase.User?
    @Published var didAuthenticateUser = false
    
    init() {
        self.userSession = Auth.auth().currentUser
        
        print("DEBUG: User session is (String(describing: self.userSession?.uid))")
    }
    
    func login(withEmail email: String, password: String){
        Auth.auth().signIn(withEmail: email, password: password) { result, error in
            if let error = error {
                print("DEBUG: Failed to sign in with error(error.localizedDescription)")
                return
            }
            
            guard let user = result?.user else { return }
            self.userSession = user
            print("Did log user in")
        }
    }
    
    func register(withEmail email: String, password: String, fullname: String, username: String, isVerified: Bool){
        Auth.auth().createUser(withEmail: email, password: password) { result, error in
            if let error = error {
                print("DEBUG: Failed to register with error(error.localizedDescription)")
                return
            }
            
            guard let user = result?.user else { return }
            
            print("DEBUG: Registerd User Succesfully")
            
            let data = ["email": email, "username" :username.lowercased(), "fullname": fullname, "isVerified": isVerified, "uid": user.uid]
            
            Firestore.firestore().collection("users")
                .document(user.uid)
                .setData(data) { _ in
                    self.didAuthenticateUser = true
                }
        }
    }
    
    func signOut() {
        userSession = nil
        try? Auth.auth().signOut()
    }
}

Here is the code for the ProfilePhotoSelectorView

import SwiftUI

struct ProfilePhotoSelectorView: View {
    var body: some View {
        VStack {
            AuthHeaderView(title1: "Account Creation:", title2: "Add A Profile Picture")
            
            Button {
                print("Pick Photo Here")
            } label: {
                VStack{
                    Image("PhotoIcon")
                        .resizable()
                        .renderingMode(.template)
                        .frame(width: 180, height: 180)
                        .scaledToFill()
                        .padding(.top, 44)
                        .foregroundColor(Color("AppGreen"))
                    
                    Text("Tap To Add Photo")
                        .font(.title3).bold()
                        .padding(.top, 10)
                        .foregroundColor(Color("AppGreen"))
                }
            }

            
            Spacer()
        }
        .ignoresSafeArea()
        .preferredColorScheme(.dark)
    }
}

struct ProfilePhotoSelectorView_Previews: PreviewProvider {
    static var previews: some View {
        ProfilePhotoSelectorView()
    }
}

Tried all variations of the new NavigationStack and tried some other button code to see if i could trigger it from there. No Reso

3

Answers


  1. If I understand your question correctly, you want to use NavigationStack,
    but it is not working for you.

    There are many missing parts, but here is my attempt of using NavigationStack
    to trigger the destination, given a change in viewModel.didAuthenticateUser.

    struct ContentView: View {
        @StateObject var viewModel = AuthViewModel()
        
        var body: some View {
            NavigationStack(path: $viewModel.didAuthenticateUser) {  // <-- here
                RegistrationView()
                    .environmentObject(viewModel)
            }
        }
    }
    
    struct RegistrationView: View {
        @State private var email = ""
        @State private var username = ""
        @State private var fullName = ""
        @State private var password = ""
        @State private var isVerified = false
        
        @Environment(.presentationMode) var presentationMode
        @EnvironmentObject var viewModel: AuthViewModel
        
        var body: some View {
            VStack{
                AuthHeaderView(title1: "Get Started.", title2: "Create Your Account.")
                VStack(spacing: 40) {
                    CustomInputFields(imageName: "envelope", placeholderText: "Email", isSecureField: false, text: $email)
    
                    CustomInputFields(imageName: "person", placeholderText: "Username", isSecureField: false, text: $username)
    
                    CustomInputFields(imageName: "person", placeholderText: "Full Name", isSecureField: false, text: $fullName)
    
                    CustomInputFields(imageName: "lock", placeholderText: "Password", isSecureField: true, text: $password)
                }
                .padding(32)
    
                Button {
                    viewModel.register(withEmail: email, password: password, fullname: fullName, username: username, isVerified: isVerified)
                } label: {
                    Text("Sign Up")
                        .font(.headline)
                        .foregroundColor(.white)
                        .frame(width: 340, height: 50)
                        .background(Color("AppGreen"))
                        .clipShape(Capsule())
                        .padding()
                }
                .shadow(color: .gray.opacity(0.5), radius: 10, x:0, y:0)
                
                Spacer()
                
                Button {
                    presentationMode.wrappedValue.dismiss()
                } label: {
                    HStack {
                        Text("Already Have And Account?")
                            .font(.caption)
                        
                        Text("Sign In")
                            .font(.footnote)
                            .fontWeight(.semibold)
                    }
                }
                .padding(.bottom, 32)
                .foregroundColor(Color("AppGreen"))
                
            }
            .navigationDestination(for: Bool.self) { _ in  // <-- here
                ProfilePhotoSelectorView()
            }
            .ignoresSafeArea()
            .preferredColorScheme(.dark)
        }
    }
    
    class AuthViewModel: ObservableObject {
        @Published var userSession: Firebase.User?
        @Published var didAuthenticateUser: [Bool] = [] // <-- here
        
        init() {
            self.userSession = Auth.auth().currentUser
            
            print("DEBUG: User session is (String(describing: self.userSession?.uid))")
        }
        
        func login(withEmail email: String, password: String){
            Auth.auth().signIn(withEmail: email, password: password) { result, error in
                if let error = error {
                    print("DEBUG: Failed to sign in with error(error.localizedDescription)")
                    return
                }
    
                guard let user = result?.user else { return }
                self.userSession = user
                print("Did log user in")
            }
        }
        
        func register(withEmail email: String, password: String, fullname: String, username: String, isVerified: Bool){
    
            Auth.auth().createUser(withEmail: email, password: password) { result, error in
                if let error = error {
                    print("DEBUG: Failed to register with error(error.localizedDescription)")
                    return
                }
    
                guard let user = result?.user else { return }
    
                print("DEBUG: Registerd User Succesfully")
    
                let data = ["email": email, "username" :username.lowercased(), "fullname": fullname, "isVerified": isVerified, "uid": user.uid]
    
                Firestore.firestore().collection("users")
                    .document(user.uid)
                    .setData(data) { _ in
                        self.didAuthenticateUser = [true]  // <-- here
                    }
            }
        }
        
        func signOut() {
            userSession = nil
            try? Auth.auth().signOut()
        }
    }
    
    
    struct ProfilePhotoSelectorView: View {
        var body: some View {
            VStack {
                AuthHeaderView(title1: "Account Creation:", title2: "Add A Profile Picture")
    
                Button {
                    print("Pick Photo Here")
                } label: {
                    VStack{
                        Image(systemName: "globe")
                            .resizable()
                            .renderingMode(.template)
                            .frame(width: 180, height: 180)
                            .scaledToFill()
                            .padding(.top, 44)
                            .foregroundColor(Color("AppGreen"))
                        
                        Text("Tap To Add Photo")
                            .font(.title3).bold()
                            .padding(.top, 10)
                            .foregroundColor(Color("AppGreen"))
                    }
                }
                Spacer()
            }
            .ignoresSafeArea()
            .preferredColorScheme(.dark)
        }
    }
    
    Login or Signup to reply.
  2. Using NavigationLink in this way is not recommended, so I’m not surprised it would lead to buggy behavior. This is exacerbated by the fact that your RegistrationView is not placed within a NavigationView (deprecated) or NavigationStack, as these views provide most of the functionality of a navigation link.

    Like you said, this usage of NavigationLink is deprecated. The isActive property has always been a little ambiguous in my opinion (from what I understand it is not meant as an ‘activator’ of the navigation link, rather as a way to read whether the link is active). The new way of presenting navigation links (using .navigationDestination) is much better.

    Presenting a view using a boolean property

    What you essentially want is to present the ProfilePhotoSelectorView when a boolean property is switched to true. This is common paradigm in SwiftUi, and there are many ways to do this, such as .sheet(isPresented:content:) or .popover(isPresented:content:). Note the isPresented parameter in both methods are boolean properties. Using .sheet, for example:

    struct RegistrationView: View {
        // ...
        @EnvironmentObject var viewModel: AuthViewModel
        
        var body: some View {
            VStack {
                // ...
            }
            // Presents the photo selector view when `didAuthenticateUser` is true
            .sheet(isPresented: $viewModel.didAuthenticateUser) {
                ProfilePhotoSelectorView()
            }
        }
    }
    

    Adding a new view to the Navigation Tree

    If you are adamant on using navigation links (i.e. you really want the ProfilePhotoSelectorView to be a node in the navigation tree), you’re going to have to learn to use the new NavigationStack and append the view onto the path. This will require some retooling (and probably some reading on your part; here and here are good starting points). The view model would be the most likely place to control the stack, although you may eventually want to create a dedicated viewmodel. Here’s a simple example:

    struct ContentView: View {
        @StateObject var viewModel = AuthViewModel()
        
        var body: some View {
            NavigationStack(path: $viewModel.navigationPath) {
                RegistrationView()
                    .environmentObject(viewModel)
                    
                    .navigationDestination(for: RegistrationScreen.self) { screen in
                        switch screen {
                        case .photoSelection:
                            ProfilePhotoSelectorView()
                        }
                    }
            }
        }
    }
    
    class AuthViewModel: ObservableObject {
        // ...
    
        // A new enum that defines the various types of possible views in the navigation stack
        enum RegistrationScreen: Hashable {
            case photoSelection
        }
    
        // The navigation path
        @Published var navigationPath: [RegistrationScreen] = []
    
        // Example usages of the navigation path. These functions show how to programmatically control the navigation stack
        func showPhotoSelectionScreen() {
            self.navigationPath.append(.photoSelection)
        }
    
        func goToRootOfNavigation() {
            self.navigationPath = []
        }
    }
    
    Login or Signup to reply.
  3. You can only have one level of NavigationLinks unless you mark the links as isDetail(false) or use .navigationStyle(.stack).

    The reason is because in landscape it uses a split view and the link replaces the large detail pane on the right.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search