I am building a login for my app which calls an API on my server and returns user information and a JWT token. I decided to split my views into separate code blocks with a view to moving them to their own files in the project later on. Since I have done this however, my button in the login form doesn't run the login function in my ViewModel, nor does it even print and output to the console.
I have included my ContentView.swift here to show my thinking and basic setup.
struct ContentView: View {
@StateObject private var loginVM = LoginViewModel()
@StateObject private var projectListVM = ProjectListViewModel()
@State private var isSecured: Bool = true
var body: some View {
Group {
if loginVM.isAuthenticated {
MainView()
} else {
LoginView()
}
}
}
}
struct LoginView: View {
@StateObject private var loginVM = LoginViewModel()
@StateObject private var projectListVM = ProjectListViewModel()
@State private var isPasswordVisible = false
var body: some View {
Form {
VStack(alignment: .center, spacing: 0.0) {
Spacer()
VStack(alignment: .leading, spacing: 10) {
Text("Email")
.font(.fieldLabel)
.foregroundColor(Color.secondary01Light)
.frame(alignment: .leading)
TextField("Username", text: $loginVM.username)
.keyboardType(.emailAddress)
.textInputAutocapitalization(.never)
.padding()
.overlay {
RoundedRectangle(cornerRadius: 4, style: .continuous)
.stroke(Color(UIColor.systemGray4), lineWidth: 1)
}
}
VStack(alignment: .leading, spacing: 10) {
Spacer()
Text("Password")
.font(.fieldLabel)
.foregroundColor(Color.secondary01Light)
.frame(alignment: .leading)
SecureField("Password", text: $loginVM.password)
.padding()
.overlay {
RoundedRectangle(cornerRadius: 4, style: .continuous)
.stroke(Color(UIColor.systemGray4), lineWidth: 1)
}
}
}
VStack {
Button(action: {
print("Login Tapped")
loginVM.login()
}) {
Text("Sign In")
.frame(maxWidth: .infinity)
.padding(8)
.cornerRadius(8)
}
.padding(.top, 30)
.frame(height: nil)
.buttonStyle(.borderedProminent)
.tint(Color.primary01)
.controlSize(.regular)
.font(.custom("Poppins-ExtraBold", size: 16))
.foregroundColor(.white)
}
}
}
}
The above code shows the button inside a form and with an action and inside of the action a call to the login() method and a print statement.
When I tap the button in the simulator I am not even getting an output in the console and it doesn't seem to be running my login function either.
Below is my LoginViewModel which I have had working without a problem in the past.
class LoginViewModel: ObservableObject {
var username: String = ""
var password: String = ""
@Published var isAuthenticated: Bool = false
func login() {
print("Login Called")
Webservice().login(username: username, password: password) { result in
print("Login API Call Made")
switch result {
case .success(_):
print("Got User Data")
DispatchQueue.main.async {
self.isAuthenticated = true
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
I have the Form fields in place and the @StateObject
in the view but doesn't seem to want to fire.
CodePudding user response:
You initialize loginVM
two times! Once in ContentView
and again in LoginView
(both using @StateObject
) ... so you create two different states that are not connected.
What you want to do is create it only once in ContentView
and pass it down (e.g. with .environmentObject
) to LoginView
:
struct ContentView: View {
// here you create/initialize the object
@StateObject private var loginVM = LoginViewModel()
var body: some View {
Group {
if loginVM.isAuthenticated {
MainView()
} else {
LoginView()
}
}
.environmentObject(loginVM) // pass the object to all child views
}
}
struct LoginView: View {
// here you get the passed Object from the environment
@EnvironmentObject private var loginVM: LoginViewModel
var body: some View {
// ...
}
}