Building a userModel in swiftui - ios

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).

Related

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

How to conditionally load view upon app launch in SwiftUI?

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

SwiftUI: How to set UserDefaults first time view renders?

So I have this code, where I fetch a url from firestore and then append it to an array, which is then stored in userDefaults(temporarily).
In the view I basically just iterate over the array stored in userdefaults and display the images.
But the problem is, that I have to rerender the view before the images show.
How can i fix this?
struct PostedImagesView: View {
#State var imagesUrls : [String] = []
#ObservedObject var postedImagesUrls = ProfileImages()
var body: some View {
VStack{
ScrollView{
ForEach(postedImagesUrls.postedImagesUrl, id: \.self) { url in
ImageWithURL(url)
}
}
}
.onAppear{
GetImage()
print("RAN GETIMAGE()")
}
}
// Get Img Url from Cloud Firestore
func GetImage() {
guard let userID = Auth.auth().currentUser?.uid else { return }
let db = Firestore.firestore()
db.collection("Users").document(userID).collection("PostedImages").document("ImageTwoTester").getDocument { (document, error) in
if let document = document, document.exists {
// Extracts the value of the "Url" Field
let imageUrl = document.get("Url") as? String
UserDefaults.standard.set([], forKey: "postedImagesUrls")
imagesUrls.append(imageUrl!)
UserDefaults.standard.set(imagesUrls, forKey: "postedImagesUrls")
} else {
print(error!.localizedDescription)
}
}
}
}

Returning Nil When Running Method from separate class [duplicate]

This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 2 years ago.
I have a View Controller that attempts to call a method from my UserModel class which gets a user document and fits the return data into a User structure. However, it is telling me it unexpectedly finds nil when unwrapping an optional value.
My UserModel:
class UserModel {
var user:User?
func getUser(userId: String) -> User? {
let docRef = Firestore.firestore().collection("Users").document(userId)
// Get data
docRef.getDocument { (document, error) in
if let document = document, document.exists {
var user:User = User(name: document["name"] as! String, phone: document["phone"] as! String, imageUrl: document["imageUrl"] as! String)
} else {
print("Document does not exist")
}
}
return user!
}
}
My Structure:
struct User {
var name:String
var phone:String
var imageUrl:String
}
My ViewController:
override func viewDidLoad() {
super.viewDidLoad()
userId = Auth.auth().currentUser?.uid
}
override func viewDidAppear(_ animated: Bool) {
let model = UserModel()
user = model.getUser(userId: userId!)
print(user?.name)
}
The method runs fine when it is inside my View Controller, so I know it's getting the uid, the database call works, and the values all exist. I have printed them all separately. However, within its own class it doesn't work.
Any ideas?
It looks like getDocument is an async function. Hence, you should make getUser async:
func getUser(userId: String, completion: #escaping (User?) -> Void) {
let docRef = Firestore.firestore().collection("Users").document(userId)
// Get data
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let user:User = User(name: document["name"] as! String, phone: document["phone"] as! String, imageUrl: document["imageUrl"] as! String)
completion(user)
} else {
completion(nil)
}
}
}
This is how you should call it:
let model = UserModel()
model.getUser(userId: userId!) { user in
print(user?.name)
}

SwiftUI Use EnvironmentObject as a model for my ViewModel

I have an EnvironmentObject which stores user data. After making a call to Firestore I'm filling it with data and using it everywhere. The problem occurs in my view model. I can easily retrieve data from this model but for more complex things I need to use a view model.
Can EnvironmentObject be used as a model for a view model or it should be used only for local preferences in the app, like storing some default values or preferences?
Is it better to use a separate model for my UserViewModel?
While it is very easy to access its data, It is very hard to fill it with dynamic data after making an external call for example. Especially in the SceneDelegate, where it's almost impossible because I can not make a network call there.
SceneDelegate
let userData = UserData()
Auth.auth().addStateDidChangeListener { (auth, user) in
if user != nil {
if let userDefaults = UserDefaults.standard.dictionary(forKey: "userDefaults") {
userData.profile = Profile(userDefaults: userDefaults)
userData.uid = userDefaults["uid"] as? String ?? ""
userData.documentReference = Firestore.firestore().document(userDefaults["documentReference"] as! String)
userData.loggedIn = true
}
}
}
window.rootViewController = UIHostingController(rootView: tabViewContainerView.environmentObject(userData))
UserData
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("")
#Published var savedItems = [SavedItem]()
init(document: DocumentSnapshot? = nil) {
if let document = document {
print("document")
let messagesDataArray = document["saved"] as? [[String: Any]]
let parsedMessaged = messagesDataArray?.compactMap {
return SavedItem(dictionary: $0)
}
self.savedItems = parsedMessaged ?? [SavedItem]()
}
}
}
UserViewModel
class UserViewModel: ObservableObject {
#Published var userData: UserData?
init(userData: UserData? = nil) {
self.userData = userData
}

Resources