I am trying to create a basic sign up screen. The screen appears fine however when I click submit, and an error is returned, the #State variables (such as $viewModel.firstName and $lastname reset back to empty strings, so my user loses all their progress.
Sign Up Screen
struct SignUpScreen: View {
#State
var firstName: String = ""
#State
var lastName: String = ""
#State
var birthday: String = ""
#State
var number: String = ""
#State
var email: String = ""
#State
var password: String = ""
#State
var confirmPassword: String = ""
#ObservedObject
var viewModel: SignUpViewModel = SignUpViewModel()
var body: some View {
ZStack {
VStack {
VClearBackground()
Spacer()
}
ScrollView {
VStack(alignment: .leading) {
Group {
PreHeaderText(header: "Get Started")
.alignmentGuide(.leading, computeValue: {d in
d[.leading]
})
.padding(EdgeInsets.init(top: 32, leading: 0, bottom: 0, trailing: 0))
HeaderText(header: "Create Account")
EditText(hint: "John", text: $viewModel.firstName, label: "FIRST NAME", textContentType: UITextContentType.name)
EditText(hint: "Doe", text: $lastName, label: "LAST NAME", textContentType: UITextContentType.name)
EditText(hint: "01/01/2001", text: $birthday, label: "BIRTHDAY")
EditText(hint: "(123) 456-7890)", text: $number, label: "MOBILE NUMBER", textContentType: UITextContentType.telephoneNumber, keyboardType: UIKeyboardType.phonePad)
EditText(hint: "email#exmaple.com", text: $email, label: "EMAIL", textContentType: UITextContentType.emailAddress)
EditText(hint: "********", text: $password, label: "PASSWORD", textContentType: UITextContentType.newPassword)
EditText(hint: "********", text: $confirmPassword, label: "CONFIRM PASSWORD", textContentType: UITextContentType.newPassword)
}
Group {
if self.viewModel.error != nil {
HStack {
Spacer()
Text(viewModel.error ?? "")
.foregroundColor(ColorTheme.error.color)
Spacer()
}
.padding()
}
HStack {
Spacer()
VowerButton(text: "Submit") {
self.viewModel.signUp(firstName: self.viewModel.firstName, lastName: self.lastName, email: self.email, birthday: self.birthday, phoneNumber: self.number, password: self.password, confirmPassword: self.confirmPassword)
}
Spacer()
}
.padding()
HStack {
Spacer()
NavigationLink(destination: LoginScreen(), isActive: $viewModel.goToLogin) {
CtaText(text: "Have an account?", cta: "Login") {
self.viewModel.onGoToLoginClicked()
}
}
.padding()
Spacer()
}
Spacer()
}
}
}
.padding(EdgeInsets.init(top: 16, leading: 16, bottom: 16, trailing: 16))
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
}
.background(LinearGradient(gradient: Gradient(colors: [.black, ColorTheme.brandPurple.color]), startPoint: .top, endPoint: .bottom))
.edgesIgnoringSafeArea(.all)
}
}
Sign Up ViewModel
class SignUpViewModel : ObservableObject {
#State
var firstName: String = ""
#Published
var error: String? = nil
#Published
var goHome: Bool = false
#Published
var goToLogin: Bool = false
func onGoToLoginClicked() {
self.goToLogin = true
}
func signUp(firstName: String, lastName: String, email: String, birthday: String, phoneNumber: String, password: String, confirmPassword: String) {
if (firstName.count < 3) {
error = "Please enter first name"
return
}
if (lastName.count < 3) {
error = "Please enter last name"
return
}
if (!email.isEmail()) {
error = "Pleaes enter valid email"
return
}
if (birthday.isEmpty) {
error = "Pleae enter valid birthday"
return
}
if (!phoneNumber.isDigits) {
error = "Please enter valid phone number"
return
}
if (password.count < 8) {
error = "Please enter a password that is at least 8 characters long"
}
if (password != confirmPassword) {
error = "Password do not match"
}
Auth.auth().createUser(withEmail: email, password: password, completion: { authResult, error in
if authResult != nil {
self.goHome = true
} else {
self.error = error?.localizedDescription
}
})
}
}
EditText View
struct EditText: View {
var hint: String
#Binding
var text: String
var label: String = ""
var defaultValue = ""
var textContentType: UITextContentType? = .none
var keyboardType: UIKeyboardType = .default
private func initializeDefaultValue() {
DispatchQueue.main.async {
self.text = self.defaultValue
}
}
var body: some View {
initializeDefaultValue()
return VStack(alignment: .leading) {
Text(label).font(.system(size: 12)).bold()
.foregroundColor(ColorTheme.text.color)
HStack {
TextField(hint, text: $text)
.lineLimit(1)
.textContentType(textContentType)
.keyboardType(keyboardType)
.foregroundColor(ColorTheme.text.color)
}
Divider().background(Color(ColorTheme.brandBlue.value))
}
.padding(EdgeInsets.init(top: 12, leading: 0, bottom: 8, trailing: 0))
}
}
Ok, now I see (continuing from your second question, which I answered first). #State should be used in View it is designed for it. In ObservableObject you have to use #Published, so below is how it should be
class SignUpViewModel : ObservableObject {
#Published
var firstName: String = ""
Related
I'm trying to learn how to do basic login in swift with Firebase and it's causing me to lose my mind over navigating to the main page of the app once the login is complete. I have a ViewModel that manages the login, and in the view I have added an onReceive property to listen on a viewModel's boolean to detect sign in and trigger the navigation. I don't understand why it's not working, any help would be greatly appreciated!
ViewModel:
class LoginViewModel: ObservableObject {
let auth = Auth.auth()
var userInfo: User?
#Published var isSignedIn = false
func signIn(email: String, password: String) {
auth.signIn(withEmail: email, password: password) { _, error in
if let error = error as? NSError {
print("Error happened on login"+error.description)
} else {
print("Login successful")
if let user = self.auth.currentUser {
print("We have a user")
self.userInfo = user
self.isSignedIn.toggle()
}
}
}
}
}
View:
struct LoginPage: View {
#State var email = ""
#State var password = ""
#ObservedObject var viewModel = LoginViewModel()
var body: some View {
NavigationView {
ZStack {
VStack {
TextField("Email", text: $email).padding()
.background(Color(.secondarySystemBackground))
.padding()
TextField("Password", text: $password).padding()
.background(Color(.secondarySystemBackground))
.padding()
Button(action: {
guard !email.isEmpty, !password.isEmpty else {
return
}
viewModel.signIn(email: email, password: password)
}, label: {
Text("Sign in")
.padding(EdgeInsets(top: 12, leading: 35, bottom: 12, trailing: 35))
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(15)
})
}
}.navigationTitle("Login")
}.onReceive(viewModel.$isSignedIn) { isSignedIn in
if isSignedIn {
print("ok damm")
NavigationLink(destination: HomePage()) {
EmptyView()
}
}
}
}
}
Note that "ok damm" prints every time.
Try the below code it will help you to solve your issue:
NavigationLink(destination: Text("Second View"), isActive: $isSignedIn){
EmptyView()
}
View:
struct LoginPage: View {
#State var email = ""
#State var password = ""
#ObservedObject var viewModel = LoginViewModel()
var body: some View {
NavigationView {
ZStack {
VStack {
TextField("Email", text: $email).padding()
.background(Color(.secondarySystemBackground))
.padding()
TextField("Password", text: $password).padding()
.background(Color(.secondarySystemBackground))
.padding()
Button(action: {
guard !email.isEmpty, !password.isEmpty else {
return
}
viewModel.signIn(email: email, password: password)
}, label: {
Text("Sign in")
.padding(EdgeInsets(top: 12, leading: 35, bottom: 12, trailing: 35))
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(15)
})
}
NavigationLink(destination: HomePage()), isActive: $isSignedIn){
EmptyView()
}
}.navigationTitle("Login")
}
}
}
Answer by Jatin Bhuva works but is deprecated for iOS 16. Here's the solution for new iOS versions:
struct LoginPage: View {
#State var email = ""
#State var password = ""
#ObservedObject var viewModel = LoginViewModel()
var body: some View {
NavigationStack {
ZStack {
VStack {
TextField("Email", text: $email).padding()
.background(Color(.secondarySystemBackground))
.padding()
TextField("Password", text: $password).padding()
.background(Color(.secondarySystemBackground))
.padding()
Button(action: {
guard !email.isEmpty, !password.isEmpty else {
return
}
viewModel.signIn(email: email, password: password)
}, label: {
Text("Sign in")
.padding(EdgeInsets(top: 12, leading: 35, bottom: 12, trailing: 35))
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(15)
})
Text("Don't have an account yet ?").padding(EdgeInsets(top: 20, leading: 0, bottom: 10, trailing: 0))
Button(action: {
guard !email.isEmpty, !password.isEmpty else {
return
}
viewModel.signUp(email: email, password: password)
}, label: {
Text("Create an account")
.padding(EdgeInsets(top: 12, leading: 35, bottom: 12, trailing: 35))
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(15)
})
}
}.navigationTitle("Login")
.navigationDestination(isPresented: $viewModel.isSignedIn) {
HomePage()
}
}
}
}
Notice how I replaced the NavigationView with a NavigationStack and added .navigationDestination(isPresented:) that listens for changes on my model's isSignedIn published property.
switch statement in swiftui can handle that easily. I use it all the time;
//Added code
enum LoginStages {
case preLogin, postLogin
}
class LoginViewModel: ObservableObject {
let auth = Auth.auth()
var userInfo: User?
#Published var loginStage: LoginStages = LoginStages.preLogin
//Added code
#Published var isSignedIn = false
func signIn(email: String, password: String) {
auth.signIn(withEmail: email, password: password) { _, error in
if let error = error as? NSError {
print("Error happened on login"+error.description)
} else {
print("Login successful")
if let user = self.auth.currentUser {
print("We have a user")
self.userInfo = user
self.isSignedIn.toggle()
loginStage = .postLogin //Added code
}
}
}
}
}
And your view should look like this;
struct LoginPage: View {
#State var email = ""
#State var password = ""
#ObservedObject var viewModel = LoginViewModel()
var body: some View {
switch viewModel.loginStage {
case .preLogin:
//This is ur initial code which the user sees before login.
// the same as the code you have above.
case .postLogin:
//Here, just call the post login view after a successful login;
// I am assuming it is called home view for this example.
HomeView()
}
}
}
I'm having difficulties with the textfields. I'm fetching the user data from Firebase RTDB and i want to display some of the data in a profile view which contains textfields for the ability to edit the data.
My UserData:
struct UserData: Codable, Identifiable {
var email : String?
var name : String?
var firstname : String?
var lastname : String?
var type: String?
var uid: String?
var profileImageUrl : String?
var id : String?
var fcmToken2 : String?
var onboarding : Bool?
var phone : String?
}
The View with TextFields:
struct ProfileViewDemo: View {
#Binding var user : UserData
#State private var email: String = ""
var body: some View {
TextField("\(user.firstname!)", text: $email, onEditingChanged: { edit in
self.editing = edit
})
}
}
How do i preset the email var to the value of user.email? I tried using .onAppear, but that caused problem with the photo loading. Are there any ways to preset the email var with the data from UserData? Thanks.
EDIT (Adding the ProfileView code)
struct ProfileViewDemo: View {
#ObservedObject var session : SessionStore
#Binding var user : UserData
#State private var number: String = ""
#Binding var email: String = "123"
#State private var editing = false
#State private var editing2 = false
#State private var editing3 = false
#State private var editing4 = false
#State var refresh: Bool = false
#State private var image: UIImage?
#State private var shouldPresentImagePicker = false
#State private var shouldPresentActionScheet = false
#State private var shouldPresentCamera = false
var body: some View {
ScrollView{
VStack{
VStack{
ZStack{
ZStack{
if(image == nil){
KFImage.url(URL(string: "\(user.profileImageUrl!)"))
.loadDiskFileSynchronously()
.cacheMemoryOnly()
.fade(duration: 0.25)
.onProgress { receivedSize, totalSize in }
.onSuccess { result in }
.onFailure { error in }
.resizable()
.aspectRatio(contentMode: .fill)
.clipShape(Circle())
.overlay(Circle().stroke(Color(red: 50 / 255, green: 51 / 255, blue: 53 / 255), lineWidth: 2))
.frame(width: 140, height: 140)
}
if (image != nil) {
Image(uiImage: image!)
.resizable()
.aspectRatio(contentMode: .fill)
.clipShape(Circle())
.overlay(Circle().stroke(Color(red: 50 / 255, green: 51 / 255, blue: 53 / 255), lineWidth: 2))
.frame(width: 140, height: 140)
}
}
ZStack{
Button {
self.shouldPresentActionScheet = true
} label: {
Image(systemName: "camera")
.font(.system(size: 12))
.padding(.all, 5)
.background(Color(red: 50 / 255, green: 51 / 255, blue: 53 / 255))
.foregroundColor(Color.white)
.clipShape(Circle())
}
.sheet(isPresented: $shouldPresentImagePicker) {
SUImagePickerView(sourceType: self.shouldPresentCamera ? .camera : .photoLibrary, image: self.$image, isPresented: self.$shouldPresentImagePicker)
}.actionSheet(isPresented: $shouldPresentActionScheet) { () -> ActionSheet in
ActionSheet(title: Text("Choose mode"), message: Text("Please choose your preferred mode to set your profile image"), buttons: [ActionSheet.Button.default(Text("Camera"), action: {
self.shouldPresentImagePicker = true
self.shouldPresentCamera = true
}), ActionSheet.Button.default(Text("Photo Library"), action: {
self.shouldPresentImagePicker = true
self.shouldPresentCamera = false
}), ActionSheet.Button.cancel(Text("Atšaukti"))])
}
}
.padding(.all, 3)
.background(Color.white)
.clipShape(Circle())
.offset(x: 50)
.offset(y: 50)
}
}
.padding(.bottom, 15)
ZStack {
ZStack(alignment: .leading){
ZStack(alignment: .leading){
TextField("\(user.email!)", text: $email, onEditingChanged: { edit in
self.editing3 = edit
})
.padding(.leading, 5)
.padding(.trailing, 5)
.padding()
.offset(y: 0)
}.frame(maxWidth: .infinity, maxHeight: 50)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(editing3 ? Color(UIColor.systemGray2) : Color(UIColor.systemGray5), lineWidth: 2)
)
.background(Color(UIColor.white))
.cornerRadius(5)
Text("El. Pašto adresas")
.disableAutocorrection(true)
.padding(.leading, 5)
.padding(.trailing, 5)
.background(Color.white)
.font(Font.custom("Montserrat-Regular", size: 15.0))
.foregroundColor(Color.gray)
.padding()
.offset(y: -25)
}
.frame(maxWidth: .infinity, maxHeight: 50)
}
.padding(.bottom, 15)
}
.padding()
.navigationBarItems(trailing:
Button("Išsaugoti") {
print("Presed Profilio Informacija išsaugota!")
updateProfileInfo()
UIApplication.shared.endEditing()
})
}
.onTapGesture {
self.endTextEditing()
}
}
func updateProfileInfo () {
if (image != nil) {
session.uplaodImage(image!) { (url) in
if let url = url {
session.updateProfileImage(url: url)
print("Profile Image updated")
}
}
}
if (image == nil){
print("Skipping image update - empty")
}
refresh.toggle()
}
}
Code to fetch UserData:
func fetchUsers(){
ref.child("users").observe(.childAdded) { (snapshot) in
guard let dictionary = snapshot.value as? [String: AnyObject] else { return}
var user = UserData()
user.email = (dictionary["email"] as! String)
user.name = (dictionary["name"] as! String)
user.firstname = (dictionary["firstname"] as! String)
user.lastname = (dictionary["lastname"] as! String)
user.type = (dictionary["type"] as! String)
user.uid = (dictionary["uid"] as! String)
user.profileImageUrl = (dictionary["profileImageUrl"] as! String)
user.id = snapshot.key
user.fcmToken2 = (dictionary["fcmToken"] as! String)
user.phone = (dictionary["phone"] as! String)
user.onboarding = (dictionary["onboarding"] as! Bool)
self.users.append(user)
}
}
Use OnAppear function on the View and assign values or Fire your API call from there.
struct ProfileViewDemo: View {
#Binding var user : UserData
#Binding var email: String = ""
var body: some View {
if email == "" {
TextField("Enter email", text: $email)
}
else {
TextField("", text: $email)
}
}.onAppear(perform: {
email = user.email
//You can fire your API call in here and assign the values
}
}
I'm having problem with my app. When I tap on a TextField, trying to fill data, in the sheet that just show up, my app freezes and four errors appear on console:
> [Assert] Animation styles expected to be empty on user driven
> presentation. Actually contains: (
> "<<_UIViewControllerKeyboardAnimationStyle: 0x281ee4d00>; animated = YES; duration = 0.35; force = NO>" )
>
> [KeyboardSceneDelegate] Animation styles were not empty on user driven
> presentation!
>
> Fit_Vein/ProfileView.swift:69: Fatal error: Unexpectedly found nil
> while unwrapping an Optional value
>
> Fit_Vein/ProfileView.swift:69: Fatal error: Unexpectedly found nil
> while unwrapping an Optional value
This situation takes place when I go from ProfileView to SettingsView, then try to do one of options: change email, password or delete account and input data in those sheets.
My code:
ProfileView:
struct ProfileView: View {
#ObservedObject private var profileViewModel: ProfileViewModel
#EnvironmentObject private var sessionStore: SessionStore
#Environment(\.colorScheme) var colorScheme
#State private var image = UIImage()
#State private var shouldPresentAddActionSheet = false
#State private var shouldPresentImagePicker = false
#State private var shouldPresentCamera = false
#State private var shouldPresentSettings = false
init(profileViewModel: ProfileViewModel) {
self.profileViewModel = profileViewModel
}
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.size.width
let screenHeight = geometry.size.height
ScrollView(.vertical) {
HStack {
if profileViewModel.profilePicturePhotoURL != nil {
AsyncImage(url: profileViewModel.profilePicturePhotoURL!) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 50))
.shadow(color: .gray, radius: 7)
.frame(width: screenWidth * 0.4, height: screenHeight * 0.2)
.onTapGesture {
self.shouldPresentAddActionSheet = true
}
} placeholder: {
Image(uiImage: UIImage(named: "blank-profile-hi")!)
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 50))
.shadow(color: .gray, radius: 7)
.frame(width: screenWidth * 0.4, height: screenHeight * 0.2)
}
} else {
Image(uiImage: UIImage(named: "blank-profile-hi")!)
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 50))
.shadow(color: .gray, radius: 7)
.frame(width: screenWidth * 0.4, height: screenHeight * 0.2)
.onTapGesture {
self.shouldPresentAddActionSheet = true
}
}
Spacer(minLength: screenWidth * 0.05)
VStack {
HStack {
Text(profileViewModel.profile!.firstName)
.foregroundColor(.green)
.font(.system(size: screenHeight * 0.03))
.fontWeight(.bold)
Spacer()
NavigationLink(destination: SettingsView(profile: profileViewModel).environmentObject(sessionStore), isActive: $shouldPresentSettings) {
Button(action: {
shouldPresentSettings = true
}, label: {
Image(systemName: "slider.vertical.3")
.resizable()
.aspectRatio(contentMode: .fit)
})
.frame(width: screenWidth * 0.12, height: screenHeight * 0.04)
.foregroundColor(colorScheme == .dark ? .white : .black)
}
}
.padding(.top, screenHeight * 0.02)
HStack {
Text(profileViewModel.profile!.username)
.foregroundColor(Color(uiColor: UIColor.lightGray))
Spacer()
}
Spacer()
}
}
.padding()
VStack {
Text("Level 1")
.font(.system(size: screenHeight * 0.03))
.fontWeight(.bold)
RoundedRectangle(cornerRadius: 25)
.frame(width: screenWidth * 0.9)
.padding()
.overlay(
HStack {
RoundedRectangle(cornerRadius: 25)
.foregroundColor(.green)
.padding()
.frame(width: screenWidth * 0.7)
Spacer()
}
)
.shadow(color: .gray, radius: 7)
Text("7 / 10 Workouts")
}
Spacer()
}
.sheet(isPresented: $shouldPresentImagePicker) {
ImagePicker(sourceType: self.shouldPresentCamera ? .camera : .photoLibrary, selectedImage: self.$image)
.onDisappear {
profileViewModel.uploadPhoto(image: image)
}
}
.actionSheet(isPresented: $shouldPresentAddActionSheet) {
ActionSheet(title: Text("Add a new photo"), message: nil, buttons: [
.default(Text("Take a new photo"), action: {
self.shouldPresentImagePicker = true
self.shouldPresentCamera = true
}),
.default(Text("Upload a new photo"), action: {
self.shouldPresentImagePicker = true
self.shouldPresentCamera = false
}),
ActionSheet.Button.cancel()
])
}
}
}
}
SettingsView:
import SwiftUI
struct SettingsView: View {
#ObservedObject private var profileViewModel: ProfileViewModel
#StateObject private var sheetManager = SheetManager()
#State private var shouldPresentActionSheet = false
#Environment(\.dismiss) var dismiss
private class SheetManager: ObservableObject {
enum Sheet {
case email
case password
case logout
case signout
}
#Published var showSheet = false
#Published var whichSheet: Sheet? = nil
}
init(profile: ProfileViewModel) {
self.profileViewModel = profile
}
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.size.width
let screenHeight = geometry.size.height
Form {
Section(header: Text("Chats")) {
Toggle(isOn: .constant(false), label: {
Text("Hide my activity status")
})
}
Section(header: Text("Account")) {
Button(action: {
sheetManager.whichSheet = .email
sheetManager.showSheet.toggle()
}, label: {
Text("Change e-mail address")
})
Button(action: {
sheetManager.whichSheet = .password
sheetManager.showSheet.toggle()
}, label: {
Text("Change password")
})
Button(action: {
sheetManager.whichSheet = .logout
shouldPresentActionSheet = true
}, label: {
Text("Logout")
.foregroundColor(.red)
})
Button(action: {
sheetManager.whichSheet = .signout
shouldPresentActionSheet = true
}, label: {
Text("Delete account")
.foregroundColor(.red)
})
}
Section(header: Text("Additional")) {
Label("Follow me on GitHub:", systemImage: "link")
.font(.system(size: 17, weight: .semibold))
Link("#Vader20FF", destination: URL(string: "https://github.com/Vader20FF")!)
.font(.system(size: 17, weight: .semibold))
}
}
.navigationBarTitle("Settings")
.navigationBarTitleDisplayMode(.large)
.sheet(isPresented: $sheetManager.showSheet) {
switch sheetManager.whichSheet {
case .email:
ChangeEmailAddressSheetView(profile: profileViewModel)
case .password:
ChangePasswordSheetView(profile: profileViewModel)
case .signout:
DeleteAccountSheetView(profile: profileViewModel)
default:
Text("No view")
}
}
.confirmationDialog(sheetManager.whichSheet == .logout ? "Are you sure you want to logout?" : "Are you sure you want to delete your account? All data will be lost.", isPresented: $shouldPresentActionSheet, titleVisibility: .visible) {
if sheetManager.whichSheet == .logout {
Button("Logout", role: .destructive) {
profileViewModel.sessionStore!.signOut()
dismiss()
}
Button("Cancel", role: .cancel) {}
} else {
Button("Delete Account", role: .destructive) {
sheetManager.showSheet.toggle()
}
Button("Cancel", role: .cancel) {}
}
}
}
}
}
struct DeleteAccountSheetView: View {
#ObservedObject private var profileViewModel: ProfileViewModel
#Environment(\.dismiss) var dismiss
#State private var email = ""
#State private var password = ""
init(profile: ProfileViewModel) {
self.profileViewModel = profile
}
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.size.width
let screenHeight = geometry.size.height
NavigationView {
VStack {
Form {
Section(footer: Text("Before you delete your account please provide your login credentials to confirm it is really you.")) {
TextField("E-mail", text: $email)
SecureField("Password", text: $password)
}
}
Button(action: {
withAnimation {
dismiss()
profileViewModel.deleteUserData() {
profileViewModel.sessionStore!.deleteUser(email: email, password: password) {
print("Successfully deleted user.")
}
}
}
}, label: {
Text("Delete account permanently")
})
.frame(width: screenWidth * 0.7, height: screenHeight * 0.08)
.background(Color.green)
.cornerRadius(15.0)
.font(.system(size: screenHeight * 0.026))
.foregroundColor(.white)
.padding()
}
.navigationBarHidden(true)
.ignoresSafeArea(.keyboard)
}
}
}
}
struct ChangeEmailAddressSheetView: View {
#ObservedObject private var profileViewModel: ProfileViewModel
#Environment(\.dismiss) var dismiss
#State private var oldEmail = ""
#State private var password = ""
#State private var newEmail = ""
init(profile: ProfileViewModel) {
self.profileViewModel = profile
}
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.size.width
let screenHeight = geometry.size.height
NavigationView {
VStack {
Form {
Section(footer: Text("Before you change your e-mail address please provide your login credentials to confirm it is really you.")) {
TextField("Old e-mail address", text: $oldEmail)
SecureField("Password", text: $password)
TextField("New e-mail address", text: $newEmail)
}
}
Button(action: {
withAnimation {
dismiss()
profileViewModel.emailAddressChange(oldEmailAddress: oldEmail, password: password, newEmailAddress: newEmail) {}
}
}, label: {
Text("Change e-mail address")
})
.frame(width: screenWidth * 0.7, height: screenHeight * 0.08)
.background(Color.green)
.cornerRadius(15.0)
.font(.system(size: screenHeight * 0.026))
.foregroundColor(.white)
.padding()
}
.navigationBarHidden(true)
.ignoresSafeArea(.keyboard)
}
}
}
}
struct ChangePasswordSheetView: View {
#ObservedObject private var profileViewModel: ProfileViewModel
#Environment(\.dismiss) var dismiss
#State private var email = ""
#State private var oldPassword = ""
#State private var newPassword = ""
init(profile: ProfileViewModel) {
self.profileViewModel = profile
}
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.size.width
let screenHeight = geometry.size.height
NavigationView {
VStack {
Form {
Section(footer: Text("Before you change your password please provide your login credentials to confirm it is really you.")) {
TextField("E-mail", text: $email)
SecureField("Old password", text: $oldPassword)
SecureField("New password", text: $newPassword)
}
}
Button(action: {
withAnimation {
dismiss()
profileViewModel.passwordChange(emailAddress: email, oldPassword: oldPassword, newPassword: newPassword) {}
}
}, label: {
Text("Change password")
})
.frame(width: screenWidth * 0.7, height: screenHeight * 0.08)
.background(Color.green)
.cornerRadius(15.0)
.font(.system(size: screenHeight * 0.026))
.foregroundColor(.white)
.padding()
}
.navigationBarHidden(true)
.ignoresSafeArea(.keyboard)
}
}
}
}
ProfileViewModel:
import Foundation
import SwiftUI
#MainActor
class ProfileViewModel: ObservableObject {
#Published var sessionStore: SessionStore?
private let firestoreManager = FirestoreManager()
private let firebaseStorageManager = FirebaseStorageManager()
#Published var profile: Profile?
#Published var profilePicturePhotoURL: URL?
#Published var fetchingData = true
init(forPreviews: Bool) {
self.profile = Profile(id: "sessionStore!.currentUser!.uid", firstName: "firstname", username: "username", birthDate: Date(), age: 18, country: "country", city: "city", language: "language", gender: "gender", email: "email", profilePictureURL: nil)
}
init() {
Task {
try await fetchData()
}
}
func setup(sessionStore: SessionStore) {
self.sessionStore = sessionStore
}
func fetchData() async throws {
if sessionStore != nil {
if sessionStore!.currentUser != nil {
print("Fetching Data")
fetchingData = true
let (firstname, username, birthDate, age, country, city, language, gender, email, profilePictureURL) = try await self.firestoreManager.fetchDataForProfileViewModel(userID: sessionStore!.currentUser!.uid)
self.profile = Profile(id: sessionStore!.currentUser!.uid, firstName: firstname, username: username, birthDate: birthDate, age: age, country: country, city: city, language: language, gender: gender, email: email, profilePictureURL: profilePictureURL)
if profilePictureURL != nil {
self.firebaseStorageManager.getDownloadURLForImage(stringURL: profilePictureURL!, userID: sessionStore!.currentUser!.uid) { photoURL in
self.profilePicturePhotoURL = photoURL
}
}
Task {
fetchingData = false
}
}
} else {
fetchingData = false
}
}
func uploadPhoto(image: UIImage) {
if self.profile!.profilePictureURL != nil {
Task {
try await self.firebaseStorageManager.deleteImageFromStorage(userPhotoURL: self.profile!.profilePictureURL!, userID: self.profile!.id)
}
}
self.firebaseStorageManager.uploadImageToStorage(image: image, userID: self.profile!.id) { photoURL in
self.firestoreManager.addProfilePictureURLToUsersData(photoURL: photoURL) {
Task {
try await self.fetchData()
}
}
}
}
func emailAddressChange(oldEmailAddress: String, password: String, newEmailAddress: String, completion: #escaping (() -> ())) {
self.sessionStore!.changeEmailAddress(oldEmailAddress: oldEmailAddress, password: password, newEmailAddress: newEmailAddress) {
print("Successfully changed user's e-mail address")
}
}
func passwordChange(emailAddress: String, oldPassword: String, newPassword: String, completion: #escaping (() -> ())) {
self.sessionStore!.changePassword(emailAddress: emailAddress, oldPassword: oldPassword, newPassword: newPassword) {
print("Successfully changed user's password")
}
}
func deleteUserData(completion: #escaping (() -> ())) {
if self.profile!.profilePictureURL != nil {
self.firestoreManager.deleteUserData(userUID: sessionStore!.currentUser!.uid) {
print("Successfully deleted user data")
Task {
try await self.firebaseStorageManager.deleteImageFromStorage(userPhotoURL: self.profile!.profilePictureURL!, userID: self.sessionStore!.currentUser!.uid)
completion()
}
}
}
}
}
ProfileModel:
import Foundation
import SwiftUI
struct Profile: Codable, Identifiable {
var id: String
var firstName: String
var username: String
var birthDate: Date
var age: Int
var country: String
var city: String
var language: String
var gender: String
var email: String
var profilePictureURL: String?
}
If a user enters text into the first EditText and then clicks return, it will clear all their text. However if a user enters text into the second EditText and clicks return, nothing happens. The first EditText will also not display text colors correctly, it displays the text color as black while the rest correctly display it as white.
Sign Up Screen
struct SignUpScreen: View {
#State
var firstName: String = ""
#State
var lastName: String = ""
#State
var birthday: String = ""
#State
var number: String = ""
#State
var email: String = ""
#State
var password: String = ""
#State
var confirmPassword: String = ""
#ObservedObject
var viewModel: SignUpViewModel = SignUpViewModel()
var body: some View {
ZStack {
VStack {
VClearBackground()
Spacer()
}
ScrollView {
VStack(alignment: .leading) {
Group {
PreHeaderText(header: "Get Started")
.alignmentGuide(.leading, computeValue: {d in
d[.leading]
})
.padding(EdgeInsets.init(top: 32, leading: 0, bottom: 0, trailing: 0))
HeaderText(header: "Create Account")
EditText(hint: "John", text: $viewModel.firstName, label: "FIRST NAME", textContentType: UITextContentType.name)
EditText(hint: "Doe", text: $lastName, label: "LAST NAME", textContentType: UITextContentType.name)
EditText(hint: "01/01/2001", text: $birthday, label: "BIRTHDAY")
EditText(hint: "(123) 456-7890)", text: $number, label: "MOBILE NUMBER", textContentType: UITextContentType.telephoneNumber, keyboardType: UIKeyboardType.phonePad)
EditText(hint: "email#exmaple.com", text: $email, label: "EMAIL", textContentType: UITextContentType.emailAddress)
EditText(hint: "********", text: $password, label: "PASSWORD", textContentType: UITextContentType.newPassword)
EditText(hint: "********", text: $confirmPassword, label: "CONFIRM PASSWORD", textContentType: UITextContentType.newPassword)
}
Group {
if self.viewModel.error != nil {
HStack {
Spacer()
Text(viewModel.error ?? "")
.foregroundColor(ColorTheme.error.color)
Spacer()
}
.padding()
}
HStack {
Spacer()
VowerButton(text: "Submit") {
self.viewModel.signUp(firstName: self.viewModel.firstName, lastName: self.lastName, email: self.email, birthday: self.birthday, phoneNumber: self.number, password: self.password, confirmPassword: self.confirmPassword)
}
Spacer()
}
.padding()
HStack {
Spacer()
NavigationLink(destination: LoginScreen(), isActive: $viewModel.goToLogin) {
CtaText(text: "Have an account?", cta: "Login") {
self.viewModel.onGoToLoginClicked()
}
}
.padding()
Spacer()
}
Spacer()
}
}
}
.padding(EdgeInsets.init(top: 16, leading: 16, bottom: 16, trailing: 16))
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
}
.background(LinearGradient(gradient: Gradient(colors: [.black, ColorTheme.brandPurple.color]), startPoint: .top, endPoint: .bottom))
.edgesIgnoringSafeArea(.all)
}
}
EditText
struct EditText: View {
var hint: String
#Binding
var text: String
var label: String = ""
var defaultValue = ""
var textContentType: UITextContentType? = .none
var keyboardType: UIKeyboardType = .default
private func initializeDefaultValue() {
DispatchQueue.main.async {
self.text = self.defaultValue
}
}
var body: some View {
initializeDefaultValue()
return VStack(alignment: .leading) {
Text(label).font(.system(size: 12)).bold()
.foregroundColor(ColorTheme.text.color)
HStack {
TextField(hint, text: $text)
.lineLimit(1)
.textContentType(textContentType)
.keyboardType(keyboardType)
.foregroundColor(ColorTheme.text.color)
}
Divider().background(Color(ColorTheme.brandBlue.value))
}
.padding(EdgeInsets.init(top: 12, leading: 0, bottom: 8, trailing: 0))
}
}
Here it is
EditText(hint: "John", text: $viewModel.firstName, label: "FIRST NAME", textContentType: UITextContentType.name)
EditText(hint: "Doe", text: $lastName, label: "LAST NAME", textContentType: UITextContentType.name)
as you see 1st text field is bound to view model (not provided, so I can't say what's wrong there), but 2nd to internal view state.
As I see there is firstName as internal view state, so possible the intention was (and actual fix) just use it
EditText(hint: "John", text: $firstName, label: "FIRST NAME", textContentType: UITextContentType.name)
When I type in my EditText view to fill out all the required information and then click submit. Everything I typed disappears. I want this text to remain. I am guessing there is something wrong with my #State objects but cannot figure out what.
SignUpViewModel
class SignUpViewModel : ObservableObject {
#Published
var error: String? = nil
#Published
var goHome: Bool = false
#Published
var goToLogin: Bool = false
func onGoToLoginClicked() {
self.goToLogin = true
}
func signUp(firstName: String, lastName: String, email: String, birthday: String, phoneNumber: String, password: String, confirmPassword: String) {
if (firstName.count < 3) {
error = "Please enter first name"
return
}
if (lastName.count < 3) {
error = "Please enter last name"
return
}
if (!email.isEmail()) {
error = "Pleaes enter valid email"
return
}
if (birthday.isEmpty) {
error = "Pleae enter valid birthday"
return
}
if (!phoneNumber.isDigits) {
error = "Please enter valid phone number"
return
}
if (password.count < 8) {
error = "Please enter a password that is at least 8 characters long"
}
if (password != confirmPassword) {
error = "Password do not match"
}
Auth.auth().createUser(withEmail: email, password: password, completion: { authResult, error in
if authResult != nil {
self.goHome = true
} else {
self.error = error?.localizedDescription
}
})
}
}
SignUp View
struct SignUpScreen: View {
#State
var firstName: String = ""
#State
var lastName: String = ""
#State
var birthday: String = ""
#State
var number: String = ""
#State
var email: String = ""
#State
var password: String = ""
#State
var confirmPassword: String = ""
#EnvironmentObject
var viewModel: SignUpViewModel
var body: some View {
ZStack {
VStack {
VClearBackground()
Spacer()
}
ScrollView {
VStack(alignment: .leading) {
Group {
PreHeaderText(header: "Get Started")
.alignmentGuide(.leading, computeValue: { d in
d[.leading]
})
.padding(EdgeInsets.init(top: 32, leading: 0, bottom: 0, trailing: 0))
HeaderText(header: "Create Account")
EditText(hint: "Huey", text: $firstName, label: "FIRST NAME", textContentType: UITextContentType.name)
EditText(hint: "Freeman", text: $lastName, label: "LAST NAME", textContentType: UITextContentType.name)
EditText(hint: "04-19-1994", text: $birthday, label: "BIRTHDAY")
EditText(hint: "(281) 456-7890)", text: $number, label: "MOBILE NUMBER", textContentType: UITextContentType.telephoneNumber, keyboardType: UIKeyboardType.phonePad)
EditText(hint: "email#exmaple.com", text: $email, label: "EMAIL", textContentType: UITextContentType.emailAddress)
EditText(hint: "********", text: $password, label: "PASSWORD", textContentType: UITextContentType.newPassword)
EditText(hint: "********", text: $confirmPassword, label: "CONFIRM PASSWORD", textContentType: UITextContentType.newPassword)
}
Group {
if self.viewModel.error != nil {
HStack {
Spacer()
Text(viewModel.error ?? "")
.foregroundColor(ColorTheme.error.color)
Spacer()
}
.padding()
}
HStack {
Spacer()
VowerButton(text: "Submit") {
self.viewModel.signUp(firstName: self.firstName, lastName: self.lastName, email: self.email, birthday: self.birthday, phoneNumber: self.number, password: self.password, confirmPassword: self.confirmPassword)
}
Spacer()
}
.padding()
HStack {
Spacer()
NavigationLink(destination: LoginScreen(), isActive: $viewModel.goToLogin) {
CtaText(text: "Have an account?", cta: "Login") {
self.viewModel.onGoToLoginClicked()
}
}
.padding()
Spacer()
}
Spacer()
}
}
}
.padding(EdgeInsets.init(top: 16, leading: 16, bottom: 16, trailing: 16))
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
}
.background(LinearGradient(gradient: Gradient(colors: [.black, ColorTheme.brandPurple.color]), startPoint: .top, endPoint: .bottom))
.edgesIgnoringSafeArea(.all)
}
}
EditText View
struct EditText: View {
var hint: String
#Binding
var text: String
var label: String = ""
var defaultValue = ""
var textContentType: UITextContentType? = .none
var keyboardType: UIKeyboardType = .default
private func initializeDefaultValue() {
DispatchQueue.main.async {
self.text = self.defaultValue
}
}
var body: some View {
initializeDefaultValue()
return VStack(alignment: .leading) {
Text(label).font(.system(size: 12)).bold()
.foregroundColor(ColorTheme.text.color)
HStack {
TextField(hint, text: $text)
.lineLimit(1)
.textContentType(textContentType)
.keyboardType(keyboardType)
.foregroundColor(ColorTheme.text.color)
}
Divider().background(Color(ColorTheme.brandBlue.value))
}
.padding(EdgeInsets.init(top: 12, leading: 0, bottom: 8, trailing: 0))
}
}
The problem is in your EditText view.
struct EditText: View {
var hint: String
#Binding
var text: String
var label: String = ""
var defaultValue = ""
var textContentType: UITextContentType? = .none
var keyboardType: UIKeyboardType = .default
private func initializeDefaultValue() {
DispatchQueue.main.async {
self.text = self.defaultValue
}
}
var body: some View {
initializeDefaultValue()
return VStack(alignment: .leading) {
Text(label).font(.system(size: 12)).bold()
.foregroundColor(ColorTheme.text.color)
HStack {
TextField(hint, text: $text)
.lineLimit(1)
.textContentType(textContentType)
.keyboardType(keyboardType)
.foregroundColor(ColorTheme.text.color)
}
Divider().background(Color(ColorTheme.brandBlue.value))
}
.padding(EdgeInsets.init(top: 12, leading: 0, bottom: 8, trailing: 0))
}
}
Specifically, it's in the body property. This is a computed property that is fetched by SwiftUI whenever the view is recomputed. In this case, this happens when the error property of the SignUpScreen view changes, as all the subviews are recomputed.
When this EditText view is recomputed, the initializeDefaultValue() function is called (it's the first line in the body property). This clears the text fields.
As for a solution, I'm not sure why you actually need the initializeDefaultValue function in here at all. It seems best suited for the ViewModel or some other location.
Also, just some other things that I saw:
func signUp(firstName: String, lastName: String, email: String, birthday: String, phoneNumber: String, password: String, confirmPassword: String) {
if (firstName.count < 3) {
error = "Please enter first name"
return
}
if (lastName.count < 3) {
error = "Please enter last name"
return
}
if (!email.isEmail()) {
error = "Pleaes enter valid email"
return
}
if (birthday.isEmpty) {
error = "Pleae enter valid birthday"
return
}
if (!phoneNumber.isDigits) {
error = "Please enter valid phone number"
return
}
if (password.count < 8) {
error = "Please enter a password that is at least 8 characters long"
}
if (password != confirmPassword) {
error = "Password do not match"
}
Auth.auth().createUser(withEmail: email, password: password, completion: { authResult, error in
if authResult != nil {
self.goHome = true
} else {
self.error = error?.localizedDescription
}
})
}
This function returns early in all error cases except the last two — I believe this was a mistake.
if self.viewModel.error != nil {
HStack {
Spacer()
Text(viewModel.error ?? "")
.foregroundColor(ColorTheme.error.color)
Spacer()
}
.padding()
}
This part of the SignUpScreen view should be able to be simplified to this:
if let err = self.viewModel.error {
HStack {
Spacer()
Text(err)
.foregroundColor(ColorTheme.error.color)
Spacer()
}
.padding()
}
or, if if-let statements are not allowed in this case:
if self.viewModel.error != nil {
HStack {
Spacer()
Text(viewModel.error!)
.foregroundColor(ColorTheme.error.color)
Spacer()
}
.padding()
}
as you know that the error is non-nil.
Hope all of this helps!
The problem code is in SignUpScreen:
#ObservedObject
var viewModel: SignUpViewModel = SignUpViewModel()
Whenever the view is re-evaluated, a new SignUpViewModel is created.
You can create the view model outside the view, and either pass it to the directly to the constructor, or inject it to the environment using environmentObject().
To use an environment object instead, change the above declaration to:
#EnvironmentObject
var viewModel: SignUpViewModel
And then create your view like this:
var signUpViewModel = SignUpViewModel()
// ...
SignUpScreen()
.environmentObject(signUpViewModel)