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)
}
Related
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
}
}
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")
}
}
}
}
I have a class with a #Published variable named userData. When the function fbLogic.getUserData() is called in this class, it updates the userData array with the data which is populated from a call to Firestore database.
The issue is that this published variable is not updating in view which access it. Or more so, these functions don't wait until the Firestore call is finished before accessing the variable. So the BarGraphView() is called with empty reports[] array, and this array isn't updated by the convertToReports() call because convertToReports() is called with an empty userData array, because fbLogic.getUserData() hasn't finished retrieving the Firebase data yet. Hopefully the code explains it better.
This is the View which is making reference to the array.
struct StatsView: View {
let ID: String;
#EnvironmentObject var statsController: StatsDataController
#State var reports: [Report] = [Report]();
#Binding var fbLogic: FirebaseLogic
var body: some View {
BarGraphView(selection: $selection, reports: $reports, originalReports: $originalReports).environmentObject(statsController).onAppear{
fbLogic.userData = fbLogic.getUserData(uid: ID); //supposed to update the userData #Published var
//These are called before fbLogic.userData has even been updated !!!
self.reports = statsController.convertToReports(users: fbLogic.userData);
print("fblogic data ", fbLogic.userData, " ", ID);};
}
This View prints out "fb logic data []", before the call to .getUserData() has even finished, and so the call to convertToReports() happens with an empty array, instead of a populated array.
Please, how do I make this view recognise that the fbLogic.userData #Pubished array has updated and call convertToReports(), and redraw the BarGraphView with the POPULATED array.
Here is part of the fbLogic class so you can understand the call to firebase:
class FirebaseLogic: ObservableObject {
#Published var userData = [UserData]()
func getUserData(uid: String) -> [UserData]{
let db = Firestore.firestore()
userData = [UserData]()
let auth = Auth.auth();
let currentUser = (auth.currentUser?.uid)!
let data = db.collection("UserData").document(currentUser).collection("Data") .getDocuments { [self] (snapshot, error) in
guard let snapshot = snapshot, error == nil else {
return
}
print("Number of documents fb logic get user data: \(snapshot.documents.count ?? -1)")
snapshot.documents.forEach({ (documentSnapshot) in
let documentData = documentSnapshot.data()
//CONVERT TO userObject here (removed for simplicity)
userData.append(userObject)
})
}
return userData
}
Try using
#ObservedObject var fbLogic: FirebaseLogic
in StatsView. Make sure you pass an #ObservedObject type to StatsView when you pass data to this view from parent view.
The reason this isn't working is that the call to getDocuments() is asynchronous. So when you return the array of mapped documents (consider using Codable), it is still empty.
Here is an updated version of your code that assigns the mapped documents to the published property.
class FirebaseLogic: ObservableObject {
#Published var userData = [UserData]()
func getUserData(uid: String) -> [UserData] {
let db = Firestore.firestore()
let localUserData = [UserData]()
let auth = Auth.auth();
let currentUser = (auth.currentUser?.uid)! // (1)
db.collection("UserData").document(currentUser).collection("Data").getDocuments { [self] (snapshot, error) in
guard let snapshot = snapshot, error == nil else {
return
}
print("Number of documents fb logic get user data: \(snapshot.documents.count ?? -1)")
snapshot.documents.forEach { documentSnapshot in
let documentData = documentSnapshot.data()
// CONVERT TO userObject here (removed for simplicity)
localUserData.append(userObject)
}
self.userData = localUserData
}
}
}
You can further improve this code by using .map() to transform your documents. See this code snippet that is part of my article about mapping Firestore documents to/from Swift:
listenerRegistration = db.collection("programming-languages")
.addSnapshotListener { [weak self] (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
self?.errorMessage = "No documents in 'programming-languages' collection"
return
}
self?.programmingLanguages = documents.compactMap { queryDocumentSnapshot in
let result = Result { try queryDocumentSnapshot.data(as: ProgrammingLanguage.self) }
...
I have an application that is:
The first view is a list of stores (the list has been read from Firebase successfully) because there is a (List)
The second view is the store details (here, I couldn't read the data from Firebase)
#ObservedObject var viewModel = StoreViewModel()
var body: some View {
ScrollView {
VStack {
Text(viewModel.stores.name)}}
How read this data from Firebase ?
class StoreViewModel: ObservableObject {
#Published var stores = [StoreView]()
private var db = Firestore.firestore()
func fetchData() {
db.collection("Stores").addSnapshotListener{(querySnapshot, error)in
guard let documents = querySnapshot?.documents else {
print("No documents in Firebease")
return
}
self.stores = documents.compactMap { queryDocumentSnapshot -> StoreView? in
return try? queryDocumentSnapshot.data(as: StoreView.self)
}
}
}
}
First, your view model never reads from the database because fetchData() is never called. Therefore, consider calling it automatically in the view model's initializer:
class StoreViewModel: ObservableObject {
#Published var stores = [StoreView]()
private let db = Firestore.firestore()
init() {
db.collection("Stores").addSnapshotListener{ (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents in Firebease")
if let error = error {
print(error)
}
return
}
self.stores = documents.compactMap { queryDocumentSnapshot -> StoreView? in
return try? queryDocumentSnapshot.data(as: StoreView.self)
}
}
}
}
Second, stores is an array of StoreView objects, so you must pick one the elements of the array to display any data from it:
Text(viewModel.stores[0].name) // this will display the name of the first store added to the array
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.
}