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.
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 {
// ...
}
}
Related
Usual caveat of being new to swiftui and apologies is this is a simple question.
I have a view where I have a date picker, as well as two arrows to increase/decrease the day. When this date is update, I am trying to filter a list of 'sessions' from the database which match the currently displayed date.
I have a filteredSessions variable which applies a filter to all 'sessions' from the database. However I do not seem to have that filter refreshed each time the date is changed.
I have the date to be used stored as a "#State" object in the view. I thought this would trigger the view to update whenever that field is changed? However I have run the debugger and found the 'filteredSessions' variable is only called once, and not when the date is changed (either by the picker or the buttons).
Is there something I'm missing here? Do I need a special way to 'bind' this date value to the list because it isn't directly used by the display?
Code below. Thanks
import SwiftUI
struct TrainingSessionListView: View {
#StateObject var viewModel = TrainingSessionsViewModel()
#State private var displayDate: Date = Date.now
#State private var presentAddSessionSheet = false
private var dateManager = DateManager()
private let oneDay : Double = 86400
private var addButton : some View {
Button(action: { self.presentAddSessionSheet.toggle() }) {
Image(systemName: "plus")
}
}
private var decreaseDayButton : some View {
Button(action: { self.decreaseDay() }) {
Image(systemName: "chevron.left")
}
}
private var increaseDayButton : some View {
Button(action: { self.increaseDay() }) {
Image(systemName: "chevron.right")
}
}
private func sessionListItem(session: TrainingSession) -> some View {
NavigationLink(destination: TrainingSessionDetailView(session: session)) {
VStack(alignment: .leading) {
Text(session.title)
.bold()
Text("\(session.startTime) - \(session.endTime)")
}
}
}
private func increaseDay() {
self.displayDate.addTimeInterval(oneDay)
}
private func decreaseDay() {
self.displayDate.addTimeInterval(-oneDay)
}
var body: some View {
NavigationView {
VStack {
HStack {
Spacer()
decreaseDayButton
Spacer()
DatePicker("", selection: $displayDate, displayedComponents: .date)
.labelsHidden()
Spacer()
increaseDayButton
Spacer()
}
.padding(EdgeInsets(top: 25, leading: 0, bottom: 0, trailing: 0))
Spacer()
ForEach(filteredSessions) { session in
sessionListItem(session: session)
}
Spacer()
}
.navigationTitle("Training Sessions")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing: addButton)
.sheet(isPresented: $presentAddSessionSheet) {
TrainingSessionEditView()
}
}
}
var filteredSessions : [TrainingSession] {
print("filteredSessions called")
return viewModel.sessions.filter { $0.date == dateManager.dateToStr(date: displayDate) }
}
}
struct TrainingSessionListView_Previews: PreviewProvider {
static var previews: some View {
TrainingSessionListView()
}
}
There are two approaches and for your case and for what you described I would take the first one. I only use the second approach if I have more complex filters and tasks
You can directly set the filter on the ForEach this will ensure it gets updated whenever the displayDate changes.
ForEach(viewModel.sessions.filter { $0.date == dateManager.dateToStr(date: displayDate) }) { session in
sessionListItem(session: session)
}
Or you can like CouchDeveloper said, introduce a new state variable and to trigger a State change you would use the willSet extension (doesn't exist in binding but you can create it)
For this second option you could do something like this.
Start create the Binding extension for the didSet and willSet
extension Binding {
func didSet(execute: #escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
let snapshot = self.wrappedValue
self.wrappedValue = $0
execute(snapshot)
}
)
}
func willSet(execute: #escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
execute($0)
self.wrappedValue = $0
}
)
}
}
Introduce the new state variable
#State var filteredSessions: [TrainingSession] = []
// removing the other var
We introduce the function that will update the State var
func filterSessions(_ filter: Date) {
filteredSessions = viewModel.sessions.filter { $0.date == dateManager.dateToStr(date: date) }
}
We update the DatePicker to run the function using the willSet
DatePicker("", selection: $displayDate.willSet { self.filterSessions($0) }, displayedComponents: .date)
And lastly we add a onAppear so we fill the filteredSessions immidiatly (if you want)
.onAppear { filterSessions(displayDate) } // uses the displayDate that you set as initial value
Don't forget in your increaseDay() and decreaseDay() functions to add the following after the addTimeInterval
self.filterSessions(displayDate)
As I said, this second method might be better for more complex filters
Thank you all for your responses. I'm not sure what the issue was originally but it seems updating my view to use Firebase's #FirestoreQuery to access the collection updates the var filteredSessions... much better than what I had before.
New code below seems to be working nicely now.
import SwiftUI
import FirebaseFirestoreSwift
struct TrainingSessionListView: View {
#FirestoreQuery(collectionPath: "training_sessions") var sessions : [TrainingSession]
#State private var displayDate: Date = Date.now
#State private var presentAddSessionSheet = false
private var dateManager = DateManager()
private let oneDay : Double = 86400
private var addButton : some View {
Button(action: { self.presentAddSessionSheet.toggle() }) {
Image(systemName: "plus")
}
}
private var todayButton : some View {
Button(action: { self.displayDate = Date.now }) {
Text("Today")
}
}
private var decreaseDayButton : some View {
Button(action: { self.decreaseDay() }) {
Image(systemName: "chevron.left")
}
}
private var increaseDayButton : some View {
Button(action: { self.increaseDay() }) {
Image(systemName: "chevron.right")
}
}
private func sessionListItem(session: TrainingSession) -> some View {
NavigationLink(destination: TrainingSessionDetailView(sessionId: session.id!)) {
VStack(alignment: .leading) {
Text(session.title)
.bold()
Text("\(session.startTime) - \(session.endTime)")
}
}
}
private func increaseDay() {
self.displayDate.addTimeInterval(oneDay)
}
private func decreaseDay() {
self.displayDate.addTimeInterval(-oneDay)
}
var body: some View {
NavigationView {
VStack {
HStack {
Spacer()
decreaseDayButton
Spacer()
DatePicker("", selection: $displayDate, displayedComponents: .date)
.labelsHidden()
Spacer()
increaseDayButton
Spacer()
}
.padding(EdgeInsets(top: 25, leading: 0, bottom: 10, trailing: 0))
if filteredSessions.isEmpty {
Spacer()
Text("No Training Sessions found")
} else {
List {
ForEach(filteredSessions) { session in
sessionListItem(session: session)
}
}
}
Spacer()
}
.navigationTitle("Training Sessions")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: todayButton, trailing: addButton)
.sheet(isPresented: $presentAddSessionSheet) {
TrainingSessionEditView()
}
}
}
var filteredSessions : [TrainingSession] {
return sessions.filter { $0.date == dateManager.dateToStr(date: displayDate)}
}
}
struct TrainingSessionListView_Previews: PreviewProvider {
static var previews: some View {
TrainingSessionListView()
}
}
I am trying to follow the guidance in a WWDC video to use a #State struct to configure and present a child view. I would expect the struct to be able to present the view, however the config.show boolean value does not get updated when set by the button.
The code below has two buttons, each toggling a different boolean to show an overlay. Toggling showWelcome shows the overlay but toggling config.show does nothing. This seems to be working as intended, I just don't understand why SwiftUI behaves this way. Can someone explain why it's not functioning like I expect, and/or suggest a workaround?
https://developer.apple.com/videos/play/wwdc2020/10040/ # 5:14
struct InformationOverlayConfig {
#State var show = false
#State var title: String?
}
struct InformationOverlay: View {
#Binding var config: InformationOverlayConfig
var body: some View {
if config.title != nil {
Text(config.title!)
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 15))
}
}
}
struct TestView: View {
#State private var configWelcome = InformationOverlayConfig(title: "Title is here")
#State private var showWelcome = false
var body: some View {
VStack {
Text("hello world")
Spacer()
Button("Toggle struct parameter", action: {
configWelcome.show.toggle()
})
Button("Toggle boolean state", action: {
showWelcome.toggle()
})
}
.overlay(
VStack {
InformationOverlay(config: $configWelcome).opacity(configWelcome.show ? 1 : 0)
InformationOverlay(config: $configWelcome).opacity(showWelcome ? 1 : 0)
})
}
You "Config" is not a View. State variables only go in Views.
Also, do not use force unwrapping for config.title. Optional binding or map are the non-redundant solutions.
Lastly, there is no need for config to be a Binding if it actually functions as a constant for a particular view.
struct InformationOverlay: View {
struct Config {
var show = false
var title: String?
}
let config: Config
var body: some View {
VStack {
if let title = config.title {
Text(title)
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 15))
}
// or
config.title.map {
Text($0)
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 15))
}
}
}
}
struct TestView: View {
#State private var configWelcome = InformationOverlay.Config(title: "Title is here")
#State private var showWelcome = false
var body: some View {
VStack {
Text("hello world")
Spacer()
Button("Toggle struct parameter") {
configWelcome.show.toggle()
}
Button("Toggle boolean state") {
showWelcome.toggle()
}
}
.overlay(
VStack {
InformationOverlay(config: configWelcome).opacity(configWelcome.show ? 1 : 0)
InformationOverlay(config: configWelcome).opacity(showWelcome ? 1 : 0)
}
)
}
}
For some reason, my selectedTask State is Empty when presenting the Sheet,
even if I set it on the onTapGesture.
What I'm I missing?
struct TasksTabView: View {
#State private var showComputedTaskSheet: Bool = false
#State var selectedTask: OrderTaskCheck?
var body: some View {
VStack(alignment: .leading) {
List {
ForEach(Array(tasks.enumerated()), id:\.1.title) { (index, task) in
VStack(alignment: .leading, spacing: 40) {
HStack(spacing: 20) {
PillForRow(index: index, task: task)
}.padding(.bottom, 30)
}.onTapGesture {
// Where I'm setting selectedTask
self.selectedTask = task
self.showComputedTaskSheet.toggle()
}
}
}
}.listStyle(SidebarListStyle())
}
.sheet(isPresented: $showComputedTaskSheet) {
// self.selectedTask is returns nil
showScreen(task: self.selectedTask!)
}
.onAppear {
UITableView.appearance().backgroundColor = .white
}
}
Since I have no access to your full project this example can help you to get the idea, you can use .sheet() with item initializer like aheze said.
The advantage is here you pass optional to input item and you receive unwrapped safe value to work!
struct ContentView: View {
#State private var customValue: CustomValue?
var body: some View {
Button("Show the Sheet View") { customValue = CustomValue(description: "Hello, World!") }
.sheet(item: $customValue){ item in sheetView(item: item) }
}
func sheetView(item: CustomValue) -> some View {
return VStack {
Text(item.description)
Button("Close the Sheet View") { customValue = nil }.padding()
}
}
}
struct CustomValue: Identifiable {
let id: UUID = UUID()
var description: String
}
I'm having trouble changing views when a username and password is successfully entered. I know the username and password works because the print statement is executed. Its a basic login type form.
import SwiftUI
struct LoginView: View {
#ObservedObject var networkManager: NetworkManager = NetworkManager()
#State var username: String = ""
#State var password: String = ""
var body: some View {
NavigationView {
VStack {
Image("dashlogovert")
.resizable()
.scaledToFit()
.frame(width: 280.0, height: 280.0, alignment: .top)
Form {
TextField("Username", text: $username)
SecureField("Password", text: $password)
Button(action: {
self.AttempLogin()
})
{
Text("Login")
.fontWeight(.bold)
.frame(width: 300, height: 30, alignment: .center)
.font(.title)
.padding()
.background(Color(red: 132/255, green: 203/255, blue: 161/255))
.foregroundColor(.white)
}
}
}
.navigationBarTitle("Login")
}
}
func AttempLogin(){
self.networkManager.loginFunction(username: self.username, password: self.password){
if self.networkManager.loggedIn {
print("You are logging in");
Dashboard()
} else {
print("You aren't logging in");
FailedLogin()
}
}
}
}
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
LoginView()
}
}
Dashboard() is the new view I will take the user to when they successful logon, and fail is also a view.
Also what is the best way to do this if I have done it a really bad way.
You can pass back your logged in state from loginFunction() using #escaping closure if it’s asynchronous task, and once you have that state you can pass it back to your view body by using closure again as a parameter in AttemptLogin() function, and assign that new state value to #State property wrapper, which will call body refresh and check for updated state.
Check below code-:
import SwiftUI
enum LoginState:String {
case failed
case success
case unknownState
}
class NetworkManager:ObservableObject{
func loginFunction(username:String,password:String,closure: #escaping (String)->()){
closure(LoginState.failed.rawValue)
}
}
struct LoginView: View {
#ObservedObject var networkManager: NetworkManager = NetworkManager()
#State var username: String = ""
#State var password: String = ""
#State var isLoggedIn:String = LoginState.unknownState.rawValue
var body: some View {
NavigationView {
VStack {
Image("dashlogovert")
.resizable()
.scaledToFit()
.frame(width: 280.0, height: 280.0, alignment: .top)
Form {
TextField("Username", text: $username)
SecureField("Password", text: $password)
Button(action: {
self.AttempLogin { state in
isLoggedIn = state
}
})
{
Text("Login")
.fontWeight(.bold)
.frame(width: 300, height: 30, alignment: .center)
.font(.title)
.padding()
.background(Color(red: 132/255, green: 203/255, blue: 161/255))
.foregroundColor(.white)
}
if isLoggedIn == LoginState.success.rawValue{
Dashboard()
}
if isLoggedIn == LoginState.failed.rawValue{
FailedLogin()
}
}
}
.navigationBarTitle("Login")
}
}
func AttempLogin(_ closure: #escaping (String)->()){
self.networkManager.loginFunction(username: self.username, password: self.password){ loginState in
if loginState == LoginState.success.rawValue {
print("You are logging in");
closure(loginState)
}
if loginState == LoginState.failed.rawValue {
print("You aren't logging in");
closure(loginState)
}
}
}
}
struct Dashboard:View{
var body: some View{
Text("Dashboard")
}
}
struct FailedLogin:View{
var body: some View{
Text("Login")
}
}
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
LoginView()
}
}
I have taken NetworkManager and other classes as dummy on my side.You need to write your logic accordingly.
i want to signup users when they click the signup button in my app. When the signup is complete and successfully created a user on the server side I want to present the next screen and only then.
In normal way I have a PresentationButton and set the destination and when somebody clicked the button the next screen is presented directly, but now it's async.
How to handle that?
Currently I have this PresentationButton:
PresentationButton(
Text(isSignIn ? "SignIn" : "Next").font(.headline).bold()
.frame(width: 100)
.padding(10)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(20)
, destination: HomeScreen()
)
That's with the suggestion by Uro Arangino:
struct PasswordView : View {
#State private var password = ""
#State private var showNextScreen = false
var loginMode: LoginType
#ObjectBinding var signupManager = SignUpManager()
var body: some View {
VStack {
// if self.signupManager.showModal { self.showNextScreen.toggle() } <- Can't do this
TwitterNavigationView(backBtnOn: false)
middleView()
Spacer()
bottomView()
}
}
func bottomView() -> some View {
return VStack(alignment: .trailing) {
Divider()
BindedPresentationButton(
showModal: $showNextScreen,
label: getCustomText((Text("Registrieren"))),
destination: HomeTabView(),
onTrigger: {
let user = User(name: "rezo", username: "ja lol ey", profileDescription: "YAS", email: "dbjdb#dedde.de", telephoneNumber: nil, profileImage: UIImage(named: "twitter-logo")!, bannerImage: UIImage(systemName: "star")!)
self.signupManager.signIn(forUser: user, password: "ultraSecure", loginMode: .email)
// UserDefaults.standard.set(true, forKey: "loggedIn")
})
}.padding([.leading, .trailing]).padding(.top, 5).padding(.bottom, 10)
}
}
My bindable object class
final class SignUpManager: BindableObject {
let didChange = PassthroughSubject<SignUpManager, Never>()
var showModal: Bool = false {
didSet {
didChange.send(self)
}
}
func signIn(forUser user: User, password: String, loginMode: LoginType) {
if loginMode == .email {
LoginService.instance.signupWithEmail(user: user, andPassword: password, completion: handleCompletion)
} else {
LoginService.instance.login(withPhoneNumber: user.telephoneNumber!, completion: handleCompletion)
}
}
private func handleCompletion(_ status: Bool) {
if status {
showModal = true
}
}
}
You can use a presentation button with a binding.
See: https://stackoverflow.com/a/56547016/3716612
struct ContentView: View {
#State var showModal = false
var body: some View {
BindedPresentationButton(
showModal: $isSignIn,
label: Text(isSignIn ? "SignIn" : "Next")
.font(.headline)
.bold()
.frame(width: 100)
.padding(10)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(20),
destination: HomeScreen()
)
}
}