Mapping data to a variable not working Firebase-SwiftUI - firebase

i have data in my vm, but i cannot map that data to the #published variable for an unknown reason. all the keys are the same, and the data is correct.
Model:
struct Cars: Identifiable, Codable {
let brand: String?
let driverTrain: String?
let engine: String?
let fuel: String?
let horsePower: String?
let model: String?
// let photo: String?
let price: String?
let topSpeed: String?
let transmission: String?
let trim: String?
let year: String?
let carID: String
var id: String {
return carID
}
enum CodingKeys: String, CodingKey {
case brand = "Brand"
case driverTrain = "Driver train"
case engine = "Engine"
case fuel = "Fuel"
case horsePower = "HorsePower"
case model = "Model"
// case photo = "Photo"
case price = "Price"
case topSpeed = "Top Speed"
case transmission = "Transmission"
case trim = "Trim"
case year = "Year"
case carID = "id_cars"
}
}
View Model:
import Foundation
import FirebaseFirestore
class AssetsViewModel: ObservableObject {
#Published var cars = [Cars]()
private var db = Firestore.firestore()
func fetchData() {
print("Fetching data...")
db.collection("Cars").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents found")
return
}
print("Found \(documents.count) documents")
DispatchQueue.main.async {
self.cars = documents.map { (queryDocumentSnapshot) -> Cars in
let data = queryDocumentSnapshot.data()
// print("Data for document \(queryDocumentSnapshot.documentID): \(data)")
let brand = data["Brand"] as? String
let driverTrain = data["Driver train"] as? String
let engine = data["Engine"] as? String
let fuel = data["Fuel"] as? String
let horsePower = data["HorsePower"] as? String
let model = data["Model"] as? String
let price = data["Price"] as? String
let topSpeed = data["Top Speed"] as? String
let transmission = data["Transmission"] as? String
let trim = data["Trim"] as? String
let year = data["Year"] as? String
let carID = data["id_cars"] as? String ?? ""
let car = Cars(brand: brand, driverTrain: driverTrain, engine: engine, fuel: fuel, horsePower: horsePower, model: model, price: price, topSpeed: topSpeed, transmission: transmission, trim: trim, year: year, carID: carID)
// print("Created car: \(car)")
return car
}
}
print (self.cars)
}
}
}
photos of the firebase will be providided
I have putting checking if all the keys are the same, if the data is actually running through (the data is perfect and works when i print it, but i just cannot get it into the variable) and ive tried to put it on Dispatch.Queue.Async

Related

SwiftUI: Trying to get a Firebase Firestore subcollection as a sub-type of the main collection [duplicate]

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

Firebase Swiftui not saving fetched data

I am trying to fetch a userID key from firebase. The code is initiated in a .OnAppear and the data is fetched. If I try to print self.appUserId inside docRef it prints out correctly. If I print it at the end like in the code below the variable is empty. appUserID is declared as:
#State private var appUserId = ""
Can someone explain how I can fetch this firebase data and save it to a string
.onAppear{
let userName = user?.email ?? ""
let docRef = db.collection("SUBSCRIBE").document(userName)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
//Setting Values
let data = document.data()
self.appUserId = data?["AppUserId"] as? String ?? ""
} else {
print("Document does not exist")
}
}
print(self.appUserId)
}

SwiftUI - Get field value from a Firebase document

I'm pretty new to SwiftUI and using Firebase and have stumbled upon retrieving a value from a document on Firebase.
I've done some Googling but have yet to find a way to do this.
My code so far looks like this:
//Test1: get user info
func readUserInfo(_ uid: String) -> String {
let db = Firestore.firestore()
let docRef = db.collection("users").document(uid)
var returnThis = ""
docRef.getDocument { (document, error) -> String in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
print("Document data: \(dataDescription)")
returnThis = dataDescription
return(dataDescription)
} else {
print("Document does not exist")
}
}
return returnThis
}
//Test2: get user info
func readUserInfo(_ uid: String) -> String {
let db = Firestore.firestore()
let docRef = db.collection("users").document(uid)
var property = "not found"
docRef.getDocument { (document, error) in
if let document = document, document.exists {
property = document.get("phoneNumber") as! String
}
}
return property
}
Test1:
Got an error saying "Declared closure result 'String' is incompatible with contextual type 'Void'"
Test2:
The returned value is always "not found", so I guess assigning document.get("phoneNumber") as! String to the property variable didn't work.
In both cases, I was able to print the value out. However, I want the value to be passed to a Text element so I can display it on the interface.
Could someone please help? Thanks so much in advance.
The reason why the value is never found is because the network request you are making takes time to retrieve that data. By the time it reaches the return statement the data from firebase has not been been retrieved yet so it will just return the value it was initially set to which was an empty string. What you should do is add a completion handler to the function so that once the data is finally retrieved you can then return it.
struct ContentView: View {
#State private var text: String = ""
var body: some View {
Text(text)
.onAppear {
readUserInfo("sydQu5cpteuhPEgmHepn") { text in
self.text = text
}
}
}
func readUserInfo(_ uid: String, _ completion: #escaping (String) -> Void) {
let db = Firestore.firestore()
let docRef = db.collection("users").document(uid)
docRef.getDocument { document, error in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
print("Document data: \(dataDescription)")
completion(dataDescription)
} else {
completion("Document does not exist")
}
}
}
}

Firebase How to get friends info

I have retrieved friends UIDs, What's the best way to observe their info like name, photo etc.. ?
now UidArray holding friends uids, Should i loop for each uid to show their info on Tableview ?
let userIdArray = self.UidArray
for id in userIdArray {
ref.observeSingleEvent(of: .value, with: { (snapshot) in
guard let dictionary = snapshot.value as? [String: Any] else {
return
}
let displayN = dictionary["displayname"] as? String
let Uid = dictionary["uid"] as? String
let ImgUrl = dictionary["urlToImage"] as? String
// Append results to another array, reload table view if loop is done.
User Model:
class Userr: NSObject {
var displayname: String?
var email: String?
var gender: String?
var uid: String?
var urlToImage: String?
}
I just want to fetch data once. I don't want to hit the database each time cell displays by scrolling.
I would like to know what's the best way to get friends info based on array of their uids?
A for loop isn't needed and is very overkill as the .childAdded that is used in the observe method for Firebase will call the function as many times as there are users, just like a for loop.
func handleFetchAllUsers(_ completion: #escaping ([User]) -> ()) {
DispatchQueue.global(qos: .userInteractive).async {
var data: [User] = []
let reference = Database.database().reference().child("users")
reference.observe(.childAdded, with: { (snapshot) in
guard let dictionary = snapshot.value as? [String: AnyObject] else { return }
let username = dictionary["username"] as? String
let name = dictionary["name"] as? String
let email = dictionary["email"] as? String
let user = User()
user.name = name
user.username = username
user.email = email
data.append(user)
DispatchQueue.main.async {
completion(data)
}
}, withCancel: nil)
}, withCancel: nil)
}
}
You'd call the function like so:
var users: [User] = []
handleFetchAllUsers { (user) in // USER IS THE ARRAY OF USERS RETURNED FROM THE FUNCTION
self.users = user
self.collectionView.reloadData() // IF YOU'RE USING A UICOLLECTIONVIEW.
}

Casting Firebase Snapshot to Swift 3 object

I am retrieving Objects from the Firebase DB and I need to cast them to a custom struct class object
Class:
struct Request {
var Address: String!
var Position: Position!
var RequestID: String!
var Status: String!
}
The function that gets the snapshot from my Firebase DB:
self.ref.child("requests").observe(.childAdded, with: { snapshot in
//I need to cast this snapshot object to a new Request object here
let dataChange = snapshot.value as? [String:AnyObject]
print(dataChange)
})
How can I get this done?
A couple of things. Firebase doesn't have objects - it's a JSON structure. When you get the snapshot.value like so:
let dataChange = snapshot.value as? [String:AnyObject]
The [String: AnyObject] is defining the data as a Dictionary.
You can then access the key:value pairs in dataChange like this
let address = dataChange["address"]
and
let position = dataChange["position"]
From there you can either create new objects and populate them within the closure (adding them to an array for example) or put more intelligence in the object and pass the dictionary and let the object populate itself.
The following is pseudo code but it presents the process:
//create the object and populate it 'manually'
self.ref.child("requests").observe(.childAdded, with: { snapshot in
let dataChange = snapshot.value as? [String:AnyObject]
let aRequest = Request()
aRequest.address = dataChange["address"]
aRequest.position = dataChange["position"]
self.requestArray.append(aRequest)
})
or
Class Request {
var address = ""
var position = ""
func initWithDict(aDict: [String: AnyObject]) {
self.address = aDict["address"]
self.position = aDict["position"]
}
}
//let the object populate itself.
self.ref.child("requests").observe(.childAdded, with: { snapshot in
let dataChange = snapshot.value as? [String:AnyObject]
let aRequest = Request(initWithDict: dataChange)
self.requestArray.append(aRequest)
})
If your struct has a lot of fields, it's easier to do like this (Swift 4+):
struct Request: Decodable {
var Address: String
var RequestID: String
var Status: String
}
self.ref.child("requests").observe(.childAdded, with: { snapshot in
guard let data = try? JSONSerialization.data(withJSONObject: snapshot.value as Any, options: []) else { return }
let yourStructObject = try? JSONDecoder().decode(Request.self, from: data)
}

Resources