I am creating an application. I make a request to the firebase store, after that I add the result to the array, but in the end, when the array is displayed from viewDidLoad or other functions, I get an empty array. But if you make a conclusion immediately after the request, then everything is displayed correctly
`
import UIKit
import Firebase
import FirebaseStorage
import FirebaseFirestore
class CatalogVC: UIViewController {
struct Item: Codable {
var title: String
var price: Int
var description: String
var imageUrl: String
}
#Published var items: [Item] = []
let database = Firestore.firestore()
#IBOutlet weak var textViewCatalog: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
let settings = FirestoreSettings()
Firestore.firestore().settings = settings
itemsList()
print(items)
showCatalogVC()
}
#IBAction func showCatalogTapped() {
}
private func showCatalogVC() {
print("SHOW CATALOG")
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let dvc = storyboard.instantiateViewController(withIdentifier: "CatalogVC") as! CatalogVC
self.present(dvc, animated: true, completion: nil)
}
func itemsList(){
database.collection("catalog")
.getDocuments { (snapshot, error) in
self.items.removeAll()
if let snapshot {
for document in snapshot.documents{
let docData = document.data()
let title: String = docData["title"] as? String ?? ""
let imageUrl: String = docData["imageUrl"] as? String ?? ""
let description: String = docData["description"] as? String ?? ""
let price: Int = docData["price"] as? Int ?? 0
let item: Item = Item(title: title, price: price, description: description, imageUrl: imageUrl)
self.items.append(item)
}
}
}
}
}
`
I am creating an application. I make a request to the firebase store, after that I add the result to the array, but in the end, when the array is displayed from viewDidLoad or other functions, I get an empty array. But if you make a conclusion immediately after the request, then everything is displayed correctly
Getting data from Firebase is asynchronous process. So here you should make everything after loading data in closure database.collection("catalog").getDocuments {...}.
func itemsList(){
database.collection("catalog")
.getDocuments { (snapshot, error) in
self.items.removeAll()
if let snapshot {
for document in snapshot.documents{
let docData = document.data()
let title: String = docData["title"] as? String ?? ""
let imageUrl: String = docData["imageUrl"] as? String ?? ""
let description: String = docData["description"] as? String ?? ""
let price: Int = docData["price"] as? Int ?? 0
let item: Item = Item(title: title, price: price, description: description, imageUrl: imageUrl)
self.items.append(item)
}
}
print(self.items) //print items to see them
//here use items data
}
}
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 have a project in swift with Firestore for the database. My firestore dataset of a user looks like this. User details with an array that contains objects.
I have a function that gets the specifick user with all firestore data:
func fetchUser(){
db.collection("users").document(currentUser!.uid)
.getDocument { (snapshot, error ) in
do {
if let document = snapshot {
let id = document.documentID
let firstName = document.get("firstName") as? String ?? ""
let secondName = document.get("secondName") as? String ?? ""
let imageUrl = document.get("imageUrl") as? String ?? ""
let joinedDate = document.get("joinedDate") as? String ?? ""
let coins = document.get("coins") as? Int ?? 0
let challenges = document.get("activeChallenges") as? [Challenge] ?? []
let imageLink = URL(string: imageUrl)
let imageData = try? Data(contentsOf: imageLink!)
let image = UIImage(data: imageData!) as UIImage?
let arrayWithNoOptionals = document.get("activeChallenges").flatMap { $0 }
print("array without opt", arrayWithNoOptionals)
self.user = Account(id: id, firstName: firstName, secondName: secondName, email: "", password: "", profileImage: image ?? UIImage(), joinedDate: joinedDate, coins: coins, activeChallenges: challenges)
}
else {
print("Document does not exist")
}
}
catch {
fatalError()
}
}
}
This is what the user model looks like:
class Account {
var id: String?
var firstName: String?
var secondName: String?
var email: String?
var password: String?
var profileImage: UIImage?
var coins: Int?
var joinedDate: String?
var activeChallenges: [Challenge]?
init(id: String, firstName: String,secondName: String, email: String, password: String, profileImage: UIImage, joinedDate: String, coins: Int, activeChallenges: [Challenge]) {
self.id = id
self.firstName = firstName
self.secondName = secondName
self.email = email
self.password = password
self.profileImage = profileImage
self.joinedDate = joinedDate
self.coins = coins
self.activeChallenges = activeChallenges
}
init() {
}
}
The problem is I don't understand how to map the activeChallenges from firestore to the array of the model. When I try : let challenges = document.get("activeChallenges") as? [Challenge] ?? []
The print contains an empty array, but when i do: let arrayWithNoOptionals = document.get("activeChallenges").flatMap { $0 } print("array without opt", arrayWithNoOptionals)
This is the output of the flatmap:
it returns an optional array
System can not know that activeChallenges is array of Challenge object. So, you need to cast it to key-value type (Dictionary) first, then map it to Challenge object
let challengesDict = document.get("activeChallenges") as? [Dictionary<String: Any>] ?? [[:]]
let challenges = challengesDict.map { challengeDict in
let challenge = Challenge()
challenge.challengeId = challengeDict["challengeId"] as? String
...
return challenge
}
This is the same way that you cast snapshot(document) to Account object
i'm running a query to firebase and i'm able to obtain the documentID but i'm not able to send the documentID over to another VC. I get everything int he query with no issues. I'm not sure what i'm missing. Any suggestions are greatly appreciated.
First VC
#IBAction func getDataTapped(_ sender: Any) {
SVProgressHUD.show()
if HOSP != (hospNameTxt.text!) {
ptListQuery = ptListCollectionRef?.whereField("hosp", isEqualTo: (hospNameTxt.text!))
}
ptListQuery?.getDocuments { (snapshot, error) in
if let err = error {
debugPrint("error getting data: \(err)")
} else {
guard let snap = snapshot else { return }
for document in snap.documents {
let data = document.data()
let ptName = data[PTNAME] as? String ?? ""
let assignedMd = data[ASSIGNEDMD] as? String ?? ""
let officeMd = data[OFFICEMD] as? String ?? ""
let assignedDate = data[ASSIGNEDDATE] as? String ?? ""
let seeNoSee = data[SEENOSEE] as? String ?? ""
let room = data[ROOM] as? String ?? ""
let app = data[APP] as? String ?? ""
let documentId = document.documentID
let username = data[USERNAME] as? String ?? ""
let userId = data[USER_ID] as? String ?? ""
let newPtList = PTList(ptName: ptName,
assignedMd: assignedMd,
officeMd: officeMd,
assignedDate: assignedDate,
seeNoSee: seeNoSee,
room: room,
app: app, documentId: documentId,
username: username, userId: userId
)
self.ptListInCell.append(newPtList)
print("docID", documentId) //this shows the documentID in the console
print(document.data()) //this shows everything in the array but the document ID
}
}
self.performSegue(withIdentifier: "goToResults", sender: self)
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "goToResults" {
let vc = segue.destination as! ResultsdataVC
vc.ptListFromCell = ptListInCell
SVProgressHUD.dismiss()
}
}
Second VC
class ResultsdataVC: UIViewController, UITableViewDataSource, UITableViewDelegate, PatListCellDelegate {
#IBOutlet weak var resultsTableView: UITableView!
#IBOutlet weak var patientFilter: UISegmentedControl!
var ptListFromCell = [PTList]()
var ptDatasToPass = [PTData]()
var selectedFilter = FilterCategory.hosp.rawValue
var patientListener: ListenerRegistration!
var patientCollectionRef: CollectionReference!
//segmentedControl
var dataFilter = 0
var tableDataSee : [String] = ["Yes"]
var tableDataNoSee : [String] = ["No"]
override func viewDidLoad() {
super.viewDidLoad()
resultsTableView.delegate = self
resultsTableView.dataSource = self
resultsTableView.rowHeight = 110
}
here is PTList model:
class PTList {
private(set) var ptName: String!
private(set) var assignedMd: String!
private(set) var officeMd: String!
private(set) var assignedDate: String!
private(set) var seeNoSee: String!
private(set) var room: String!
private(set) var app: String!
private(set) var documentId: String!
private(set) var username: String!
private(set) var userId: String!
init(ptName: String, assignedMd: String, officeMd: String, assignedDate: String, seeNoSee: String, room: String, app: String, documentId: String, username: String, userId: String) {
self.ptName = ptName
self.assignedMd = assignedMd
self.officeMd = officeMd
self.assignedDate = assignedDate
self.seeNoSee = seeNoSee
self.room = room
self.app = app
self.documentId = documentId
self.username = username
self.userId = userId
}
}
here is my db structure
struct UserClass {
var babyName: String!
var babyHeight: String!
var babyWeight: String!
var babyURL: String!
var uid: String!
var reference:DatabaseReference!
var key: String!
init?(snapshot: DataSnapshot?) {
guard let value = snapshot?.value as? [String:AnyObject],
let uid = value["uid"] as? String,
let babyName = value["BabyName"] as? String,
let babyURL = value["BabyURL"] as? String,
let babyHeight = value["BabyHeight"] as? String,
let babyWeight = value["BabyWeight"] as? String else {
return nil
}
self.key = snapshot?.key
self.reference = snapshot?.ref
self.uid = uid
self.babyURL = babyURL
self.babyName = babyName
self.babyHeight = babyHeight
self.babyWeight = babyWeight
}
func getuserData() -> String {
return ("BabyName = \(babyName)")
}
}
func fetchCurrentUserInfo() {
var currentUserRef = Database.database().reference().child("Users").child("\(userID)")
handler = currentUserRef.queryOrderedByKey().observe(DataEventType.value, with: { (snapshot) in
print("User data = \(snapshot.value)")
let user = UserClass(snapshot: snapshot)
print(user?.babyName)
self.babyName.text = user?.babyName
})
}
I am getting user data but not user.babyName. How can I fix this?
May be this will help you, as the db structure is not mentioned in question. but you have to iterate children one by one and then use for loop to fetch the exact data from firebase.
reference = FIRDatabase.database().reference()
reference.child("Users").queryOrderedByKey().observe(DataEventType.value, with: { (snapshot) in
if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
for snap in snapshots
{
let userId = child.childSnapshot(forPath: "userID").value! as! String
print(userId)
}
}
})
So, my app crash because it forces to unwrap the "photo" even if it is optional and it has nothing inside. How do I use the if let statement, like if the photo has a picture then it will show, and if none it will be nil but the app wont crash.
I have this struct User this is where my data is saved I am using firebase.
struct User {
var fullName: String!
var username: String?
var email: String!
var country: String?
var photoURL: String?
var biography: String?
var uid: String!
var ref: FIRDatabaseReference?
var key: String?
init(snapshot: FIRDataSnapshot) {
key = snapshot.key
ref = snapshot.ref
fullName = (snapshot.value! as! NSDictionary) ["fullName"] as! String
email = (snapshot.value! as! NSDictionary) ["email"] as! String
uid = (snapshot.value! as! NSDictionary) ["uid"] as! String
country = (snapshot.value! as! NSDictionary) ["country"] as! String?
biography = (snapshot.value! as! NSDictionary) ["biography"] as! String?
photoURL = (snapshot.value! as! NSDictionary) ["photoURL"] as! String?
username = (snapshot.value! as! NSDictionary) ["username"] as! String?
}
}
this is where the app crashes, because of the "self.storageRef.reference(forURL:imageURL!)" it forces to unwrap even it has nothing inside.
func loadUserInfo() {
#IBOutlet weak var userImageView: UIImageView!
#IBOutlet weak var addButton: UIButton!
#IBOutlet weak var fullName: UILabel!
#IBOutlet weak var username: UILabel!
#IBOutlet weak var country: UILabel!
#IBOutlet weak var biography: UILabel!
let userRef = dataBaseRef.child("users/\(FIRAuth.auth()!.currentUser!.uid)")
userRef.observe(.value, with: { (snapshot) in
let user = User(snapshot: snapshot)
self.username.text = user.username
self.country.text = user.country
self.biography.text = user.biography
self.fullName.text = user.fullName
var imageURL = user.photoURL
self.storageRef.reference(forURL:imageURL!).data(withMaxSize: 1 * 1024 * 1024, completion: { (imgData, error) in
if error == nil {
DispatchQueue.main.async {
if let data = imgData {
self.userImageView?.image = UIImage(data: data)
}
}
} else {
print(error!.localizedDescription)
}
})
})
{ (error) in
print(error.localizedDescription)
}
}
first I think you should change the init of the User, you should do:
let data = snapshot.value as! NSDictionary;
fullName = data["fullName"] as! String;
if you not sure the country whether exist, you could do:
country = data["country"] as? String;
then you could use let to keep save when you use the use.photoURL, just like:
if let photoURL = user.photoURL {
//...retrive photo of the url from somewhere
}
finally, I wanna say, you maybe make a mistake understand about the ? and !, or confusion with them.
? is when you think this variable/func maybe nil/can not be called, or when you make a type conversion, but you don't sure that must be success.
! is that you are sure it's exist or you will create it immediatelly. also you could comprehend it as unwrap the optional value, just cause of you make it absolute exist.
when we create our own model just like your User, you could make the columns impossible nil, you can do:
var fullName: String? = nil;
fullName = SomeJsonTypeData["fullName"] ?? "default value";
then when you use it, you dispense with any worry about it will be nil.
If just focusing on the photoURL issue, I think this may help you:
if let imageURL = user.photoURL {
self.storageRef.reference(forURL: imageURL).data(withMaxSize: 1 * 1024 * 1024, completion: { (imgData, error) in
if error == nil {
DispatchQueue.main.async {
if let data = imgData {
self.userImageView?.image = UIImage(data: data)
}
}
} else {
print(error!.localizedDescription)
}
})
}