How to conditionally load view upon app launch in SwiftUI? - ios

I'm currently trying to implement an auto-login feature to my app using UserDefaults. What I would like to do before loading any view is get the UserDefaults email and password and call the login function from my API. If successful, go to Home view, else go to LoginView. My apologies, I'm very new to Swift and on a tight schedule with my project. Here is my code segment. I'm not sure where I can add my logic:
import SwiftUI
#main
struct MyApp: App {
init() {
let email = UserDefaults.standard.string(forKey: "email");
let pw = UserDefaults.standard.string(forKey: "pw");
let api = MyAppAPI()
api.signInUser(email: email, password: pw) { result in
//JSON response contains an 'isError' field
let isError = result.value(forKey: "error") as! Bool
if !isError {
//successful login - what to do from here?
}
}
}
var body: some Scene {
WindowGroup {
LoginView()
}
}
}

Here is a simple way of doing this, you can do this onAppear
import SwiftUI
struct ContentView: View {
let email: String
let pass: String
init() {
self.email = UserDefaults.standard.string(forKey: "email") ?? ""
self.pass = UserDefaults.standard.string(forKey: "pw") ?? ""
}
#State private var result: Bool?
var body: some View {
Group {
if let unwrappedResult: Bool = result {
if unwrappedResult {
Text("Home View, Welcome!")
}
else {
Text("Wrong User or Pass, try again!")
}
}
else {
Text("loading...")
}
}
.onAppear() { loginFunction(email: email, pass: pass) { value in result = value } }
}
}
func loginFunction(email: String, pass: String, completion: #escaping (Bool) -> Void) {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(3000)) { completion(Bool.random()) }
}

Related

How can I display name of User object in ProfileViewController

I need to display user.username on Text in ProfileView but I got error when I try to fill current user with User. I have to get User in currentUser var.
import Foundation
import FirebaseAuth
import Firebase
class AuthViewModel: ObservableObject
{
#Published var userSession: FirebaseAuth.User?
#Published var currentUser: User?
private var tempUserSession: FirebaseAuth.User?
private let service = UserService()
init()
{
self.userSession = Auth.auth().currentUser
}
func login(withEmail email: String, password: String)
{
Auth.auth().signIn(withEmail: email, password: password) { authResult, error in
if let e = error
{
print(e.localizedDescription)
}
else
{
guard let user = authResult?.user else {return}
self.userSession = user
guard let uid = self.userSession?.uid else { return }
self.service.fetchUser(withUid: uid)
print("Did User log IN")
}
}
}
func fetchUser()
{
guard let uid = self.userSession?.uid else { return }
service.fetchUser(withUid: uid) { user in <----- HERE I GOT AN ERROR Extra trailing closure passed in call
self.currentUser = user
}
}
}
import Foundation
import UIKit
import FirebaseAuth
import SwiftUI
class ProfileViewController: UIViewController
{
var authViewModel = AuthViewModel()
#IBOutlet weak var userNameLabel: UILabel!
override func viewDidLoad()
{
navigationItem.hidesBackButton = true
userNameLabel.text = authViewModel.currentUser?.username // HERE I WANT TO DISPLAY CURRENT USER - USERNAME
print(userNameLabel.text)
}
}
I tried fill currentuser with user but nothings worked. Still in profile view controller I got nill. (currentUser.username = nil)
The code calls fetchUser like it's an asynchronous function and it's not; it's a synchronous function that does not return a value, nor has an escaping completion handler. So that's the cause of the error.
Here's how I would do it. Start with a a simple user class
class MyUser {
var userName = ""
var uid = ""
}
and then a simplified fetchUser using async/await
func fetchUser() {
Task {
let uid = "uid_0"
let foundUser = await self.getUserAsync(withUid: "uid_0")
print(foundUser.userName)
}
}
and then the code to fetch the user from Firestore, instantiate a MyUser object and return it
func getUserAsync(withUid: String) async -> MyUser {
let usersCollection = self.db.collection("users") //self.db points to my firestore
let thisUserDoc = usersCollection.document(withUid)
let snapshot = try! await thisUserDoc.getDocument()
let user = MyUser()
user.userName = snapshot.get("userName") as? String ?? "No Name"
user.uid = withUid
return user
}

SwiftUI Navigation - List loading multiple time after navigating from details

I am creating a SwiftUI List with Details.
This list is fetching JSON data from Firebase Realtime. The data consist of 5 birds with an ID, a name and an image URL.
My problem is the following:
Each time I click on the back button after I navigate to details, the data get doubled every single time, what am I doing wrong? (see screenshots).
I am using MVVM design pattern, I am listening and removing that listener every time the View appears and disappears.
Please, find the code below:
Main View:
var body: some View {
NavigationStack {
List(viewModel.birds) { bird in
NavigationLink(destination: DetailsView(bird: bird)) {
HStack {
VStack(alignment: .leading) {
Text(bird.name).font(.title3).bold()
}
Spacer()
AsyncImage(url: URL(string: bird.imageURL)) { phase in
switch phase {
// downloading image here
}
}
}
}
}.onAppear {
viewModel.listentoRealtimeDatabase()
}
.onDisappear {
viewModel.stopListening()
}.navigationTitle("Birds")
}
}
DetailsView:
struct DetailsView: View {
var bird: Bird
var body: some View {
Text("\(bird.name)")
}
}
Model:
struct Bird: Identifiable, Codable {
var id: String
var name: String
var imageURL: String
}
View Model:
final class BirdViewModel: ObservableObject {
#Published var birds: [Bird] = []
private lazy var databasePath: DatabaseReference? = {
let ref = Database.database().reference().child("birds")
return ref
}()
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
func listentoRealtimeDatabase() {
guard let databasePath = databasePath else {
return
}
databasePath
.observe(.childAdded) { [weak self] snapshot in
guard
let self = self,
var json = snapshot.value as? [String: Any]
else {
return
}
json["id"] = snapshot.key
do {
let birdData = try JSONSerialization.data(withJSONObject: json)
let bird = try self.decoder.decode(Bird.self, from: birdData)
self.birds.append(bird)
} catch {
print("an error occurred", error)
}
}
}
func stopListening() {
databasePath?.removeAllObservers()
}
}
screenshot how it should be

Building a userModel in swiftui

I'm currently experiencing a problem with my app. I store user data in Firebase and when the user ir using the app, and some of the details change in the user model, the user gets redirected to the Root view of the app. I tried creating an observable object, but i think that i didn't do it that well.
Currently, my setup of fetching users looks like this:
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)
self.users.append(user)
}
}
ContentView:
struct ContentView: View {
#EnvironmentObject var session : SessionStore
#State var userState = UserData()
#State private var functionResult = false
func getUser(){
DispatchQueue.main.async {
session.listen()
session.getUserFromUID { (fetcheduser) in
DispatchQueue.main.async {
self.functionResult.toggle()
self.userState = fetcheduser
print("Auth State Changed")
print(functionResult)
}
}
}
}
var body: some View {
VStack{
if (session.session != nil) {
if(functionResult == true){
MainView(user: userState)
}
else {
DelayedLaunchScreen(user: userState)
}
}
else{
HomeView()
}
}.onAppear{
getUser()
}
}
}
MainView:
UIKitTabView {
CandidateListView().tab(title: "Kandidatai", image: "person.2")
ChatsView(session: self.session).tab(title: "Žinutės", image: "message")
ChatsHomeView(session: self.session, user: self.user).environmentObject(session).tab(title: "Darbo skelbimai", image: "newspaper")
ProfileMenuView(session: self.session, user: self.user).tab(title: "Paskywra", image: "person")
//CreateJobView(user: session.getUserFrom(uid: session.uid), session: self.session).tab(title: "Paskywra", image: "person")
}
I'm guessing the problem is, that in the ContentView i have a function to fetch the user data and delay it with a fake launch screen. I dont really understand, why after a change in database the user gets popped to the root view. Is this because the user is being defined in the ContentView? Is there any other way to do this without causeing the problem? Any ideas why is this happening?
Thanks guys!
Looks like your problem is in how you structured the if that is depending on the functionResult bool:
if (functionResult == true) {
MainView(user: userState)
} else {
DelayedLaunchScreen(user: userState)
}
In your session.getUserFromUID you are toggling this state property that subsequently invalidates the ContentView that redraws the MainView if true or the DelayedLaunchScreen.
Basically every time the function is invoked (or fired if it's a listener) you are destroying what was rendered before. This might explain why it seems you are being popped out in the navigation stack.
You should be able to solve the problem by removing the functionResult property or by just using it once.
If this doesn't solve the issue, there must be something to do in the MainView and in how the userState is used to draw the views or maybe in that if session.session (maybe you can add more code to your question).

Passed in data MVVM RxSwift

I want to learn MVVM RxSwift with input and output method, I want to get a username from textfield.
I have a scenario when user not enter a username it will present an error and when user enter a username it will present in viewController.
This is when I confuse. I got the error message and successfully present error but, how can I catch the query in my viewModel and passed the data to viewController.
This is how I setup my searchViewModel
protocol ViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}
class SearchViewModel: ViewModelType {
// MARK: Binding
struct Input {
let searchText: Observable<String>
let validate: Observable<Void>
}
struct Output {
let username: Driver<String>
}
func transform(input: Input) -> Output {
let username = input.validate
.withLatestFrom(input.searchText)
.map { query in
if query.isEmpty {
return "Please enter a username. We need to know who to look for"
} else {
return query
}
}.asDriver(onErrorJustReturn: "")
return Output(username: username)
}
}
and this is my viewDidLoad in SearchViewController
let searchTextField = GFTextField()
let calloutBtn = GFButton(backgroundColor: .systemGreen, title: "Get followers")
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
setupImageView()
setupTextfield()
setupCalloutBtn()
let input = SearchViewModel.Input(
searchText: searchTextField.rx.text.orEmpty.asObservable(),
validate: calloutBtn.rx.tap.asObservable())
let output = viewModel.transform(input: input)
output.username.drive { [weak self] username in
guard let self = self else { return }
self.presentGFAlertOnMainThread(title: "Empty Username", message: username, buttonTitle: "Dismiss")
}.disposed(by: disposeBag)
}
It depends on what you want to do with the text of course, but below I assume you want to make a network request. But of course that requires understanding what your API layer looks like. I have to make some assumptions there as well, but the key is that you need to inject your API layer into your view model through its constructor/init method.
Like this:
class SearchViewModel: ViewModelType {
struct Input {
let username: Observable<String>
let getFollowers: Observable<Void>
}
struct Output {
let errorMessage: Driver<String>
let followers: Driver<[Follower]>
}
let api: API
init(api: API) {
self.api = api
}
func transform(input: Input) -> Output {
let followersResponse = input.getFollowers
.withLatestFrom(input.username)
.filter { !$0.isEmpty }
.map { makeEndpoint(using: $0) }
.flatMap { [api] in
api.load($0)
}
.share()
let missingName = input.getFollowers
.withLatestFrom(input.username)
.compactMap { $0.isEmpty ? "Please enter a username. We need to know who to look for" : nil }
let errorMessage = Observable.merge(
api.error.map { $0.localizedDescription },
missingName
)
.asDriver(onErrorJustReturn: "")
let followers = followersResponse
.asDriver(onErrorJustReturn: [])
return Output(errorMessage: errorMessage, followers: followers)
}
}
-- EDIT --
If all you want to do is push the non-empty text field back to the view controller, then it would look like this:
class SearchViewModel: ViewModelType {
struct Input {
let username: Observable<String>
let getFollowers: Observable<Void>
}
struct Output {
let errorMessage: Driver<String>
let username: Driver<String>
}
func transform(input: Input) -> Output {
let errorMessage = input.getFollowers
.withLatestFrom(input.username)
.compactMap { $0.isEmpty ? "Please enter a username. We need to know who to look for" : nil }
.asDriver(onErrorJustReturn: "")
let username = input.getFollowers
.withLatestFrom(input.username)
.filter { !$0.isEmpty }
.asDriver(onErrorJustReturn: "")
return Output(errorMessage: errorMessage, username: username)
}
}
The key here is that you need a Driver for each output that the view controller will want to subscribe to.

SwiftUI Updating EnvironmentObject outside the view

I have a model called UserData which is used only for the current logged in user. It saves data as profile data, user id, reference to Firestore document, etc.
I'm injecting this as an #EnvironmentObject so I can have access to it everywhere.
final class UserData: ObservableObject {
#Published var profile = Profile.default
#Published var loggedIn: Bool = Auth.auth().currentUser != nil ? true : false
#Published var uid: String = ""
#Published var documentReference: DocumentReference = Firestore.firestore().document("")
}
I also have a UserViewModel with methods on it.
class UserViewModel: ObservableObject {
func login(email: String, password: String, completion: #escaping (Response) -> Void) {
AuthService().login(email: email, password: password) { (response: Response) in
// let error = response.error
completion(response)
}
}
func register(email: String, password: String, firstName: String, lastName: String, completion: #escaping (Response) -> Void) {
AuthService().register(email: email, password: password, firstName: firstName, lastName: lastName) { (response: Response) in
// let error = response.error
completion(response)
}
}
func logout() {
try? Auth.auth().signOut()
}
}
The issue arrives in the View
#EnvironmentObject var userData: UserData
var userViewModel = UserViewModel()
func login() {
if self.email == "" || self.password == "" {
self.error = "You must provide both an email and password"
} else {
self.userViewModel.login(email: self.email, password: self.password) { response in
self.userData.profile = response.profile!
self.userData.uid = response.uid
self.userData.documentReference = response.documentReference ?? Firestore.firestore().document("")
UserDefaults.standard.set(response.userDefaults, forKey: "userDefaults")
}
}
}
As you can see I can not update this UserModel so easily. I have to update each of its properties one by one and also for the registration I have to do the same thing. Same thing in the SceneDelegate. I have the same lines in three different places, login view, registration view, and SceneDelegate
self.userData.profile = response.profile!
self.userData.uid = response.uid
self.userData.documentReference = response.documentReference ?? Firestore.firestore().document("")
UserDefaults.standard.set(response.userDefaults, forKey: "userDefaults")
Is there a better way to handle such things? Like having a single place to update this #EnvironmentObject. They are really cool to use them in the view but updating them outside the view is a real pain. Also It doesn't work well with the UserViewModel since I can not pass UserData to the UserViewModel.
How can I properly use this #EnvironmentObject called UserData

Resources