Async Next Screen Presentation in SwiftUI - ios

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()
)
}
}

Related

Button not running the action

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 {
// ...
}
}

Animation not working inside sheet for swiftui

I am using a sheet to present a list of options and on click of the option I want to change the view with the animation of sliding from trailing. As per my understanding and what I have read on various sites I have written this code but I am not sure why it is not working the way intended. I just want to know where exactly this code went wrong.
struct XYZ: App {
let persistenceController = PersistenceController.shared
#State var isPresented : Bool = false
#State var isSwiped : Bool = false
var body: some Scene {
WindowGroup {
optionList(isPresented: $isPresented)
.sheet(isPresented: $isPresented, content: {
Text("This is from modal view!")
.onTapGesture {
withAnimation(Animation.easeIn(duration: 10)){
isSwiped.toggle()
}
}
if isSwiped {
checkedList()
.transition(.move(edge: .trailing))
}
})
}
}
}
struct optionList : View {
#Binding var isPresented : Bool
var body: some View {
Text("This is a testView")
.onTapGesture {
withAnimation{
isPresented.toggle()
}
}
}
}
struct checkedList : View {
#State var name : String = "WatchList"
var arr = ["First", "Second", "Third", "Fourth", "Fifth", "Sixth", "Seventh"]
#State var mp : [Int:Int] = [:]
var body: some View {
VStack{
HStack{
TextField("WatchlistName", text: $name)
.padding(.all)
Image(systemName: "trash.fill")
.padding(.all)
.onTapGesture {
print("Delete watchList!!")
}
}
ScrollView{
ForEach(arr.indices) { item in
HStack (spacing: 0) {
Image(systemName: mp.keys.contains(item) ? "checkmark.square" : "square")
.padding(.horizontal)
Text(arr[item])
}
.padding(.bottom)
.frame(width: UIScreen.main.bounds.width, alignment: .leading)
.onTapGesture {
if mp.keys.contains(item) {
mp[item] = nil
} else {
mp[item] = 1
}
}
}
}
Button {
print("Remove Ticked Elements!")
deleteWatchListItem(arr: Array(mp.keys))
} label: {
Text("Save")
}
}
}
func deleteWatchList(ind: Int){
print(ind)
}
func deleteWatchListItem(arr : [Int]) {
print(arr)
}
}
I tried to create a view and with the animation using withanimation with a bool variable tried to change the view.
It sounds like what you want is to push the checkedList on to a NavigationStack…
struct ContentView: View {
#State var isPresented : Bool = false
var body: some View {
Text("This is a testView")
.onTapGesture {
isPresented.toggle()
}
.sheet(isPresented: $isPresented, content: {
NavigationStack {
NavigationLink("This is from modal view!") {
checkedList()
}
}
})
}
}

KeyboardSceneDelegate: Animation styles were not empty on user driven presentation

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?
}

Click on button is not opening the view

I am working on an app and in the guest screen, I have created 2 button to register and login
the button is defined as:
Button( action: {
RegisterLoginView(isLoginScreen: false)
}){
MyButtonView(stringOfButton: register, isDarkButton: true)
.padding(.leading, 10)
}
First, Xcode complains that Result of 'RegisterLoginView' initializer is unused but my biggest concern is that when I press on the button, I thought that RegisterLoginView will be opened but nothing happened. Any idea ?
guest view:
var body: some View {
VStack {
TabView {
GuestFlowImageTab(title: title1,
desc: desc1,
image: "houseprice")
GuestFlowImageTab(title: title2,
desc: desc2,
image: "invest")
GuestFlowFormTab()
}
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
HStack {
Button( action: {
RegisterLoginView(isLoginScreen: false)
}){
MyButtonView(stringOfButton: register, isDarkButton: true)
.padding(.leading, 10)
}
Button( action: {
RegisterLoginView(isLoginScreen: true)
}){
MyButtonView(stringOfButton: login, isDarkButton: false)
.padding(.trailing, 10)
}
}
}
and the RegisterLoginView is defined as below:
struct RegisterLoginView: View {
let titleTxt = "My Realtor"
let emailTxt = "Email"
let passwordTxt = "Password"
let nameTxt = "Name"
let loginBtn = "Sign In"
let registerBtn = "Sign Up"
#State var isLoginScreen: Bool
#State private var name = ""
#State private var email = ""
#State private var password = ""
var body: some View {
VStack() {
Text(titleTxt)
Image("iconhouseorange")
if(isLoginScreen) { TextField(nameTxt, text: self.$name) }
TextField(emailTxt, text: self.$email)
TextField(passwordTxt, text: self.$password)
Button(isLoginScreen ? loginBtn : registerBtn) {}
}
}
}
struct RegisterLoginView_Previews: PreviewProvider {
static var previews: some View {
RegisterLoginView(isLoginScreen: true)
}
}
Thanks for your help
You should need to use NavigationLink instead of a button for redirection.
NavigationLink(destination: RegisterLoginView(isLoginScreen: false)) {
MyButtonView(stringOfButton: register, isDarkButton: true)
.padding(.leading, 10)
}
Also, if your parent view (guest view) is not under navigation view then wrapped your view with navigation view.
Guest View
var body: some View {
NavigationView {
VStack {
// --- Other code ----
}
}
}

SwiftUI/Combine no updates to #ObservedObject from #Published

I have a logon screen followed by an overview screen. When the user is successful at logon, the logon response sends back a list of items, which I want to display on the subsequent overview screen.
I can see the response being successfully mapped but the overview view is not receiving any update to the #ObservedObject. I could be missing something obvious but I've been through a bunch of articles and haven't managed to get anything working. Any help appreciated!
Logon view
import SwiftUI
struct LogonView: View {
#State private var username: String = ""
#State private var password: String = ""
#State private var inputError: Bool = false
#State private var errorMessage: String = ""
#State private var loading: Bool? = false
#State private var helpShown: Bool = false
#State private var successful: Bool = false
//MARK:- UIView
var body: some View {
NavigationView {
VStack {
VStack {
TextField("Username", text: $username)
.padding(.horizontal)
.disabled(loading! ? true : false)
Rectangle()
.frame(height: 2.0)
.padding(.horizontal)
.foregroundColor(!inputError ? Color("SharesaveLightGrey") : Color("SharesaveError"))
.animation(.easeInOut)
}.padding(.top, 80)
VStack {
SecureField("Password", text: $password)
.padding(.horizontal)
.disabled(loading! ? true : false)
Rectangle()
.frame(height: 2.0)
.padding(.horizontal)
.foregroundColor(!inputError ? Color("SharesaveLightGrey") : Color("SharesaveError"))
.animation(.easeInOut)
}.padding(.top, 40)
if (inputError) {
HStack {
Text(errorMessage)
.padding(.top)
.padding(.horizontal)
.foregroundColor(Color("SharesaveError"))
.animation(.easeInOut)
.lineLimit(nil)
.font(.footnote)
Spacer()
}
}
SharesaveButton(action: {self.submit(user: self.username, pass: self.password)},
label: "Log on",
loading: $loading,
style: .primary)
.padding(.top, 40)
.animation(.interactiveSpring())
NavigationLink(destination: OverviewView(), isActive: $successful) {
Text("")
}
Spacer()
}
.navigationBarTitle("Hello.")
.navigationBarItems(
trailing: Button(action: { self.helpShown = true }) {
Text("Need help?").foregroundColor(.gray)
})
.sheet(isPresented: $helpShown) {
SafariView( url: URL(string: "http://google.com")! )
}
}
}
//MARK:- Functions
private func submit(user: String, pass: String) {
loading = true
inputError = false
let resultsVM = ResultsViewModel()
resultsVM.getGrants(user: user, pass: pass,
successful: { response in
self.loading = false
if ((response) != nil) { self.successful = true }
},
error: { error in
self.inputError = true
self.loading = false
self.successful = false
switch error {
case 401:
self.errorMessage = "Your credentials were incorrect"
default:
self.errorMessage = "Something went wrong, please try again"
}
},
failure: { fail in
self.inputError = true
self.loading = false
self.successful = false
self.errorMessage = "Check your internet connection"
})
}
}
Results View Model
import Foundation
import Moya
import Combine
import SwiftUI
class ResultsViewModel: ObservableObject {
#Published var results: Results = Results()
func getGrants(
user: String,
pass: String,
successful successCallback: #escaping (Results?) -> Void,
error errorCallback: #escaping (Int) -> Void,
failure failureCallback: #escaping (MoyaError?) -> Void
)
{
let provider = MoyaProvider<sharesaveAPI>()
provider.request(.getSharesave(username: user, password: pass)) { response in
switch response.result {
case .success(let value):
do {
let data = try JSONDecoder().decode(Results.self, from: value.data)
self.results = data
successCallback(data)
} catch {
let errorCode = value.statusCode
errorCallback(errorCode)
}
case .failure(let error):
failureCallback(error)
}
}
}
}
Overview View
import SwiftUI
import Combine
struct OverviewView: View {
#ObservedObject var viewModel: ResultsViewModel = ResultsViewModel()
var body: some View {
let text = "\(self.viewModel.results.market?.sharePrice ?? 0.00)"
return List {
Text(text)
}
}
}
You submitted request to one instance of ResultsViewModel
private func submit(user: String, pass: String) {
loading = true
inputError = false
let resultsVM = ResultsViewModel() // << here
by try to read data from another instance of ResultsViewModel
struct OverviewView: View {
#ObservedObject var viewModel: ResultsViewModel = ResultsViewModel() // << here
but it must be the one instance, so modify as follows
1) In OverviewView
struct OverviewView: View {
#ObservedObject var viewModel: ResultsViewModel // expects injection
2) In LogonView
struct LogonView: View {
#ObservedObject var resultsViewModel = ResultsViewModel() // created once
and inject same instance for OverviewView
NavigationLink(destination: OverviewView(viewModel: self.resultsViewModel), isActive: $successful) {
Text("")
}
and in submit
private func submit(user: String, pass: String) {
loading = true
inputError = false
let resultsVM = self.resultsViewModel // use created
Please try after change in initialise OverviewView like below in NavigationLink
NavigationLink(destination: OverviewView(viewModel: self.resultsVM),
isActive: $successful) {
Text("")
}
OR
pass results in OverviewView as argument like below
NavigationLink(destination: OverviewView(results: self.resultsVM.results),
isActive: $successful) {
Text("")
}
.....
struct OverviewView: View {
let results: Results
var body: some View {
let text = "\(self.results.market?.sharePrice ?? 0.00)"
return List {
Text(text)
}
}
}

Resources