Firestore method in view model not invoking in SwiftUI onAppear method - swift

I want to implement a Text field that displays the current user's existing score in the DB (Firestore). Because of the nature of async in Firebase query, I also need to do some adjustment in my codes. However, it seems that completion() handler does not work well:
// ViewModel.swift
import Foundation
import Firebase
import FirebaseFirestore
class UserViewModel: ObservableObject {
let current_user_id = Auth.auth().currentUser!.uid
private var db = Firestore.firestore()
#Published var xp:Int?
func fetchData(completion: #escaping () -> Void) {
let docRef = db.collection("users").document(current_user_id)
docRef.getDocument { snapshot, error in
print(error ?? "No error.")
self.xp = 0
guard let snapshot = snapshot else {
completion()
return
}
self.xp = (snapshot.data()!["xp"] as! Int)
completion()
}
}
}
// View.swift
import SwiftUI
import CoreData
import Firebase
{
#ObservedObject private var users = UserViewModel()
var body: some View {
VStack {
HStack {
// ...
Text("xp: \(users.xp ?? 0)")
// Text("xp: 1500")
.fontWeight(.bold)
.padding(.horizontal)
.foregroundColor(Color.white)
.background(Color("Black"))
.clipShape(CustomCorner(corners: [.bottomLeft, .bottomRight, .topRight, .topLeft], size: 3))
.padding(.trailing)
}
.padding(.top)
.onAppear() {
self.users.fetchData()
}
// ...
}
}
My result kept showing 0 in Text("xp: \(users.xp ?? 0)"), which represents that the step is yet to be async'ed. So what can I do to resolve it?

I would first check to make sure the data is valid in the Firestore console before debugging further. That said, you can do away with the completion handler if you're using observable objects and you should unwrap the data safely. Errors can always happen over network calls so always safely unwrap anything that comes across them. Also, make use of the idiomatic get() method in the Firestore API, it makes code easier to read.
That also said, the problem is your call to fetch data manually in the horizontal stack's onAppear method. This pattern can produce unsavory results in SwiftUI, so simply remove the call to manually fetch data in the view and perform it automatically in the view model's initializer.
class UserViewModel: ObservableObject {
#Published var xp: Int?
init() {
guard let uid = Auth.auth().currentUser?.uid else {
return
}
let docRef = Firestore.firestore().collection("users").document(uid)
docRef.getDocument { (snapshot, error) in
if let doc = snapshot,
let xp = doc.get("xp") as? Int {
self.xp = xp
} else if let error = error {
print(error)
}
}
}
}
struct ContentView: View {
#ObservedObject var users = UserViewModel()
var body: some View {
VStack {
HStack {
Text("xp: \(users.xp ?? 0)")
}
}
}
}
SwiftUI View - viewDidLoad()? is the problem you ultimately want to solve.

Related

onDelete causing NSRangeException

In this app, there is a main screen (WorkoutScreen) that displays the contents of a list one at a time as it iterates through the list (current workout in a list of many). In a popOver, a list that contains all of the workouts appears and has the ability to add, delete or move items in that list.
When I delete the bottom most item, there is no error. When I delete any other item in the list I get this NSRangeException error that crashes the app:
/*
2022-04-24 15:41:21.874306-0400 Trellis
beta[9560:3067012] *** Terminating app due to
uncaught exception 'NSRangeException', reason:
'*** __boundsFail: index 3 beyond bounds [0 ..
2]'
*** First throw call stack:
(0x1809150fc 0x19914fd64 0x180a1e564 0x180a2588c
0x1808c0444 0x1852dcce4 0x1852e1400 0x185424670
0x185423df0 0x185428a40 0x18843e4a0 0x188510458
0x188fd83ec 0x10102f3bc 0x1010500a4 0x188494f4c
0x10102c664 0x10103e0d4 0x18841a944 0x10102be18
0x10103122c 0x18837b8ac 0x188363484 0x18834bb64
0x188371d20 0x1883b88e4 0x1b28fe910 0x1b28fe318
0x1b28fd160 0x18831e780 0x18832f3cc 0x1883f5e34
0x18834206c 0x188345f00 0x182eb0798 0x184613138
0x184605958 0x184619f80 0x184622874 0x1846050b0
0x183266cc0 0x1835015fc 0x183b7d5b0 0x183b7cba0
0x1809370d0 0x180947d90 0x180882098 0x1808878a4
0x18089b468 0x19c42638c 0x18323d088 0x182fbb958
0x1885547a4 0x188483928 0x1884650c0 0x10109a630
0x10109a700 0x1015b9aa4)
libc++abi: terminating with uncaught exception
of type NSException
dyld4 config:
DYLD_LIBRARY_PATH=/usr/lib/system/introspection
DYLD_INSERT_LIBRARIES=/Developer/usr/lib/libBacktrac
eRecording.dylib:/Developer/usr/lib/libMainThreadChecker.dylib:/Developer/Library/Private
Frameworks/DTDDISupport.framework/libViewDebuggerSupport.dylib
*** Terminating app due to uncaught exception 'NSRangeException', reason: '***
__boundsFail: index 3 beyond bounds [0 .. 2]'
terminating with uncaught exception of type NSException
(lldb)
*/
struct WorkoutScreen: View {
#EnvironmentObject var workoutList: CoreDataViewModel //calls it from environment
#StateObject var vm = CoreDataViewModel() //access on the page
#Environment(\.scenePhase) var scenePhase
var body: some View{
//displays the current item in the list
}
}
When I add an item to the list I get the error:
'''
CoreData: warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'FruitEntity' so +entity is unable to disambiguate."
'''
Moving Items without adding or deleting any prior gives me this error upon closing the pop over:
'''
Error saving: Error Domain=NSCocoaErrorDomain Code=133020 "Could not merge changes." UserInfo={conflictList=(
"NSMergeConflict (0x2804f1480) for NSManagedObject (0x28327d900) with objectID '0x9ede5774e26501a4...
'''
Here is the core data and related functions:
class CoreDataViewModel: NSObject, ObservableObject {
private let container: NSPersistentContainer
private let context: NSManagedObjectContext
// Whenever you put your Core Data fetch in a view model, you should use an NSFetchedResultsController.
// This allows you to automatically update your #Published var when your Core Data store changes.
// You must inherit from NSObject to use it.
private let fetchResultsController: NSFetchedResultsController<FruitEntity>
#Published var savedEntities: [FruitEntity] = []
override init() {
container = NSPersistentContainer(name: "FruitsContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
print("ERROR LOADING CORE DATA: \(error)")
}
else {
print("Successfully loaded core data")
}
}
context = container.viewContext
let request = NSFetchRequest<FruitEntity>(entityName: "FruitEntity")
let sort = NSSortDescriptor(keyPath: \FruitEntity.order, ascending: true)
request.sortDescriptors = [sort]
// This initializes the fetchResultsController
fetchResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
// Because you inherit from NSObject, you must call super.init() to properly init the parent class. The order of when
// this is to be called has changed.
super.init()
// Because this is a delegate action, you must set the delegate. Since the view model will respond, we set the delegate to self.
fetchResultsController.delegate = self
fetchFruits()
}
func fetchFruits() {
do {
// Instead of calling container.viewContext.fetch(request) which is static, use fetchResultsController.performFetch()
try fetchResultsController.performFetch()
// Make sure the fetch result is not nil
guard let fruitRequest = fetchResultsController.fetchedObjects else { return }
savedEntities = fruitRequest
// You do not need to let error. error is automatically captured in a do catch.
} catch {
print("Error fetching \(error)")
}
}
func addFruit(text: String, nummSets: Int16, nummWeights: Int16, nummReps: Int16, secOrRepz: Bool, orderNumz: Int64, multilimbz: Bool, countDownz: Int16, repTimez: Int16, restTimez: Int16, circuitz: Bool) {
let newFruit = FruitEntity(context: container.viewContext)
newFruit.name = text
newFruit.numOFSets = nummSets
newFruit.numOFWeight = nummWeights
newFruit.numOFReps = nummReps
newFruit.measure = secOrRepz
newFruit.order = orderNumz
newFruit.multiLimb = multilimbz
newFruit.countDownSec = countDownz
newFruit.timePerRep = repTimez
newFruit.restTime = restTimez
newFruit.circuit = circuitz
saveData()
}
func deleteFunction(indexSet: IndexSet) {
guard let index = indexSet.first else { return }
let entity = savedEntities[index]
container.viewContext.delete(entity)
saveData()
}
func saveData() {
do {
try context.save()
fetchFruits()
} catch let error {
print("Error saving: \(error)")
}
}
}
// This is your delegate extension that handles the updating when your Core Data Store changes.
extension CoreDataViewModel: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller:
NSFetchedResultsController<NSFetchRequestResult>) {
// Essentially, you are redoing just the fetch as the NSFetchedResultsController knows how to fetch from above
guard let fruits = controller.fetchedObjects as? [FruitEntity] else { return }
self.savedEntities = fruits
}
}
Here is the list struct:
struct WorkoutListPopUp: View {
#ObservedObject var vm = CoreDataViewModel()
#EnvironmentObject var listViewModel: ListViewModel
#EnvironmentObject var workoutList: CoreDataViewModel
//Too many #State var to list here
var body: some View {
Button (action: {
//this triggers the bug>
vm.addFruit(text: "Workout name", nummSets: Int16(addSets) ?? 3, nummWeights: Int16(addWeights) ?? 0, nummReps: Int16(addReps) ?? 8, secOrRepz: addSecOrReps, orderNumz: Int64((vm.savedEntities.count)), multilimbz: dualLimbs, countDownz: 10, repTimez: 3, restTimez: 60, circuitz: false)
loadNums()
}, label: {
Image(systemName: "plus")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:20, height: 20)
.foregroundColor(Color.pink.opacity(1.0))
.padding(.top, 0)
})
List(){
ForEach(vm.savedEntities) {entity in
VStack{
EditWorkouts(entity: entity, prescribeMeasure: $prescribeMeasure, addReps: $addReps, measurePrescribed: $measurePrescribed, repTimePicker: $repTimePicker, repz: $repz, restPicker: $restPicker, setz: $setz, ready2Press: $ready2Press, workoutz: $workoutz, weightz: $weightz, setsRemaining: $setsRemaining, workoutNum: $workoutNum, workoutInstructions: $workoutInstructions, multiplelimbs: $multiplelimbs, showAllInfo: $showAllInfo)
//are these onChanges needed if "EditWorkouts" file is saving?
.onChange(of: entity.name) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.numOFSets) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.numOFReps) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.numOFWeight) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.measure) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.order) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.circuit) { text in
vm.saveData()
loadNums()
}
}
}
.onDelete(perform: vm.deleteFunction)
.onMove(perform: moveItem)
}
}
func loadNums(){
if vm.savedEntities.count > 0 {
workoutz = vm.savedEntities[workoutNum].name ?? "NO Name"
setz = String(vm.savedEntities[workoutNum].numOFSets)
weightz = String(vm.savedEntities[workoutNum].numOFWeight)
repz = String(vm.savedEntities[workoutNum].numOFReps)
multiplelimbs = vm.savedEntities[workoutNum].multiLimb
prescribeMeasure = vm.savedEntities[workoutNum].measure
if setsRemaining == 0 && ((workoutNum + 1) - (Int(vm.savedEntities.count)) == 0) {
workoutInstructions = "Goal: \(repz) \(measurePrescribed)"
}
else {
workoutInstructions = "Goal: \(repz) \(measurePrescribed)"
}
}
else {
workoutz = "Add a Workout 👉"
workoutInstructions = " "
}
}
func moveItem(indexSet: IndexSet, destination: Int) {
let source = indexSet.first!
if destination > source {
var startIndex = source + 1
let endIndex = destination - 1
var startOrder = vm.savedEntities[source].order
while startIndex <= endIndex {
vm.savedEntities[startIndex].order = startOrder
startOrder = startOrder + 1
startIndex = startIndex + 1
}
vm.savedEntities[source].order = startOrder
}
else if destination < source {
var startIndex = destination
let endIndex = source - 1
var startOrder = vm.savedEntities[destination].order + 1
let newOrder = vm.savedEntities[destination].order
while startIndex <= endIndex {
vm.savedEntities[startIndex].order = startOrder
startOrder = startOrder + 1
startIndex = startIndex + 1
}
vm.savedEntities[source].order = newOrder
}
vm.savedEntities[source].circuit = false
vm.saveData()
loadNums()
}
}
This is the EditWorkouts file that the WorkoutPopUp file connects to:
struct EditWorkouts: View {
#EnvironmentObject var workoutList: CoreDataViewModel
#StateObject var vm = CoreDataViewModel()
#EnvironmentObject var listViewModel: ListViewModel
let entity: FruitEntity
//too many #State vars to post
var body: some View {
VStack{
HStack{
//many lines of code for options that alter the respective workout on the list. All are followed by their version of:
//.onChange(of:
//vm.savedEntities[Int(entity.order)].multiLimb) { _ in
//vm.saveData()
//loadNums()"
//}
//-or-
//.onChange(of:vm.savedEntities[Int(entity.order)].circuit) { _ in
//entity.circuit = entity.circuit
//vm.saveData()
//}
}
}
}
}
Picture of CoreData FruitEntity:
Image
Thank you again for your time!!
There are a couple of issues with your code. I suspect one is the sole contributor to the crash, but the other may be contributing as well. First, the most likely culprit. If you use .onDelete(), you can't use id: \.self. The reason is pretty simple: the ForEach can get pretty confused as to which entity is which. .self is often not unique, and it really needs to be if you are deleting and rearranging things in the ForEach(), i.e. .onDelete() and .onMove().
The solution is simple. Whatever you are using in the ForEach should conform to Identifiable. Core Data managed objects all conform to Identifiable, so the fix is easy; remove the `id: .self``:
struct ListView: View {
#StateObject var vm = CoreDataViewModel()
var body: some View {
List {
ForEach(vm.savedEntities) {entity in
Text(entity.name ?? "")
}
.onDelete(perform: vm.deleteFunction)
}
// This just adds a button to create entities.
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
vm.addFruit()
} label: {
Image(systemName: "plus")
}
}
}
}
}
That fix alone will most likely stop the crash. However, I also noticed that you were having issues with your updates in your view. That is because you did not implement an NSFetchedResultsController and NSFetchedResultsControllerDelegate which updates your array when your Core Data store changes. Your view model should look like this:
import SwiftUI
import CoreData
class CoreDataViewModel: NSObject, ObservableObject {
private let container: NSPersistentContainer
private let context: NSManagedObjectContext
// Whenever you put your Core Data fetch in a view model, you should use an NSFetchedResultsController.
// This allows you to automatically update your #Published var when your Core Data store changes.
// You must inherit from NSObject to use it.
private let fetchResultsController: NSFetchedResultsController<FruitEntity>
#Published var savedEntities: [FruitEntity] = []
override init() {
container = NSPersistentContainer(name: "FruitsContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
print("ERROR LOADING CORE DATA: \(error)")
}
else {
print("Successfully loaded core data")
}
}
context = container.viewContext
let request = NSFetchRequest<FruitEntity>(entityName: "FruitEntity")
let sort = NSSortDescriptor(keyPath: \FruitEntity.order, ascending: true)
request.sortDescriptors = [sort]
// This initializes the fetchResultsController
fetchResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
// Because you inherit from NSObject, you must call super.init() to properly init the parent class. The order of when
// this is to be called has changed.
super.init()
// Because this is a delegate action, you must set the delegate. Since the view model will respond, we set the delegate to self.
fetchResultsController.delegate = self
// Renamed function to conform to naming conventions. You should use an active verb like fetch to start the name.
fetchFruits()
}
func fetchFruits() {
do {
// Instead of calling container.viewContext.fetch(request) which is static, use fetchResultsController.performFetch()
try fetchResultsController.performFetch()
// Make sure the fetch result is not nil
guard let fruitRequest = fetchResultsController.fetchedObjects else { return }
savedEntities = fruitRequest
// You do not need to let error. error is automatically captured in a do catch.
} catch {
print("Error fetching \(error)")
}
}
// This is just to be able to add some data to test.
func addFruit() {
var dateFormatter: DateFormatter {
let df = DateFormatter()
df.dateStyle = .short
return df
}
let fruit = FruitEntity(context: context)
fruit.name = dateFormatter.string(from: Date())
fruit.measure = false
fruit.numOfReps = 0
fruit.numOfSets = 0
fruit.numOfWeight = 0
fruit.order = 0
saveData()
}
func deleteFunction(indexSet: IndexSet) {
guard let index = indexSet.first else { return }
let entity = savedEntities[index]
container.viewContext.delete(entity)
saveData()
}
func saveData() {
do {
try context.save()
} catch let error {
print("Error saving: \(error)")
}
}
}
// This is your delegate extension that handles the updating when your Core Data Store changes.
extension CoreDataViewModel: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
// Essentially, you are redoing just the fetch as the NSFetchedResultsController knows how to fetch from above
guard let fruits = controller.fetchedObjects as? [FruitEntity] else { return }
self.savedEntities = fruits
}
}
You will notice refreshID no longer exists in the view. It updates without it. Also, please note that by incorporating the data store init into your view model, you can't expand it to have other entities with other views. Each will have a different context and they will crash the app. You are better off having a controller class that creates a singleton for the Core Data store, such as what Apple gives you in the default set up.
In the end, I think you issue was a combination of using id: .self which is known to crash with .onDelete() AND the fact that you were using refreshID not NSFetchedResultsController to update the List.

Unable to use a defined state variable in the init()

I am trying to implement a search bar in my app, as now I want to use the keyword typed in the search bar to make an API call to fetch backend data, here is my code:
struct SearchView: View {
#State private var searchText : String=""
#ObservedObject var results:getSearchList
init(){
results = SearchList(idStr: self.searchText)
}
var body: some View {
NavigationView {
VStack {
SearchBar(text: $searchText)
}.navigationBarTitle(Text("Search"))
}
}
}
I implement SearchBar view followed the this tutorial https://www.appcoda.com/swiftui-search-bar/ exactly,
and getSearchList is a class which has an var called idStr,
struct searchResEntry: Codable, Identifiable{
var id:Int
var comment:String
}
class SearchList: ObservableObject {
// 1.
#Published var todos = [searchResEntry]()
var idStr: String
init(idStr: String) {
self.idStr = idStr
let url = URL(string: "https://..." + idStr)!
// 2.
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let todoData = data {
// 3.
let decodedData = try JSONDecoder().decode([searchResEntry].self, from: todoData)
DispatchQueue.main.async {
self.todos = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
the problem I am struggling now is that I want to use the variable searchText to initialize the getSearchList , getSearchList has an var called idStr, this idStr is to used to store the typed keyword, my code always get an error: 'self' used before all stored properties are initialized , I have no idea how to deal with this.
Here is your code, edited by me:
struct SearchView: View {
#StateObject var results = SearchList()
var body: some View {
NavigationView {
VStack {
SearchBar(text: $results.searchText)
}.navigationBarTitle(Text("Search"))
}
}
}
struct SearchResEntry: Codable, Identifiable {
var id:Int
var backdrop_path:String
}
class SearchList: ObservableObject {
#Published var todos = [SearchResEntry]()
#Published var searchText: String = ""
var cancellable: AnyCancellable?
init() {
cancellable = $searchText.debounce(
for: .seconds(0.2),
scheduler: RunLoop.main
).sink { _ in
self.performSearch()
}
}
func performSearch() {
if let pathParam = searchText.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed),
let url = URL(string: "https://hw9node-310902.uc.r.appspot.com/mutisearch/\(pathParam)") {
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let todoData = data {
let decodedData = try JSONDecoder().decode([SearchResEntry].self, from: todoData)
DispatchQueue.main.async {
self.todos = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
} else {
print("Invalid URL")
}
}
}
Explanation
You are free to reverse the optional changes i made, but here are my explanations:
Use capital letter at the beginning of a Type's name. e.g write struct SearchResEntry, don't write struct searchResEntry. This is convention. Nothing big will happen if you don't follow conventions, but if anyone other than you (or maybe even you in 6 months) look at that code, chances are they go dizzy.
Dont start a Type's name with verbs like get! Again, this is just a convention. If anyone sees a getSomething() or even GetSomething() they'll think thats a function, not a Type.
Let the searchText be a published property in your model that performs the search. Don't perform search on init, instead use a function so you can initilize once and perform search any time you want (do results.performSearch() in your View). Also you can still turn your searchText into a binding to pass to your search bar (look at how i did it).
EDIT answer to your comment
I could right-away think of 3 different answers to your comment. This is the best of them, but also the most complicated one. Hopefully i chose the right option:
As you can see in the class SearchList i've added 2 things. First one is a cancellable to store an AnyCancellable, and second is the thing in init() { ... }. In init, we are doing something which results in an AnyCancellable and then we are storing that in the variable that i added.
What am i doing In init?
first $searchText gives us a Publisher. Basically, the publisher is called whenever the searchText value changes. Then you see .debounce(for: .seconds(0.2), on: RunLoop.main) which means only let the latest input go through and reach the next thing (the next thing is .sink { } as you can see), only if the user has stopped writing for 0.2 seconds. This is very helpful to avoid a load of requests to the server which can eventually make servers give you a 429 Too Many Requests error if many people are using your app (You can remove the whole .debounce thing if you don't like it). And the last thing is .sink { } which when any value reaches that point, it'll call the performSearch func for you and new results will be acquired from the server.
Alternative way
(again talking about your comment)
This is the simpler way. Do as follows:
remove init() { ... } completely if you've added it
remove var cancellable completely if you've added it
in your SearchView, do:
.onChange(of: results.searchText) { _ in
results.performSearch()
}
pretty self-explanatory; it'll perform the search anytime the searchText value is changed.

SwiftUI - Show the data fetched from Firebase in view?

Firebase I am trying to show data taken from Firestore in my SwiftUI view but I have a problem. I have no problem with pulling data from Firebase. But I cannot show the data as I want. I work with MVVM architecture.
My model is like this:
struct UserProfileModel: Identifiable {
#DocumentID var id : String?
var username : String
var uidFromFirebase : String
var firstName : String
var lastName : String
var email : String
}
ViewModel:
class UserProfileViewModel: ObservableObject {
#Published var user : [UserProfileModel] = []
private var db = Firestore.firestore()
func data(){
db.collection("Users").whereField("uuidFromFirebase", isEqualTo: Auth.auth().currentUser!.uid).addSnapshotListener { (snapshot, error) in
guard let documents = snapshot?.documents else {
print("No Documents")
return
}
self.user = documents.compactMap { queryDocumentSnapshot -> UserProfileModel? in
return try? queryDocumentSnapshot.data(as: UserProfileModel.self)
}
}
}
}
View:
struct MainView: View {
#ObservedObject private var viewModel = UserProfileViewModel()
var body: some View { // -> Error: The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
VStack{
Text(viewModel.user.username) // -> I want to do this but XCode is giving an error.
// This works but I don't want to do it this way.
List(viewModel.user) { user in
VStack{
Text(user.username)
Text(user.firstName)
Text(user.lastName)
Text(user.email)
Text(user.uidFromFirebase)
}
}
}
}
}
In the videos and articles titled "SwiftUI fetch data from Firebase" that I watched and read, I have always narrated on List and ForEach. But I want to use the data wherever. I shared all my code with you. I want to learn how I can do this.
Looks to me like you really just want to have one user that you're pulling the data for, but you've set up your UserProfileViewModel with an array of users ([UserProfileModel]). There are a number of ways that you could take care of this. Here's one (check the code for inline comments about what is going on):
class UserProfileViewModel: ObservableObject {
#Published var user : UserProfileModel? = nil // now an optional
private var db = Firestore.firestore()
func data(){
db.collection("Users").whereField("uuidFromFirebase", isEqualTo: Auth.auth().currentUser!.uid).addSnapshotListener { (snapshot, error) in
guard let documents = snapshot?.documents else {
print("No Documents")
return
}
self.user = documents.compactMap { queryDocumentSnapshot -> UserProfileModel? in
return try? queryDocumentSnapshot.data(as: UserProfileModel.self)
}.first // Take the first document (since there probably should be only one anyway)
}
}
}
struct MainView: View {
#ObservedObject private var viewModel = UserProfileViewModel()
var body: some View {
VStack {
if let user = viewModel.user { //only display data if the user isn't nil
Text(user.username)
Text(user.firstName)
Text(user.lastName)
Text(user.email)
Text(user.uidFromFirebase)
}
}
}
}
I'd say a more traditional way of handling this might be to store your user profile document Users/uid/ -- that way you can just user document(uid) to find it rather than the whereField query.

SwiftUI Async data fetch in onAppear

I have class getDataFromDatabase which has func readData() thats read data from Firebase.
class getDataFromDatabase : ObservableObject {
var arrayWithQuantity = [Int]()
var arrayWithTime = [Double]()
func readData(completion: #escaping(_ getArray: Array<Int>?,_ getArray: Array<Double>?) -> Void) {
let db = Firestore.firestore()
db.collection("amounts").getDocuments { (querySnapshot, err) in
if let e = err{
print("There's any errors: \(e)")
}
if err != nil{
print((err?.localizedDescription)!)
return
}
for i in querySnapshot!.documents{
let quantityFromDb = i.get("amount") as! Int
let timeFromDb = i.get("averageTimeRecognition") as! Double
self.arrayWithQuantity.append(quantityFromDb)
self.arrayWithTime.append(timeFromDb)
}
completion(self.arrayWithQuantity, self.arrayWithTime)
}
}
}
I use func readData() in onAppear:
struct CheckDatabaseView: View {
#State private var quantityFromDatabase: Array<Int> = []
#State private var timeFromDatabase: Array<Double> = []
#State private var flowersName: Array<String> = ["Carnation", "Daisy", "Hyacinth", "Iris", "Magnolia", "Orchid", "Poppy", "Rose", "Sunflower", "Tulip"]
#State private var isReady: Bool = false
var body: some View {
ScrollView(.vertical, showsIndicators: false){
ZStack(alignment: .top){
VStack(spacing: 40){
Text("Hello, world!")
// BarView(value: CGFloat(timeFromDatabase[0]), name: flowersName[0])
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top)
}
.navigationBarTitle(Text("Your datas in database").foregroundColor(.blue), displayMode: .inline)
.onAppear{
let gd = getDataFromDatabase()
gd.readData { (quantity, time) in
self.quantityFromDatabase = quantity!
self.timeFromDatabase = time!
}
}
}
}
I cannot use values self.quantityFromDatabase and self.timeFromDatabase because are empty. I know the problem is with the asynchronous retrieval of data. I've tried with DispatchQueue.main.async, but I still not get these values. How is the other method to get it? I need this values, because I want to draw charts in VStack (the comment line there).
EDIT
As #Rexhin Hoxha wrote below, i modified the code but i am not sure if the way is correct. I changed var arrayWithQuantity = [Int]() and var arrayWithTime = [Double]() by adding #Published in class getDataFromDatabase (now it's GetDataFromDatabaseViewModel):
class GetDataFromDatabaseViewModel : ObservableObject {
#Published var arrayWithQuantity = [Int]()
#Published var arrayWithTime = [Double]()
func readData() {
let db = Firestore.firestore()
db.collection("amounts").getDocuments { (querySnapshot, err) in
if let e = err{
print("There's any errors: \(e)")
}
if err != nil{
print((err?.localizedDescription)!)
return
}
for i in querySnapshot!.documents{
let quantityFromDb = i.get("amount") as! Int
let timeFromDb = i.get("averageTimeRecognition") as! Double
self.arrayWithQuantity.append(quantityFromDb)
self.arrayWithTime.append(timeFromDb)
}
print("Array with quantity: \(self.arrayWithQuantity.count)")
}
}
}
also in struct I initialized #ObservedObject var gd = GetDataFromDatabaseViewModel() and onAppear now looks like this:
.onAppear{
self.gd.readData()
print("Quantity after reading: \(self.gd.arrayWithQuantity.count)")
}
but print in onAppear still print an empty Array. Where did I do a mistake?
So the problem is in your completion handler. It returns before you retrieve the data.
Solution is to make your arrays #Published and read the data in real time from the view. You have to remove the completion handler.
Call the function on ‚onAppear()‘ and use #ObservedObject to bind to your ViewModel (getDataFromDatabase). This is how it’s done in SwiftUI.
Please capitalize the first letter and use something more generic like „YouViewName“ViewModel.
Your name is fine for a method/function but not for a Class

Display query values within view's body

What I want to do: I want to display the user's name on the Profile view after a user logs into the app. Currently I have implemented a login using Firebase Auth. When users create an account, it creates a record in the "Users" collection in Firestore that records First and Last Name and email address.
What I've Tried: On the Profile view, I currently have a function "getUser" that checks if the user is logged in, and then matches the user's Firebase Auth email address and matches it to the record in Firestore. This is working because I've checked what info the query is returning by what's logged in the console. However, I'm at a lost how to get the "fName" information and displaying it within the view's body.
Here's screenshots of the database structure and my current code.
import SwiftUI
import Firebase
import Combine
import FirebaseFirestore
struct ProfileView: View {
#EnvironmentObject var session: SessionStore
let db = Firestore.firestore()
func getUser() {
session.listen()
let query = db.collection("users").whereField("email", isEqualTo: session.session!.email!)
query.getDocuments { (QuerySnapshot, err) in
if let docs = QuerySnapshot?.documents {
for docSnapshot in docs {
print (docSnapshot.data())
}
}
}
}
var body: some View {
Group {
if (session.session != nil) {
NavigationView {
VStack {
Text("Welcome back \(session.session!.email ?? "user")")
Spacer()
Button(action: session.signOut) {
Text("Sign Out")
}.padding(.bottom, 60)
}
} // end NavigationView
} else {
AuthView()
}
}.onAppear(perform: getUser)
}
}
I guess you should save the values returned from Firestore with UserDefaults
import UIKit
//your get user data logic here
let fName:String = <the first name retrieved from firestore>
// Get the standard UserDefaults as "defaults"
let defaults = UserDefaults.standard
// Save the String to the standard UserDefaults under the key, "first_name"
defaults.set(fName, forKey: "first_name")
And in your profile page you should retrieve this value
// you should have defaults initialized here
fnameToDisplay = defaults.string(forKey: "first_name")
Hello I prefer to make a ViewModel - NetworkManager class that comfort ObservableObject and inside of it you can get and fetch data and when they finish loading they will update the view, Let me show you an example how it works:
as you can see I created a NetworkManager extends ObservableObject (cause I want to get notified when the user finish loading) inside of it you can see that var user is annotated with #Published that make the user observed and when a new data set will notify the View to update. also you can see that I load the data on init so the fist time this class initialized it will call the fetchData() func
class NetworkManager: ObservableObject {
let url: String = "https://jsonplaceholder.typicode.com/todos/1"
var objectWillChange = PassthroughSubject<NetworkManager, Never>()
init() {
fetchData()
}
#Published var user: User? {
didSet {
objectWillChange.send(self)
print("set user")
}
}
func fetchData() {
guard let url = URL(string: url) else {return}
print("fetch data")
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard error == nil else {return}
print("no error")
guard let data = data else {return}
print("data is valid")
let user = try! JSONDecoder().decode(User.self, from: data)
DispatchQueue.main.async {
self.user = user
}
}.resume()
}
}
ContentView is where I'm gonna place the user data or info I declared #ObservedObject var networkManager cause it extend ObservableObject and I place the views and pass User
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
VStack {
DetailsView(user: networkManager.user)
}
}
}
struct DetailsView: View {
var user: User?
var body: some View {
VStack {
Text("id: \(user?.id ?? 0)")
Text("UserID: \(user?.userId ?? 0 )")
Text("title: \(user?.title ?? "Empty")")
}
}
}
class User: Decodable, ObservableObject {
var userId: Int = 0
var id: Int = 0
var title: String = ""
var completed: Bool = false
}
PS: I didn't make objects unwrapped correctly please consider that so
you not to have any nil exception