Optimize search in SwiftUI to load without lagging - arrays

What will be the best way to optimize this swift code to filter without lagging. I have tried both appending and binding but still, it lags.
import Contacts
import SwiftUI
import Combine
class ContactStore: ObservableObject {
#Published var contacts: [ContactsModel] = []
var searchText: String = ""{
didSet {
self.fetchContacts()
}
}
init() {
self.fetchContacts()
}
private func fetchContacts() {
let store = CNContactStore()
store.requestAccess(for: .contacts) { (granted, error) in
if let error = error {
print("failed to request access", error)
return
}
if granted {
// print("access granted")
let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey, CNContactBirthdayKey]
let keysToFetch = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey] as [CNKeyDescriptor]
let request = CNContactFetchRequest(keysToFetch: keys as [CNKeyDescriptor])
do {
if self.searchText.isEmpty {
try store.enumerateContacts(with: request, usingBlock: { (contact, stopPointer) in
self.contacts.append(ContactsModel(firstName: contact.givenName, lastName: contact.familyName, birthDay: contact.birthday?.date ?? Date(timeInterval: -3600, since: Date()) , telephone: contact.phoneNumbers.first?.value.stringValue ?? ""))
})
} else {
self.contacts = []
let predicate = CNContact.predicateForContacts(matchingName: self.searchText)
let contacts = try store.unifiedContacts(matching: predicate, keysToFetch: keysToFetch)
print(contacts)
for i in contacts{
self.contacts = [ContactsModel(firstName: i.givenName, lastName: i.familyName, telephone: i.phoneNumbers.first?.value.stringValue ?? "")]
// self.contacts.append(ContactsModel(firstName: i.givenName, lastName: i.familyName, telephone: i.phoneNumbers.first?.value.stringValue ?? ""))
}
}
} catch let error {
print("Failed to enumerate contact", error)
}
} else {
print("access denied")
}
}
}
}

Try with searchText as a #Published var. Then use Combine to subscribe to, adding a debounce. In this way, you can limit the frequency of execution of the fetch request.
It would be something like this
$searchText
.debounce(for: 5, scheduler: RunLoop.main) //5 seconds debounce
.sink(
receiveValue: {
self.fetchContacts()
}
)
.store(in: &disposables)

Related

Push the data from the request into the array

I want to write the data that I received from the request into an array and then display it through a list
Here is my structure for the json file
struct DataRespons: Codable {
let data: [String]
let status: String
}
struct UserRespons: Codable {
let status: String
let data: UserData
}
struct UserData: Codable, Identifiable {
let id: String
let firstName: String
let lastName: String
let age: Int
let gender: String
let country: String
}
This is my class for JSON requests and decoding
import Foundation
#MainActor
class NetworkModel: ObservableObject {
#Published var listId: [String] = []
var statusList = ""
var statusUser = ""
var temp = ""
var user: [UserData] = [] // here I am not sure if this type Array
#Published var userData = UserRespons(status: "??", data: UserData(id: "???", firstName: "???", lastName: "??", age: 4, gender: "???", country: "???"))
this func for receive a letter with links to which I should make requests
func getList() {
guard let url = URL(string: "https://opn-interview-service.nn.r.appspot.com/list") else { fatalError("Missing URL") }
var request = URLRequest(url: url)
request.addValue("bearer \(token)", forHTTPHeaderField: "Authorization")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print("Requst error",error)
return
}
guard let response = response as? HTTPURLResponse else { return }
if response.statusCode == 200 {
guard let data = data else { return }
DispatchQueue.main.async { [self] in
do {
let decoded = try JSONDecoder().decode(DataRespons.self, from: data)
self.listId = decoded.data
self.statusList = decoded.status
for i in self.listId.indices {
print("This is id[\(i)] = \(listId[i])")
getUser(url: "\(listId[i])")
// #MARK: NEED HERE HELP user.append(.init(id: <#T##String#>, firstName: <#T##String#>, lastName: <#T##String#>, age: <#T##Int#>, gender: <#T##String#>, country: <#T##String#>))
}
} catch let error{
print("Error decode",error)
}
}
}
}
dataTask.resume()
}
I want to add data that will come from requests to an empty array so that it can then be displayed in the list
function for decoding data user
func getUser(url: String) {
guard let url = URL(string: "https://opn-interview-service.nn.r.appspot.com/get/\(url)") else { fatalError("Missing URL") }
var request = URLRequest(url: url)
request.addValue("bearer \(token)", forHTTPHeaderField: "Authorization")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print("Requst error",error)
return
}
guard let response = response as? HTTPURLResponse else { return }
if response.statusCode == 200 {
guard let data = data else { return }
DispatchQueue.main.async { [self] in
do {
let decoded = try JSONDecoder().decode(UserRespons.self, from: data)
self.userData = decoded
self.statusUser = decoded.status
print("UserData: name = \(userData.data.firstName) Lastname = \(userData.data.lastName) gender = \(userData.data.gender)")
} catch let error{
print("Error decode",error)
}
}
}
}
dataTask.resume()
}
I don't know how to throw all the data into the array
First of all iterating the indices is cumbersome, replace
for i in self.listId.indices {
print("This is id[\(i)] = \(listId[i])")
getUser(url: "\(listId[i])")
}
with
for id in self.listId {
print("This is \(id)")
getUser(url: id)
}
But this is not the right place to append the user data. Do it in getUser. But first declare the array as #Published (and in plural form)
#Published var users = [UserData]()
and delete the property userData because it's not needed.
#Published var userData = UserRespons(status: "??", data: UserData(id: "???", firstName: "???", lastName: "??", age: 4, gender: "???", country: "???"))
Now replace the decoding part in getUser with
let decoded = try JSONDecoder().decode(UserRespons.self, from: data)
self.statusUser = decoded.status
users.append(decoded.data)
print("UserData: name = \(decoded.data.firstName) Lastname = \(decoded.data.lastName) gender = \(decoded.data.gender)")
I recommend to move to async/await. It's much less code and it switches to the main thread automatically.

Get an [String] link from a json file

Here is my structure for the json file
struct DataRespons: Codable {
let data: [String]
let status: String
}
JSON file url
{
"status": "success",
"data": [
"f7c75c1f-10ab-4298-9dc9-e80b7bd07dfd",
"6f5f6eeb-191d-4ad9-b5ef-6f61fd5fcefc",
"8008800880088008",
"64a3f5d0-37c7-4c30-8d0f-3b67fb5c8fde"
]
}
This is my class for JSON requests and decoding
I use these functions to get an array of links
I hope I wrote correctly what I want to receive, and if there are any questions, please contact me
#MainActor
class NetworkModel: ObservableObject {
#Published var listId: [String] = []
var statusList = ""
var statusUser = ""
#Published var userData = UserRespons(status: "??", data: UserData(id: "???", firstName: "???", lastName: "??", age: 4, gender: "???", country: "???"))
func getList() {
guard let url = URL(string: "some URL") else { fatalError("Missing URL") }
var request = URLRequest(url: url)
request.addValue("bearer \(token)", forHTTPHeaderField: "Authorization")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print("Requst error",error)
return
}
guard let response = response as? HTTPURLResponse else { return }
if response.statusCode == 200 {
guard let data = data else { return }
DispatchQueue.main.async {
do {
let decoded = try JSONDecoder().decode(DataRespons.self, from: data)
self.listId = decoded.data
self.statusList = decoded.status
} catch let error{
print("Error decode",error)
}
}
}
}
dataTask.resume()
}
I can't through index [String] to get each element
use this code to access the data elements of an array:
// ....
self.listId = decoded.data
for i in listId.indices {
print("--> listId[\(i)] = \(listId[i]) ")
}
print("--> listId[0] = \(listId[0]) ")
print("--> listId[1] = \(listId[1]) ")
// ....
Read the very basics of Swift, specially regarding arrays here:
https://docs.swift.org/swift-book/LanguageGuide/CollectionTypes.html

Store Indetifiable struct array in Firestore

I have this code:
struct Restaurants: Identifiable {
let name: String
let imageUrl: URL
let id: String
let rating: Double
let url: String
let category: String
static var viewModels: [Restaurants] = [
Restaurants(name: "Silverio's Mexican Kitchen", imageUrl: URL(string: "https://mainsite-prod-cdn.azureedge.net/partner-images/432257/micrositeimage_p1.jpg")!, id: "hjkhjhjh", rating: 2, url: "https://google.com"),//, category: "Mexican"),
Restaurants(name: "Taqueria La Esquinita", imageUrl: URL(string: "https://s3-media0.fl.yelpcdn.com/bphoto/x-KCQ7osmvBWLA9WpPdO_Q/o.jpg")!, id: "hjdha", rating: 3, url: "https://google.com")//, category: "Mexican")
] {
didSet {
print("data set")
}
}
}
How do I store viewModels in Firestore? I want to maintain this object structure so that I can carry it across multiple devices.
The idea of this app is to help groups find a place to eat. Therefore, I have this structure:
I am now using sub-collections for each Restaurant thanks to #FaridShumbar. How do I get documents from a sub-collection?
You need the get() method in order to retrieve documents.
Following examples from the documentation, you could retrieve your documents as follows:
db.collection("parties").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())")
}
}
}
This example retrieves all documents in a collection.
I did it by saving each element in the struct separately and then putting it back together when I get the document.
Save Code:
db.collection("parties").document(Utilities.code).collection("Restaurants").document(card.name).setData([
"name" : card.name,
"img" : imgUrl,
"id" : card.id,
"rating" : card.rating,
"url" : card.url,
"yes" : FieldValue.arrayUnion([Utilities.name])
], merge: true) { error in
if error != nil {
print("error adding restaurant: \(error!)")
}
}
Retrieve code:
class restaurantsFirebase: ObservableObject {
#Published var restaurants: [RestaurantListViewModel] = []
private let db = Firestore.firestore()
private var devices = Array<String>()
func fetchData() {
db.collection("parties").document(Utilities.code).addSnapshotListener { doc, error in
if error == nil {
if doc != nil && doc!.exists {
if let getDevices = doc!.get("devices") as? Array<String> {
self.devices = getDevices
}
}
}
}
db.collection("parties").document(Utilities.code).collection("Restaurants").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("no documents")
return
}
self.restaurants = documents.compactMap({ (queryDocumentSnapshot) in
print(queryDocumentSnapshot.data())
let data = queryDocumentSnapshot.data()
let name = data["name"] as? String ?? ""
let img = data["img"] as? String ?? ""
let id = data["id"] as? String ?? ""
let rating = data["rating"] as? Double ?? 0
let url = data["url"] as? String ?? ""
let yes = data["yes"] as? Array<String> ?? []
let compiled = RestaurantListViewModel(name: name, imageUrl: URL(string: img)!, id: id, rating: rating, url: url)
print("restaurant = \(compiled), restaurants = \(self.restaurants)")
print("yes = \(yes), devices = \(self.devices)")
if yes == self.devices {
// self.restaurants.append(compiled)
return compiled
}else {
return nil
}
})
}
}
}
Use:
#ObservedObject private var viewModels = restaurantsFirebase()
db.collection("parties").document(Utilities.code).addSnapshotListener { doc, error in
if error == nil {
if doc != nil && doc!.exists {
if let dev = doc!.get("devices") as? Array<String> {
print("devices = \(dev)")
devices = dev
}
if let done = doc!.get("devicesDone") as? Array<String> {
print("devicesDone = \(done), devices = \(devices)")
if done == devices {
self.viewModels.fetchData()
showFinal = true
results = false
}
}
}
}

Firebase and swiftUI, listening for real time update strange behave weird

On my project I'm using firebase Cloud Firestore in order to implement a similar system like "Facebook" to request and confirm friends.
On my app some user are Administrator and some just normal user.
Only normal user can search and add Administrator in order to join some specific event scheduled from the administrator.
here below how is my data in Cloud Firestore :
every user have collection with pending and confirm friends.
I' m using a view to list all the pending and confirm friends for each user
using the .addSnapshotListener {} of firebase I'm checking with the following function every time there is a change on the confirm and pending friend and I publish the change in 2 #Published arrays, pendingFriendsADMIN = [UserMOdel] and confirmedFriendADMIN = [UserMOdel]
func userUpdateFriendUser(userInfo: UserModel){
db.collection("userUser").document(userInfo.email).collection("pendingFriends")
.addSnapshotListener(includeMetadataChanges: false) { documentSnapshot, error in
self.pendingFriendsUSER = []
guard let documents = documentSnapshot?.documents else {
print("Error fetching documents: \(String(describing: error?.localizedDescription))")
return
}
var i = 0
for doc in documents {
debugPrint("inizio il ciclo pending user\(i)")
let idUser = doc["userID"] as? String ?? "no ID"
self.downloadImageForAdmin(userID: idUser) { (urlImage) in
let userPending = UserModel(name: "", surname: "" ,username: "", email: "", userID: "", adminLevel: "", immagine: urlImage!, position: "centro", position2: "sx", vote: 0)
userPending.name = doc["name"] as? String ?? "NA name"
userPending.surname = doc["surname"] as? String ?? "NA surname"
userPending.adminLevel = doc["adminLevel"] as? String ?? "NA admin"
userPending.email = doc["email"] as? String ?? "NA email"
userPending.username = doc["username"] as? String ?? "NA username"
userPending.userID = doc["userID"] as? String ?? "NA id"
userPending.position = doc["position"] as? String ?? "na position"
userPending.position2 = doc["position2"] as? String ?? "na position"
userPending.vote = doc["vote"] as? Int ?? 0
self.pendingFriendsUSER.append(userPending)
i = i+1
debugPrint("finito ciclo pending")
}
}
}
db.collection("userUser").document(userInfo.email).collection("confirmedFriend")
.addSnapshotListener (includeMetadataChanges: false){ documentSnapshot, error in
self.confirmedFriendUSER = []
guard let documents = documentSnapshot?.documents else {
print("Error fetching documents: \(String(describing: error?.localizedDescription))")
return
}
for doc in documents {
debugPrint("inizio il ciclo confirm user \(i)")
let idUser = doc["userID"] as? String ?? "no ID"
self.downloadImageForAdmin(userID: idUser) { (urlImage) in
let userConfirm = UserModel(name: "", surname: "" ,username: "", email: "", userID: "", adminLevel: "", immagine: urlImage!, position: "centro", position2: "sx", vote: 0)
userConfirm.name = doc["name"] as? String ?? "NA name"
userConfirm.surname = doc["surname"] as? String ?? "NA surname"
userConfirm.adminLevel = doc["adminLevel"] as? String ?? "NA admin"
userConfirm.email = doc["email"] as? String ?? "NA email"
userConfirm.username = doc["username"] as? String ?? "NA username"
userConfirm.userID = doc["userID"] as? String ?? "NA id"
userConfirm.position = doc["position"] as? String ?? "na position"
userConfirm.position2 = doc["position2"] as? String ?? "na position"
userConfirm.vote = doc["vote"] as? Int ?? 0
self.confirmedFriendUSER.append(userConfirm)
}
}
}
}
the similar method is used also for listed the change on the userFriendList.
a user, can search an administrator via the email, and send to him a friend request(see below)
the user sent the friend request with the following function:
simply, I write on the pending friend the of the user the admin email and in the admin pending friend the user email
func sendFriendRequest(userInfo: UserModel, userToRequest: UserModel, closure: #escaping warning){
// check if reuqest already sent
self.db.collection("userAdmin").document(userToRequest.email).collection("confirmedFriend").whereField("email", isEqualTo: userInfo.email).getDocuments() { (queryResult, err) in
if let err = err {
debugPrint("unable to get data , friend alrady request\(err)")
} else {
if queryResult!.documents.count > 0 {
debugPrint("siete gia amici") // mettere warning
let warning = true
closure(warning)
return
} else {
// if request never sent, metto user nella lista dell admin pending
self.db.collection("userAdmin").document(userToRequest.email).collection("pendingFriends").document(userInfo.email).setData([
"username": userInfo.username,
"email" : userInfo.email,
"userID" : userInfo.userID,
"adminLevel": userInfo.adminLevel,
"name":userInfo.name,
"surname":userInfo.surname,
"position": userInfo.position,
"position2": userInfo.position2,
"vote": userInfo.vote
], merge: false) { (err) in
self.db.collection("userUser").document(userInfo.email).collection("pendingFriends").document(userToRequest.email).setData([
"username": userToRequest.username,
"email" : userToRequest.email,
"userID" : userToRequest.userID,
"adminLevel": userToRequest.adminLevel,
"name":userToRequest.name,
"surname":userToRequest.surname,
"position": userToRequest.position,
"position2": userToRequest.position2,
"vote": userToRequest.vote
], merge: false)
}
// metto sulla mia pending request
}
}
}
}
Here the problem...
some time , not always once I sent the request to a friend admin the .addSnapshotListener duplicate the change , as you can se from the third picture there is 2 time the same pending friend.
If I exit from the view and I go back the pending friend are correct.
here the code of my AdminFriendRequest : View
import SwiftUI
import URLImage
struct AdminFriendRequest: View {
#Binding var dismissView : Bool
#ObservedObject var dm : DataManager
#Binding var meInfo: UserModel?
var body: some View {
VStack{
fakebar
Spacer()
List{
HStack {
Image(systemName: "person.2")
Text("Pending friends request:")
}.font(.headline)
.foregroundColor(.blue)
ForEach(dm.pendingFriendsADMIN) { friend in
HStack{
if friend.immagine == nil{
Image(systemName: "person")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
.clipShape(Circle())
} else {
URLImage(friend.immagine!) { proxy in
proxy.image
.resizable()
.frame(width: 30, height: 30, alignment: .center)
.clipShape(Circle())
}
}
Text(friend.username)
Spacer()
Image(systemName: "checkmark.circle")
}
.onTapGesture {
if self.meInfo != nil {
self.dm.tapToConfirmFriend(me: self.meInfo!, friendToConfirm: friend) { (isFriendConfirm) in
debugPrint("is friend confirm \(isFriendConfirm)")
}
}
}
}
if dm.pendingFriendsADMIN.isEmpty {
Text("No friend request yet").font(.caption)
}
HStack {
Image(systemName: "person.3")
Text("Friends:")
}.font(.headline)
.foregroundColor(.blue)
ForEach(dm.confirmedFriendADMIN) { friend in
HStack{
if friend.immagine == nil{
Image(systemName: "person")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
.clipShape(Circle())
} else {
URLImage(friend.immagine!) { proxy in
proxy.image
.resizable()
.frame(width: 30, height: 30, alignment: .center)
.clipShape(Circle())
}
}
Text(friend.username)
Spacer()
Image(systemName: "checkmark.circle").foregroundColor(.green)
Button(action: {
self.dm.removeFriend(me: self.meInfo!, friendConfirm: friend)
}, label: {
Text("remove friend")
})
}.padding(.all)
}
}.padding(.trailing)
}
.onAppear {
self.dm.newListUpdateForAdmin(userInfo: self.meInfo!)
}
}
var fakebar: some View {
ZStack {
HStack {
Spacer()
Image(systemName: "chevron.compact.down")
.font(.system(size: 60))
.aspectRatio(contentMode: .fit)
.foregroundColor(.white)
Spacer()
}
HStack {
Spacer()
Button(action: {
self.dismissView.toggle()
}) {
Text("Close")
.fontWeight(.bold)
.foregroundColor(.white)
.padding(.horizontal)
}
}
}
.frame(height: 44)
.background(Color.green.padding(.top, -44))
}
}
I use the onAppear to trigger the list update with the .addSnapshotListener
.onAppear {
self.dm.newListUpdateForAdmin(userInfo: self.meInfo!)
}
I can't find out why... is it correct the way how i'm using the .addSnapshotListener ?
Or any other idea how to handle the friend request. happy to change my way how to deal with the friend request.
Thanks
Some suggestions that maybe helps:
1. Implement listener state control: These help to control when a user add, modify or remove a record, helps not to reload all data and allow not to duplicate events, for example in the code below I get all users (event documentChange.add) if a new user is added you don't reload all users array.
Api:
function getUsersPending(userInfo: UserModel, onSuccess: #escaping([UserModel]) -> Void, onError: #escaping(_ errorMessage: String) -> Void, newPendingUser: #escaping(UserModel) -> Void ) {
db.collection("userUser").document(userInfo.email).collection("pendingFriends").addSnapshotListener(includeMetadataChanges: false) { documentSnapshot, error in
self.pendingFriendsUSER = []
guard let snapshot = documentSnapshot else { return }
var userPendingArray = [UserModel]()
snapshot.documentChanges.forEach { (documentChange) in
switch documentChange.type {
case: .added :
let dict = documentChange.document.data()
//Get User from firebase doc pendingUser = ....
newPendingUser(pendingUser) //escape New User
userPendingArray.appen(pendingUser)
print("Pending User Added")
case .modified :
//implements action (new escaping)
print("Pending User Modified")
case .removed :
print("User pending removed")
}
}
onSuccess(userPendingArray)
}
Users pending ViewModel sample
class UserPendingViewModel() : ObservableObject {
#Published var usersPending: [UserModel] = []
#Published var isLoading = false
var errorString : String = ""
func loadUsersPending() {
self.usersPending = []
self.isLoading = true
dm. getUsersPending(userInfo: userModel, onSuccess: { (users) in
if (self.usersPending.isEmpty) { self.usersPending = users }
self.isLoading = false
}, onError: { (errorMessage) in
print("Error Message \(errorMessage)")
}, newPendingUser: { (user) in
if (!self.usersPending.isEmpty) { self.usersPending.append(user) }
})
}
}
View
struct UserPendingView: View {
#ObservedObject var model = UserPendingViewModel()
var body: some View {
ScrollView {
if !model.usersPending.isEmpty {
ForEach(model.usersPending, id: \.messageId) { user in
//Show your data
}
}
}.onAppear{ self.model.loadUsersPending() }
}
2. Activate / Deactivate listeners. If your app is not showing the pending view users no need to keep alive listeners. Activate the listener onAppear and Deactivate onDisappear.
On previous sample New escaping, var listener declaration and result on Api
function getUsersPending(userInfo: UserModel, onSuccess: #escaping([UserModel]) -> Void, onError: #escaping(_ errorMessage: String) -> Void, newPendingUser: #escaping(UserModel) -> Void, listener: #escaping(_ listenerHandle: ListenerRegistration) -> Void ) { ) {
let listenerRegistration = db.collection("userUser").document(userInfo.email).collection("pendingFriends").addSnapshotListener(includeMetadataChanges: false) { documentSnapshot, error in
self.pendingFriendsUSER = []
guard let snapshot = documentSnapshot else { return }
var userPendingArray = [UserModel]()
snapshot.documentChanges.forEach { (documentChange) in
switch documentChange.type {
case: .added :
let dict = documentChange.document.data()
//Get User from firebase doc pendingUser = ....
newPendingUser(pendingUser) //escape New User
userPendingArray.appen(pendingUser)
print("Pending User Added")
case .modified :
//implements action (new escaping)
print("Pending User Modified")
case .removed :
print("User pending removed")
}
}
onSuccess(userPendingArray)
}
listener(listenerRegistration) //escaping listener
}
Users pending ViewModel sample, declaration listener and add function listener result (Note: import Firebase)
class UserPendingViewModel() : ObservableObject {
#Published var usersPending: [UserModel] = []
#Published var isLoading = false
var errorString : String = ""
var listener : ListenerRegistration!
func loadUsersPending() {
self.usersPending = []
self.isLoading = true
dm. getUsersPending(userInfo: userModel, onSuccess: { (users) in
if (self.usersPending.isEmpty) { self.usersPending = users }
self.isLoading = false
}, onError: { (errorMessage) in
print("Error Message \(errorMessage)")
}, newPendingUser: { (user) in
if (!self.usersPending.isEmpty) { self.usersPending.append(user) }
}) { (listener) in
self.listener = listener
}
}
}
View implement onDisappear to disconnect listener
struct UserPendingView: View {
#ObservedObject var model = UserPendingViewModel()
var body: some View {
ScrollView {
if !model.usersPending.isEmpty {
ForEach(model.usersPending, id: \.messageId) { user in
//Show your data
}
}
}.onAppear{ self.model.loadUsersPending() }
.onDisappear {
if self.model.listener != nil {
self.model.listener.remove()
}
}
}

Swift Array.append in init() not working in combination with Firestore (Google Cloud/FIrebase)

I have tried my first SwiftUI Project.
I only want to show some data stored in Firestore (Google Firebase).
Here is my code:
import SwiftUI
import FirebaseFirestore
import FirebaseFirestoreSwift
struct MonsterObj: Identifiable, Equatable, Hashable {
var id = UUID()
var name: String
var element: String
var immune: String
var size: String
var twoStarWeakness: String
var threeStarWeakness: String
#if DEBUG
static let exampleMonster = MonsterObj(id: UUID(), name: "Test Monster", element: "Test Element", immun: "Test immun", groesse: "Test groesse", twoStarWeakness: "Test 2 Weakness", threeStarWeakness: "Test3 Weakness")
#endif
}
class MonsterC: ObservableObject {
#Published var monsters = [MonsterObj]()
init() {
let db = Firestore.firestore()
var monsterNames: [String] = []
db.collection("Monster").getDocuments() { (querySnapshot, err) in
if let err = err {
print(err)
} else {
for document in querySnapshot!.documents {
monsterNames.append("\(document.documentID)")
print("document: \(document.documentID)")
}
}
}
for monsterName in monsterNames {
print(monsterName)
db.collection("Monster").document(monsterName).getDocument { (document, error) in
if let document = document, document.exists {
let elementGetter = document.get("element") as! String
let immuneGetter = document.get("immune") as! String
let sizeGetter = document.get("size") as! String
let twoStarWeaknessGetter = document.get("2 star weakness") as! String
let threeStarWeaknessGetter = document.get("3 star weakness")as! String
self.monsters.append(MonsterObj(name: monsterName, element: elementGetter, immune: immuneGetter, size: sizeGetter, twoStarWeakness: twoStarWeaknessGetter, threeStarWeakness: threeStarWeaknessGetter))
}
}
}
}
}
This is my View:
import SwiftUI
struct ContentView: View {
#EnvironmentObject var monsterT: MonsterC
var body: some View {
List(monsterT.monsters, id: \.self) { monster in
Text(monster.name)
}
}
}
And I did following to SceneDelegate.swift:
var window: UIWindow?
var monsterT = MonsterC()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView().environmentObject(monsterT)
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
So my Problem is the list is empty.
I figured out in init of class MonsterC the line monsterNames.append("\document.documentID)") does not append anything to monsterNames.
But print("document: \(document.documentID)") is printing all monsterNames.
My google Firestore structure looks like this:
Collection -> Document -> Fields
-------------------------------------------
Monster -> Anjanath -> immune: fire,
element: fire
etc.
There's only one collection ("Monster").
Can anyone explain to a beginner why .append is not working here but print is doing everything right?
You need to understand calling asynchronous functions. My advice is to restructure your code, it is not a good idea to do these async calls in your init().
Your function "db.collection("Monster").getDocuments() { (querySnapshot, err) in ..."
is asynchronous. You must either wait till it is finished to use the results, or do what you need inside the function. Note you also have another async function "db.collection("Monster").document(monsterName).getDocument {"
So .append is not working because the results of your function "db.collection("Monster").getDocuments() { (querySnapshot, err) in ..." are not available when you do the .append.
So if you must use this dodgy code, try this to fix your array problem:
class MonsterC: ObservableObject {
#Published var monsters = [MonsterObj]()
init() {
let db = Firestore.firestore()
db.collection("Monster").getDocuments() { (querySnapshot, err) in
if let err = err {
print(err)
} else {
var monsterNames: [String] = []
for document in querySnapshot!.documents {
monsterNames.append("\(document.documentID)")
print("document: \(document.documentID)")
}
for monsterName in monsterNames {
print(monsterName)
db.collection("Monster").document(monsterName).getDocument { (document, error) in
if let document = document, document.exists {
let elementGetter = document.get("element") as! String
let immuneGetter = document.get("immune") as! String
let sizeGetter = document.get("size") as! String
let twoStarWeaknessGetter = document.get("2 star weakness") as! String
let threeStarWeaknessGetter = document.get("3 star weakness")as! String
self.monsters.append(MonsterObj(name: monsterName, element: elementGetter, immune: immuneGetter, size: sizeGetter, twoStarWeakness: twoStarWeaknessGetter, threeStarWeakness: threeStarWeaknessGetter))
}
}
}
}
}
}
}

Resources