I've had a problem on and off for the past week where my else statement is executing in the MainTabView upon login/signup (meaning it can't find the currentuser??) So rather than logging in and showing the main navigation, I see a white "loading.." screen after logging in. It's odd cause some log in's have worked fine and others crash the app. Any help is very appreciated!
I don't think the problem is within EmailAuth or CreateAccountAuth but let me know if you'd like to see the code for those too.
AuthViewModel:
import SwiftUI
import FirebaseAuth
import FirebaseCore
import FirebaseStorage
import FirebaseFirestore
import FirebaseFirestoreSwift
class AuthViewModel: NSObject, ObservableObject {
#Published var userSession: FirebaseAuth.User?
#Published var currentUser: User?
#Published var selectedImage: UIImage?
private let service = UserService()
static let shared = AuthViewModel()
override init() {
super.init()
userSession = Auth.auth().currentUser
fetchUser()
}
func login(withEmail email: String, password: String) {
Auth.auth().signIn(withEmail: email, password: password) { result, error in
if let error = error {
print("DEBUG: Failed to sign in with error \(error.localizedDescription)")
return
}
self.userSession = result?.user
self.fetchUser()
}
}
func register(withEmail email: String, password: String, fullname: String) {
Auth.auth().createUser(withEmail: email, password: password) { result, error in
if let error = error {
print("DEBUG: Failed to register with error \(error.localizedDescription)")
return
}
guard let user = result?.user else { return }
self.userSession = user
let data: [String: Any] = ["email": email,
"fullname": fullname]
COLLECTION_USERS
.document(user.uid)
.setData(data)
self.uploadProfileImage(self.selectedImage)
}
}
func signOut() {
// sets user session to nil so we show login view
self.userSession = nil
// signs user out on server
try? Auth.auth().signOut()
}
func uploadProfileImage(_ image: UIImage?) {
guard let uid = userSession?.uid else { return }
ImageUploader.uploadImage(image: image) { profileImageUrl in
COLLECTION_USERS
.document(uid)
.updateData(["profileImageUrl": profileImageUrl])
//{ _ in self.userSession = user }
}
}
func fetchUser() {
guard let uid = userSession?.uid else { return }
COLLECTION_USERS.document(uid).getDocument { snapshot, _ in
guard let user = try? snapshot?.data(as: User.self) else { return }
self.currentUser = user
}
}
}
App File:
struct Page_TurnerApp: App {
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
NavigationView {
ContentView().environmentObject(AuthViewModel())
}
}
}
}
ContentView
struct ContentView: View {
#EnvironmentObject var viewModel: AuthViewModel
var body: some View {
Group {
if viewModel.userSession != nil {
MainTabView()
} else {
EmailAuth()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
MainTabView
struct MainTabView: View {
#State private var selectedIndex = 0
#EnvironmentObject var viewModel: AuthViewModel
var body: some View {
if let user = viewModel.currentUser {
TabView(selection: $selectedIndex) {
ExploreView()
.onTapGesture {
self.selectedIndex = 0
}
.tabItem {
Image(systemName: "house")
}.tag(0)
SearchView()
.onTapGesture {
self.selectedIndex = 1
}
.tabItem {
Image(systemName: "magnifyingglass")
}.tag(1)
ConversationsView()
.onTapGesture {
self.selectedIndex = 2
}
.tabItem {
Image(systemName: "message")
}.tag(2)
AccountView(user: user)
.onTapGesture {
self.selectedIndex = 3
}
.tabItem {
Image(systemName: "person.crop.circle")
}.tag(3)
}
} else {
Text("loading...")
}
}
}
My problem was that my User wasn't being created unless there was a profile image. That's why it worked for certain users and crashed for others. I changed let profileImageUrl: String
to let profileImageUrl: String? and all users can log in now
Related
I know I can use for each for this, but every time I try to implement according to documentation it throws some kind of error regarding syntax.
Here is my view:
import SwiftUI
import Combine
struct HomeTab: View {
#StateObject var callDevices = CallDevices()
var body: some View {
NavigationView {
devices
.onAppear {
callDevices.getDevices()
}
}
}
private var devices: some View {
VStack(alignment: .leading, spacing: nil) {
ForEach(content: callDevices.getDevices(), id: \.self) { device in
// i want to loop through and display here //
HStack{
Text(device.Name)
Text(device.Status)
}
}
Spacer()
}
}
}
struct HomeTab_Previews: PreviewProvider {
static var previews: some View {
HomeTab()
}
}
Here is my Call Devices which works without issue in other views:
class CallDevices: ObservableObject {
private var project_id: String = "r32fddsf"
#Published var devices = [Device]()
func getDevices() {
guard let url = URL(string: "www.example.com") else {return}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("Authorization")
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard error == nil else {print(error!.localizedDescription); return }
// guard let data = data else {print("empty data"); return }
let theData = try! JSONDecoder().decode(Welcome.self, from: data!)
DispatchQueue.main.async {
self.devices = theData.devices
}
}
.resume()
}
}
is the issue in the way I am calling my function?
try this:
(you may need to make Device Hashable)
private var devices: some View {
VStack(alignment: .leading, spacing: nil) {
ForEach(callDevices.devices, id: \.self) { device in // <-- here
// i want to loop through and display here //
HStack{
Text(device.Name)
Text(device.Status)
}
}
Spacer()
}
}
If Device is Identifiable, you can remove the id: \.self.
struct Device: Identifiable, Hashable, Codable {
let id = UUID()
var Name = ""
var Status = ""
// ... any other stuff you have
}
I have 2 tabs and the associated views are tabAView and tabBView.
On tabAView, 1 API call is there and got user object which is Published object in its ViewModel. ViewModel name is UserViewModel. UserViewModel is being observed by tabAView.
On tabBView, I have to use that user object. Because on some actions, user object value is changed, those changes should be reflected on subsequent views.
I am confused about the environment object usage here. Please suggest what will be the best approach.
Here is the code to understand better my problem.
struct ContentView: View {
enum AppPage: Int {
case TabA=0, TabB=1
}
#StateObject var settings = Settings()
var viewModel: UserViewModel
var body: some View {
NavigationView {
TabView(selection: $settings.tabItem) {
TabAView(viewModel: viewModel)
.tabItem {
Text("TabA")
}
.tag(AppPage.TabA)
AppsView()
.tabItem {
Text("Apps")
}
.tag(AppPage.TabB)
}
.accentColor(.white)
.edgesIgnoringSafeArea(.top)
.onAppear(perform: {
settings.tabItem = .TabA
})
.navigationBarTitleDisplayMode(.inline)
}
.environmentObject(settings)
}
}
This is TabAView:
struct TabAView: View {
#ObservedObject var viewModel: UserViewModel
#EnvironmentObject var settings: Settings
init(viewModel: UserViewModel) {
self.viewModel = viewModel
}
var body: some View {
Vstack {
/// code
}
.onAppear(perform: {
/// code
})
.environmentObject(settings)
}
}
This is the UserViewModel where API is hit and user object comes:
class UserViewModel: ObservableObject {
private var apiService = APIService.shared
#Published var user: EndUserData?
init () {
getUserProfile()
}
func getUserProfile() {
apiService.getUserAccount() { user in
DispatchQueue.main.async {
self.user = user
}
}
}
}
Below is the APIService function, where the user object is saved into UserDefaults for use. Which I know is incorrect.(That is why I am looking for another solution). Hiding the URL, because of its confidential.
func getUserAccount(completion: #escaping (EndUserData?) -> Void) {
self.apiManager.makeRequest(toURL: url, withHttpMethod: .get) { results in
guard let response = results.response else { return completion(nil) }
if response.httpStatusCode == 200 {
guard let data = results.data else { return completion(nil) }
do {
let str = String(decoding: data, as: UTF8.self)
print(str)
let decoder = JSONDecoder()
let responseData = try decoder.decode(ResponseData<EndUserData>.self, from: data)
UserDefaults.standard.set(data, forKey: "Account")
completion(responseData.data)
} catch let jsonError as NSError {
print(jsonError.localizedDescription)
return completion(nil)
}
}
}
}
This is another TabBView:
struct TabBView: View {
var user: EndUserData?
init() {
do {
guard let data = UserDefaults.standard.data(forKey: "Account") else {
return
}
let decoder = JSONDecoder()
let responseData = try decoder.decode(ResponseData<EndUserData>.self, from: data)
user = responseData.data
} catch let jsonError as NSError {
print(jsonError.localizedDescription)
}
}
var body: some View {
VStack (spacing: 10) {
UserSearch()
}
}
}
This is another view in TabBView, where the User object is used. Changes are not reflecting here.
struct UserSearch: View {
private var user: EndUserData?
init(comingFromAppsSection: Bool) {
do {
guard let data = UserDefaults.standard.data(forKey: "Account") else {
return
}
let decoder = JSONDecoder()
let responseData = try decoder.decode(ResponseData<EndUserData>.self, from: data)
user = responseData.data
} catch let jsonError as NSError {
print(jsonError.localizedDescription)
}
}
var body: some View {
Vstack {
Text(user.status)
}
}
}
I have removed most of the code from a confidential point of view but this code will explain the reason and error. Please look into the code and help me.
I'm struggling in a big way to get some basic code working that allows me to change a view in a Swiftui project.
I have 3 views: my default ContentView, a login screen and a main menu.
The project loads to ContentView which is just a logo. I have a boolean value which defaults to false and an extension function which either loads the login screen, or main menu dependent on the value of that boolean.
This part is working fine, project loads and i see the login page. The login button calls a function which does a URLsession, and depending on the returned value from that, sets the boolean flag to true or leaves it as false in the case of a failed login.
The bit im struggling with is getting the function to change the view. I can toggle the boolean flag in the function fine, but if I include a statement such as MainMenu() to load my main menu view, nothing happens.
I have experimented with observable objects and "subscribers" to try to get this working but i'm not sure if this is actually needed and I had no joy getting it working.
any help is greatly appreciated
Full code:
import SwiftUI
var isLoggedin = false
var authenticationFailure = false
func DoLogin(username: inout String, password: inout String){
print(isLoggedin)
let url = URL(string: "https://www.example.com/mobile/ios/test.php")!
var request = URLRequest(url: url)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
let parameters: [String: Any] = [
"username": username,
"password": password]
request.httpBody = parameters.percentEncoded()
let task = URLSession.shared.dataTask(with: request) { data,response,error in
guard let data = data,
let response = response as? HTTPURLResponse,
error == nil else{
print("error", error ?? "Unknown error")
return
}
guard (200 ... 299) ~= response.statusCode else {
print("statuscode should be 2xx, got \(response.statusCode)")
print("response = \(response)")
return
}
let responseString = String(data: data, encoding: .utf8)
if responseString == "1"{
print("Logged in")
isLoggedin = true
print(isLoggedin)
MainMenu()
}
else{
print("NO LOGIN")
isLoggedin = false
}
}
task.resume()
}
extension View {
#ViewBuilder func changeView(_ isLoggedin: Bool) -> some View {
switch isLoggedin {
case false: LoginView()
case true: MainMenu()
}
}
}
struct ContentView: View {
#State var isLoggedin = false
var body: some View {
Color.clear
.changeView(isLoggedin)
VStack{
Image("logo")
.padding(.bottom, 40)
}
}
}
struct LoginView: View {
#State var username: String = ""
#State var password: String = ""
#State var isLoggedin = false
var body: some View {
VStack{
Form{
TextField("Username: ", text:$username)
.frame(maxWidth: .infinity, alignment: .center)
.autocapitalization(.none)
SecureField("Password: ",text:$password)
Button("Login"){
DoLogin(username: &username, password: &password)
}
}
.padding(.top, 100)
}
}
}
struct MainMenu: View{
#State var isLoggedin = true
var body: some View{
VStack{
Text("Main Menu")
}
}
}
/*struct ContentView_Previews: PreviewProvider {
static var previews: some View {
/*ContentView() */
}
}*/
You have some problems with your code.
In your Content view
#State var isLoggedin = false
isn't being changed by anything inside the body of the struct, so it is always going to be false.
Your LoginView calls doLogin but it doesn't change any variables that the views use to render themselves. In the body of your doLogin method it is returning views, but it isn't returning them to anything.
Here is an example that does sort of what you want. shows different screens depending on state. SwiftUI shows views depending on states, so you need to change states to show different views. I've done this in one file so it's easier to show here:
import SwiftUI
class ContentViewModel: ObservableObject {
enum ViewState {
case initial
case loading
case login
case menu
}
#Published var username = ""
#Published var password = ""
#Published var viewState = ViewState.initial
var loginButtonDisabled: Bool {
username.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ||
password.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
func goToLogin() {
viewState = .login
}
func login() {
viewState = .loading
// I'm not actually logging in, just randomly simulating either a successful or unsuccessful login after a short delay
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if Bool.random() {
self.viewState = .menu
} else {
self.viewState = .login
}
}
}
}
struct ContentView: View {
#StateObject var viewModel = ContentViewModel()
var body: some View {
ZStack {
initialView
loginView
loadingView
menuView
}
}
private var initialView: InitialView? {
guard .initial == viewModel.viewState else { return nil }
return InitialView(viewModel: viewModel)
}
private var loginView: LoginView? {
guard .login == viewModel.viewState else { return nil }
return LoginView(viewModel: viewModel)
}
private var loadingView: LoadingView? {
guard .loading == viewModel.viewState else { return nil }
return LoadingView()
}
private var menuView: MenuView? {
guard .menu == viewModel.viewState else { return nil }
return MenuView()
}
}
struct InitialView: View {
#ObservedObject var viewModel: ContentViewModel
var body: some View {
VStack {
Text("Initial View")
.font(.largeTitle)
.padding()
Button("Login") { viewModel.goToLogin() }
}
}
}
struct LoginView: View {
#ObservedObject var viewModel: ContentViewModel
var body: some View {
VStack {
Text("Login View")
.font(.largeTitle)
.padding()
TextField("Username", text: $viewModel.username)
.padding()
TextField("Password", text: $viewModel.password)
.padding()
Button("Login") {viewModel.login() }
.padding()
.disabled(viewModel.loginButtonDisabled)
}
}
}
struct LoadingView: View {
var body: some View {
Text("Loading View")
.font(.largeTitle)
}
}
struct MenuView: View {
var body: some View {
Text("Menu View")
.font(.largeTitle)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This is all driven off one view model which publishes an enum state that is used by ContentView to show the different views. This is possible because in groups (such as the ZStack), a nil view is not rendered.
You can clone a project with this from https://github.com/Abizern/SO-68407322
I have a list of structs. Tapping a button will call a mutating function on a struct, then navigate.
The navigation however won't trigger. It'll start working again if the call to the mutating function self.logins[index].updateLastLogin() is removed. Why?
To reproduce, paste the following in an empty SwiftUI project:
struct Login: Identifiable, Hashable {
let id = UUID()
let name: String
var lastLogin: Date?
mutating func updateLastLogin() {
self.lastLogin = Date()
}
}
struct ContentView: View {
#State private var logins = ["Zaphod", "Arthur", "Ford", "Marvin", "Trillian"].map { Login(name: $0) }
#State private var selection: Login?
var body: some View {
NavigationView {
List {
ForEach(self.logins) { login in
VStack {
NavigationLink(destination: Text(login.name).font(.largeTitle),
tag: login,
selection: self.$selection) {
EmptyView()
}
Button(action: {
self.navigate(login: login)
}, label: {
Text(login.name)
})
}
}
}
}
}
func navigate(login: Login) {
guard let index = self.logins.firstIndex(of: login) else {
fatalError()
}
self.logins[index].updateLastLogin() // remove this line
self.selection = login
}
}
Here is a demo of working approach. Tested with Xcode 12 / iOS 14
struct DemoView: View {
#State private var logins = ["Zaphod", "Arthur", "Ford", "Marvin", "Trillian"].map { Login(name: $0) }
#State private var selection: UUID?
var body: some View {
NavigationView {
List {
ForEach(self.logins) { login in
VStack {
NavigationLink(destination: Text(login.name).font(.largeTitle),
tag: login.id,
selection: self.$selection) {
EmptyView()
}
Button(action: {
self.navigate(login: login)
}, label: {
Text(login.name)
})
}
}
}
}
}
func navigate(login: Login) {
guard let index = self.logins.firstIndex(of: login) else {
fatalError()
}
self.logins[index].updateLastLogin() // remove this line
self.selection = login.id
}
}
I want to show my new view after the success login process with Firebase, my signUp and Recover password are already working because I'm showing it as a sheet but in this one, I want to show a new view, I have tried with NavigationLink, with onReceive but I have been unable to do this work.
struct LoginView: View {
#ObservedObject var viewModel = ViewModel()
#State private var formOffset: CGFloat = 0
#State private var presentSignUpSheet = false
#State private var presentPasswordRecoverySheet = false
#State private var presentLobbySheet = false
var body: some View {
VStack {
HeaderView(title: Constants.appName)
Spacer()
Divider()
Group {
BodyView(value: viewModel).viewSelection(view: Constants.QuestionnaireView.signIn.rawValue)
LCButton(text: Constants.login) {
self.viewModel.signIn()
}.alert(isPresented: $viewModel.thereIsAnError) {
Alert(title: Text(Constants.alert), message: Text(viewModel.errorMessage), dismissButton: .default(Text(Constants.ok)))
}
Button(action: {
self.presentSignUpSheet.toggle()
}) {
Text(Constants.signUp)
}.sheet(isPresented: $presentSignUpSheet) {
SignUpView()
}.padding()
Button(action: {
self.presentPasswordRecoverySheet.toggle()
}) {
Text(Constants.forgotPassword)
}.sheet(isPresented: $presentPasswordRecoverySheet) {
RecoverPasswordView()
}.padding()
}
}.edgesIgnoringSafeArea(.top)
.padding()
.offset(y: self.formOffset)
}
}
class ViewModel: ObservableObject {
#Published var user = User()
#Published var confirmPassword = ""
#Published var thereIsAnError = false
#Published var errorMessage = ""
var viewDismissalModePublisher = PassthroughSubject<Bool, Never>()
var onSuccessLogin = PassthroughSubject<Bool, Never>()
private var shouldPopView = false {
didSet {
viewDismissalModePublisher.send(shouldPopView)
}
}
private var shouldShowLobbyView = false {
didSet {
onSuccessLogin.send(shouldShowLobbyView)
}
}
func registerSuccess() {
self.user.email = ""
self.user.password = ""
self.confirmPassword = ""
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.shouldPopView = true
}
}
func signUpProcess() {
if user.password != confirmPassword {
errorMessage = Constants.passConfirmWrong
thereIsAnError.toggle()
} else {
signUp()
}
}
func signUp() {
Auth.auth().createUser(withEmail: user.email, password: user.password) { (result, error) in
if error != nil {
self.errorMessage = error!.localizedDescription
self.thereIsAnError.toggle()
} else {
self.registerSuccess()
}
}
}
func signIn() {
Auth.auth().signIn(withEmail: user.email, password: user.password) { (result, error) in
if error != nil {
self.errorMessage = error!.localizedDescription
self.thereIsAnError.toggle()
} else {
self.shouldShowLobbyView.toggle()
self.user.email = ""
self.user.password = ""
}
}
}
func recoverPassword() {
Auth.auth().sendPasswordReset(withEmail: user.email) { (error) in
if error != nil {
self.errorMessage = error!.localizedDescription
self.thereIsAnError.toggle()
} else {
self.user.email = ""
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.shouldPopView = true
}
}
}
}
}
I had a similar problem and I found a solution that mimics some navigation
I was going to write the basics here but nothing better then posting the source with explanation.
ViewRouter Tutorial
This consists in a ObservableObject as EnvironmentObject and it allows you to show views in full screen, rather than sheets.
In your case you would have a LoginView and in this view you could open sheets for the Signup and Recover Password views and the LobbyView opening as fullscreen view, like it would in UIKit with the present method