How to populate loaded records from firebase? - swift

I wrote the function to lad the records from firebase but there's an error
Escaping closure captures mutating 'self' parameter
The function is written as follows:
let db = Firestore.firestore()
#State var libraryImages: [LibraryImage] = []
mutating func loadImages() {
libraryImages = []
db.collection(K.FStore.CollectionImages.collectionName).getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
if let snapshotDocuments = querySnapshot?.documents {
for document in snapshotDocuments {
let documentData = document.data()
let title: String = documentData[K.FStore.CollectionImages.title] as! String
let thumbnailUrl: String = documentData[K.FStore.CollectionImages.thumbnailUrl] as! String
let svgUrl: String = documentData[K.FStore.CollectionImages.svgUrl] as! String
let libraryImageItem = LibraryImage(title: title, thumbnailUrl: thumbnailUrl, svgUrl: svgUrl)
self.libraryImages.append(libraryImageItem)
}
}
}
}
}
Does anyone know what is causing an error and how to get rid of it?

Move all this into reference type view model and use it as observed object in your view
Here is a demo of possible approach:
struct DemoView: View {
#ObservedObject var vm = ImagesViewModel()
// #StateObject var vm = ImagesViewModel() // << SwiftUI 2.0
var body: some View {
Text("Loaded images: \(vm.libraryImages.count)")
.onAppear {
self.vm.loadImages()
}
}
}
class ImagesViewModel: ObservableObject {
let db = Firestore.firestore()
#Published var libraryImages: [LibraryImage] = []
func loadImages() {
libraryImages = []
db.collection(K.FStore.CollectionImages.collectionName).getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
if let snapshotDocuments = querySnapshot?.documents {
var images = [LibraryImage]()
for document in snapshotDocuments {
let documentData = document.data()
let title: String = documentData[K.FStore.CollectionImages.title] as! String
let thumbnailUrl: String = documentData[K.FStore.CollectionImages.thumbnailUrl] as! String
let svgUrl: String = documentData[K.FStore.CollectionImages.svgUrl] as! String
let libraryImageItem = LibraryImage(title: title, thumbnailUrl: thumbnailUrl, svgUrl: svgUrl)
images.append(libraryImageItem)
}
DispatchQueue.main.async {
self.libraryImages = images
}
}
}
}
}
}

Related

Matching collections in Firestore using SwiftUI

I'm try to fetch documents from firestore by matching the collections using the code below. I'm fetching the documents from collection("users") in case anyone update their info in the future which the new info will only uploaded to the collection("users") but not the collection("clients").
struct NewFriend: Identifiable, Hashable, Decodable {
#DocumentID var id: String?
var name: String
var uid: String
}
struct NewProspect: Identifiable, Hashable, Decodable {
#DocumentID var id: String?
var name: String
var uid: String
}
class AuthViewModel: ObservableObject {
#Published var userName = ""
#Published var friendName1 = "FriendName1"
#Published var friendName2 = "FriendName2"
#Published var friendUid1 = "friendUid1"
#Published var friendUid2 = "friendUid2"
#Published var matchingUid = ""
let friends = [NewFriend]()
let clients = [NewProspect]()
func uploadInfo() {
guard let uid = FireBaseManager.shared.auth.currentUser?.uid else { return }
let data: [String: Any] = [
"name": self.userName,
"uid": uid
]
FireBaseManager.shared.firestore.collection("users").document(uid).setData(data) { error in
if let error = error {
print("DEBUG: Failed to store user info with error: \(error.localizedDescription)")
return
}
}
}
}
func addFriend() {
guard let uid = FireBaseManager.shared.auth.currentUser?.uid else { return }
let data: [String: Any] = [
"name": self.friendName1,
"uid": self.friendUid1
]
FireBaseManager.shared.firestore.collection("users").document(uid).collections("clients").document(self.friendUid1).setData(data) { error in
if let error = error {
print("DEBUG: Failed to store user info with error: \(error.localizedDescription)")
return
}
}
func addFriend2() {
guard let uid = FireBaseManager.shared.auth.currentUser?.uid else { return }
let data: [String: Any] = [
"name": self.friendName2,
"uid": self.friendUid2
]
FireBaseManager.shared.firestore.collection("users").document(uid).collections("clients").document(self.friendUid2).setData(data) { error in
if let error = error {
print("DEBUG: Failed to store user info with error: \(error.localizedDescription)")
return
}
}
func fetchFriends() {
guard let uid = FireBaseManager.shared.auth.currentUser?.uid else { return }
FireBaseManager.shared.firestore.collection("users").document(uid).collection("clients").getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else { return }
self.friends = documents.compactMap({ try? $0.data(as: NewFriend.self) })
for i in 0 ..< self.friends.count {
self.matchingUid = self.friend[i].uid
print("print1: \(self.matchingUid)")
FireBaseManager.shared.firestore.collection("users")
.whereField("uid", isEqualTo: self.friendUid)
.getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else { return }
self.clients = documents.compactMap({ try? $0.data(as: NewProspect.self) })
print("print2: \(self.matchingUid)")
}
}
}
}
}
struct ListView: View {
#ObservedObject var viewModel = AuthViewModel()
var body: some View {
List {
ForEach(viewModel.clients) { client in
Text(client.name)
.padding()
}
}
.onAppear {
viewModel.fetchFriends()
}
}
}
I'm able to print both of the friends' uid in print1 but when it comes to print2 only one of the uid is printed and the list only shows one of the two friends. Any idea why is this happening? Or is there any better way to make this function work?
When you fetch users before print2, you are fetching with a where clause.
self.db.collection("users")
// This WHERE condition causes that only one friend will be found
.whereField("uid", isEqualTo: self.friendUid1)
.getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else { return }
self.clients = documents.compactMap({ try? $0.data(as: NewProspect.self) })
print("print2: \(self.matchingUid)")
}
The one in your example (self.friendUid) is not part of your AuthViewModel, I assume you meant .whereField("uid", isEqualTo: self.friendUid1)
Anyway, that where clause is limiting your results to one of the friends (in this case the friend with friendUid1 only.
Maybe you can use .whereField("uid", in: [self.friendUid1, self.friendUid2]) instead:
self.db.collection("users")
// In this case the WHERE condition cares about both friends
.whereField("uid", in: [self.friendUid1, self.friendUid2])
.getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else { return }
self.clients = documents.compactMap({ try? $0.data(as: NewProspect.self) })
print("print2: \(self.matchingUid)")
}

Thread 1: EXC_BAD_INSTRUCTION when fetching data

I get this Error -> Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0) randomly. I don't quite understand when exactly it happens. Most of the times it is when the view refreshes. The Error appears at the line where group.leave() gets executed.
What am I trying to do:
I want to fetch albums with their image, name and songs that also have a name and image from my firebase database. I checked for the values and they're all right as far as I can tell. But when trying to show them it is random what shows. Sometimes everything is right, sometimes one album gets showed twice, sometimes only one album gets showed at all, sometimes one album has the songs of the other album.
My firebase database has albums stored as documents, each document has albumimage/name and 2 subcollections of "unlocked" with documents(user uid) that store "locked":Bool and "songs" with a document for each song that stores image/name
This is the function that fetches my albums with their songs:
let group = DispatchGroup()
#State var albums: [Album] = []
#State var albumSongs: [AlbumSong] = []
func fetchAlbums() {
FirebaseManager.shared.firestore.collection("albums").getDocuments { querySnapshot, error in
if let error = error {
print(error.localizedDescription)
return
}
guard let documents = querySnapshot?.documents else {
return
}
let uid = FirebaseManager.shared.auth.currentUser?.uid
documents.forEach { document in
let data = document.data()
let name = data["name"] as? String ?? ""
let artist = data["artist"] as? String ?? ""
let releaseDate = data["releaseDate"] as? Date ?? Date()
let price = data["price"] as? Int ?? 0
let albumImageUrl = data["albumImageUrl"] as? String ?? ""
let docID = document.documentID
FirebaseManager.shared.firestore.collection("albums").document(docID)
.collection("songs").getDocuments { querySnapshot, error in
if let error = error {
return
}
guard let documents = querySnapshot?.documents else {
return
}
self.albumSongs = documents.compactMap { document -> AlbumSong? in
do {
return try document.data(as: AlbumSong.self)
} catch {
return nil
}
}
group.leave()
}
FirebaseManager.shared.firestore.collection("albums").document(docID)
.collection("unlocked").document(uid ?? "").getDocument { docSnapshot, error in
if let error = error {
return
}
guard let document = docSnapshot?.data() else {
return
}
group.enter()
group.notify(queue: DispatchQueue.global()) {
if document["locked"] as! Bool == true {
self.albums.append(Album(name: name, artist: artist,
songs: albumSongs, releaseDate: releaseDate, price: price, albumImageUrl: albumImageUrl))
print("albums: ",albums)
}
}
}
}
}
}
I call my fetchAlbums() in my view .onAppear()
My AlbumSong:
struct AlbumSong: Identifiable, Codable {
#DocumentID var id: String? = UUID().uuidString
let title: String
let duration: TimeInterval
var image: String
let artist: String
let track: String
}
My Album:
struct Album: Identifiable, Codable {
#DocumentID var id: String? = UUID().uuidString
let name: String
let artist: String
let songs: [AlbumSong]
let releaseDate: Date
let price: Int
let albumImageUrl: String
}
I tried looking into how to fetch data from firebase with async function but I couldn't get my code to work and using dispatchGroup worked fine when I only have one album. I would appreciate answers explaining how this code would work with async, I really tried my best figuring it out by myself a long time. Also I would love to know what exactly is happening with DispatchGroup and why it works fine having one album but not with multiple ones.
I think you are over complicating something that is very simple with async await
First, your Models need some adjusting, it may be the source of some of your issues.
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
struct AlbumSong: Identifiable, Codable {
//No need to set a UUID `#DocumentID` provides an ID
#DocumentID var id: String?
let title: String
let duration: TimeInterval
var image: String
let artist: String
let track: String
}
struct Album: Identifiable, Codable {
//No need to set a UUID `#DocumentID` provides an ID
#DocumentID var id: String?
let name: String
let artist: String
//Change to var and make nil, the initial decoding will be blank
//If any of the other variables might be optional add the question mark
var songs: [AlbumSong]?
let releaseDate: Date
let price: Int
let albumImageUrl: String
}
Then you can create a service that does the heavy lifting with the Firestore.
struct NestedFirestoreService{
private let store : Firestore = .firestore()
let ALBUM_PATH = "albums"
let SONG_PATH = "songs"
///Retrieves Albums and Songs
func retrieveAlbums() async throws -> [Album] {
//Get the albums
var albums: [Album] = try await retrieve(path: ALBUM_PATH)
//Get the songs, **NOTE: leaving the array of songs instead of making a separate collection might work best.
for (idx, album) in albums.enumerated() {
if let id = album.id{
albums[idx].songs = try await retrieve(path: "\(ALBUM_PATH)/\(id)/\(SONG_PATH)")
}else{
print("\(album) :: has invalid id")
}
}
//Add another loop for `unlocked` here just like the one above.
return albums
}
///retrieves all the documents in the collection at the path
public func retrieve<FC : Identifiable & Codable>(path: String) async throws -> [FC]{
let querySnapshot = try await store.collection(path)
.getDocuments()
return try querySnapshot.documents.compactMap { document in
try document.data(as: FC.self)
}
}
}
Then you can implement it with just a few lines in your presentation layer.
import SwiftUI
#MainActor
class AlbumListViewModel: ObservableObject{
#Published var albums: [Album] = []
private let svc = NestedFirestoreService()
func loadAlbums() async throws{
albums = try await svc.retrieveAlbums()
}
}
struct AlbumListView: View {
#StateObject var vm: AlbumListViewModel = .init()
var body: some View {
List(vm.albums, id:\.id) { album in
DisclosureGroup(album.name) {
ForEach(album.songs ?? [], id:\.id){ song in
Text(song.title)
}
}
}.task {
do{
try await vm.loadAlbums()
}catch{
print(error)
}
}
}
}
struct AlbumListView_Previews: PreviewProvider {
static var previews: some View {
AlbumListView()
}
}
If you get any decoding errors make the variables optional by adding the question mark to the type like I did with the array.
Just use them in the correct order:
let group = DispatchGroup()
#State var albums: [Album] = []
#State var albumSongs: [AlbumSong] = []
func fetchAlbums() {
group.enter()
FirebaseManager.shared.firestore.collection("albums").getDocuments { querySnapshot, error in
if let error = error {
print(error.localizedDescription)
group.leave()
return
}
guard let documents = querySnapshot?.documents else {
group.leave()
return
}
let uid = FirebaseManager.shared.auth.currentUser?.uid
documents.forEach { document in
let data = document.data()
let name = data["name"] as? String ?? ""
let artist = data["artist"] as? String ?? ""
let releaseDate = data["releaseDate"] as? Date ?? Date()
let price = data["price"] as? Int ?? 0
let albumImageUrl = data["albumImageUrl"] as? String ?? ""
let docID = document.documentID
group.enter()
FirebaseManager.shared.firestore.collection("albums").document(docID)
.collection("songs").getDocuments { querySnapshot, error in
if let error = error {
group.leave()
return
}
guard let documents = querySnapshot?.documents else {
group.leave()
return
}
self.albumSongs = documents.compactMap { document -> AlbumSong? in
do {
group.leave()
return try document.data(as: AlbumSong.self)
} catch {
group.leave()
return nil
}
}
}
group.enter()
FirebaseManager.shared.firestore.collection("albums").document(docID)
.collection("unlocked").document(uid ?? "").getDocument { docSnapshot, error in
if let error = error {
group.leave()
return
}
guard let document = docSnapshot?.data() else {
group.leave()
return
}
if document["locked"] as! Bool == true {
self.albums.append(Album(name: name, artist: artist,
songs: albumSongs, releaseDate: releaseDate, price: price, albumImageUrl: albumImageUrl))
print("albums: ",albums)
}
group.leave()
}
}
group.leave()
}
group.notify(queue: DispatchQueue.global()) {
// do your stuff
}
}

Swift - Fetch elements from Firestore

I have a problem with this function:
func fetchGameFromDB(completionHandler: #escaping ([GamesObject]) -> Void) {
db.collection("games").getDocuments { (querySnapshot, err) in
if let err = err {
print("Error: \(err)")
} else {
self.gameObject = []
for document in querySnapshot!.documents {
print("document \(document.data())")
if let name = document.data()["name"] as? String {
let docRef = self.db.collection("games").document(name)
docRef.getDocument { document, error in
if let document = document {
let data = document.data()
let name = data?["name"] as? String ?? ""
let urlStanding = data?["urlStanding"] as? String ?? ""
let img = data?["gameImg"] as? String ?? ""
let urlUpcoming = data?["urlUpcoming"] as? String ?? ""
self.gameObject.append(GamesObject(name: name, gameImg: img, urlStanding: urlStanding, urlUpcoming: urlUpcoming))
// here i have elements in gameObject
}
// here i have elements in gameObject
}
// here gameObject = []
}
// here gameObject = []
}
completionHandler(self.gameObject)
// here gameObject = []
}
}
}
I get my data well and I add it to my array but when I get to the completionHandler the array is empty.
I find solution, i check if gameObject.count == querySnapshot?.count then I use my completionHandler
func fetchGameFromDB(completionHandler: #escaping ([GamesObject]) -> Void) {
db.collection("games").getDocuments { (querySnapshot, err) in
if let err = err {
print("Error: \(err)")
} else {
self.gameObject = []
querySnapshot?.documents.forEach({ (document) in
if let name = document.data()["name"] as? String {
let docRef = self.db.collection("games").document(name)
docRef.getDocument { document, error in
if let document = document {
let data = document.data()
let name = data?["name"] as? String ?? ""
let urlStanding = data?["urlStanding"] as? String ?? ""
let img = data?["gameImg"] as? String ?? ""
let urlUpcoming = data?["urlUpcoming"] as? String ?? ""
self.gameObject.append(GamesObject(name: name, gameImg: img, urlStanding: urlStanding, urlUpcoming: urlUpcoming))
if self.gameObject.count == querySnapshot?.count {
completionHandler(self.gameObject)
}
}
}
}
})
}
}
}
the first answer there is no problem as long as the missing documents do not exist. but, that cannot be escaped if any of the documents are missing.
how about use to 'DispatchGroup' ?
func fetchGameFromDB(completionHandler: #escaping([GamesObject]) -> Void) {
db.collection("games").getDocuments { (querySnapshot, error) in
guard let docs = querySnapshot?.documents, !docs.isEmpty else {
if let error = error {
print(error)
}
return
}
let group = DispatchGroup()
docs.forEach { doc in
group.enter()
guard let name = doc.data()["name"] as? String else {
group.leave()
return
}
let docRef = self.db.collection("games").document(name)
docRef.getDocument { document, error in
if let document = document, let data = document.data() {
//do something...
gameObjects.append(GamesObject(...)) //init object
}
group.leave()
}
}
group.notify(queue: .main) {
completionHandler(gameObjects)
}
}
}

DispatchGroup is not executing correctly when downloading Firebase Storage Data

I have a situation where I have a closure that fetches User Data from Firebase, and within it I have another closure that downloads an Image from Firebase Storage.
I need to be able to download the image from Storage before signing in the user. I thought I'd try a DispatchGroup(), but I cannot assign the Image Data to self.photoImage, it always returns nil.
self.photoImage = UIImage(data: data!)
This is my first time working with Storage and GroupDispatch(), so any help would be greatly appreciated.
Relevant code:
class AppViewModel: ObservableObject {
private var db = Firestore.firestore()
#Published var userInfo: User?
#Published var signedIn: Bool = false
var handle: AuthStateDidChangeListenerHandle?
let authRef = Auth.auth()
var authHandle : AuthStateDidChangeListenerHandle?
var rootInfoCollection : CollectionReference!
var userIdRef = ""
var photoImage: UIImage?
var downloadImageTask: StorageReference?
var group = DispatchGroup()
func fetchUserData(){
db.collection("Users").document("\(userIdRef)").getDocument { [self] document, error in
// Check for error
if error == nil {
// Check that this document exists
if document != nil && document!.exists {
self.userInfo = document.map { (documentSnapshot) -> User in
let data = documentSnapshot.data()
let uid = data?["uid"] as? UUID ?? UUID()
let company = data?["company"] as? String ?? ""
let name = data?["name"] as? String ?? ""
let admin = data?["admin"] as? Bool ?? false
let photoRef = data?["photoRef"] as? String ?? ""
self.downloadImageTask = Storage.storage().reference().child(photoRef)
return User(uid: uid, company: company, name: name, admin: admin, photoRef: photoRef, photoImage: nil)
}
group.enter()
downloadImageTask?.getData(maxSize: 6 * 1024 * 1024) { data, error in
if let error = error {
print("Got an error fetching data: \(error.localizedDescription)")
return
} else {
self.photoImage = UIImage(data: data!)
}
self.group.leave()
}
DispatchQueue.global(qos: .default).async {
self.group.wait()
DispatchQueue.main.async {
if photoImage != nil {
print("Photo Image Is Set")
}
withAnimation {
self.signedIn = true
}
}
}
}
}
}
}
Although I haven't tested it, maybe this works for you:
class AppViewModel: ObservableObject {
private var db = Firestore.firestore()
#Published var userInfo: User?
#Published var signedIn: Bool = false
var handle: AuthStateDidChangeListenerHandle?
let authRef = Auth.auth()
var authHandle : AuthStateDidChangeListenerHandle?
var rootInfoCollection : CollectionReference!
var userIdRef = ""
var photoImage: UIImage?
var downloadImageTask: StorageReference?
var group = DispatchGroup()
func fetchUserData(){
db.collection("Users").document("\(userIdRef)").getDocument { [self] document, error in
// Check for error
if error == nil {
// Check that this document exists
if document != nil && document!.exists {
self.userInfo = document.map { (documentSnapshot) -> User in
let data = documentSnapshot.data()
let uid = data?["uid"] as? UUID ?? UUID()
let company = data?["company"] as? String ?? ""
let name = data?["name"] as? String ?? ""
let admin = data?["admin"] as? Bool ?? false
let photoRef = data?["photoRef"] as? String ?? ""
self.downloadImageTask = Storage.storage().reference().child(photoRef)
return User(uid: uid, company: company, name: name, admin: admin, photoRef: photoRef, photoImage: nil)
}
group.enter()
downloadImageTask?.getData(maxSize: 6 * 1024 * 1024) { data, error in
if let error = error {
print("Got an error fetching data: \(error.localizedDescription)")
return
} else {
self.photoImage = UIImage(data: data!)
}
self.group.leave()
}
group.notify(queue: .main) { [self] in
if photoImage != nil {
print("Photo Image Is Set")
}
withAnimation {
self.signedIn = true
}
}
}
}
}
}

Firestore Swift update text realtime

I have this way of collecting information.
struct MainText {
var mtext: String
var memoji: String
}
class MainTextModel: ObservableObject {
#Published var maintext : MainText!
init() {
updateData()
}
func updateData() {
let db = Firestore.firestore()
db.collection("maintext").document("Main").getDocument { (snap, err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
let memoji = snap?.get("memoji") as! String
let mtext = snap?.get("mtext") as! String
DispatchQueue.main.async {
self.maintext = MainText(mtext: mtext, memoji: memoji)
}
}
}
}
And such a way of displaying.
#ObservedObject private var viewModel = MainTextModel()
self.viewModel.maintext.memoji
self.viewModel.maintext.mtext
How can I update online without rebooting the view?
Instead of using getDocument, which only gets the document once and doesn't return updates, you'll want to add a snapshot listener.
Here's the Firestore documentation for that: https://firebase.google.com/docs/firestore/query-data/listen
In your case, you'll want to do something like:
db.collection("maintext").document("Main")
.addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
print("Error fetching document: \(error!)")
return
}
guard let data = document.data() else {
print("Document data was empty.")
return
}
if let memoji = data["memoji"] as? String, let mtext = data["mtext"] as? String {
self.maintext = MainText(mtext: mtext, memoji: memoji)
}
}