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
CodePudding user response:
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)
}
}
CodePudding user response:
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 = []
}
}