I am able to download the data & see it in the Xcode debug console when I print ("(products)" after completion(true) but when I try to use the products variable in the View Controller & print it's contents there I get an empty array []. How do I use the data after in a collection view after it is downloaded?
Model
import Foundation
import UIKit
import FirebaseFirestoreSwift
public struct StoreProducts: Codable {
#DocumentID var id: String?
var orderNumber: Int?
var country: String?
var description: String?
var price: Int?
var duration: String?
enum CodingKeys: String, CodingKey {
case orderNumber
case country
case description
case price
case duration
}
}
Model Class
import Foundation
import FirebaseFirestore
class StoreViewModel: ObservableObject {
public static let shared = StoreViewModel()
private let productsCollection: String = "products/country/subscription"
#Published var products : [StoreProducts]?
private var db = Firestore.firestore()
public func fetchProductData(completion: #escaping (Bool) -> Void) {
db.collection(canadianProductsCollection).getDocuments() { [self] (querySnapshot, err) in
//Handle Error:
if let err = err {
print("Error getting documents: \(err)")
completion(false)
} else {
//No Documents Found:
guard let documents = querySnapshot?.documents else {
print("no documents")
completion(false)
return
}
//Documents Found:
let products = documents.compactMap { document -> StoreProducts? in
return try! document.data(as: StoreProducts.self)
}
completion(true)
print ("\(products)")
}
}
}
}
View Controller
import Firebase
import FirebaseDatabase
class HomeViewController: UIViewController {
#ObservedObject private var storeViewModel = StoreViewModel()
override func viewWillAppear(_ animated: Bool) {
StoreViewModel.shared.fetchProductData(completion: { success in
if success {
print("Data loaded successfully")
print (storeViewModel.products)
} else {
//some break routine
}
})
}
}
You are storing the results from the database to a local variable and it is not passed on to your storeViewModel.
products = documents.compactMap { document -> StoreProducts? in
return try! document.data(as: StoreProducts.self)
}
I think removing the "let" might solve the problem.
Related
I did more trial and error and a bit of online research and this is what I came back with:
func presentWelcomeMessage() {
//Get specific document from current user
let docRef = Firestore.firestore()
.collection("users")
.whereField("uid", isEqualTo: Auth.auth().currentUser?.uid ?? "")
// Get data
docRef.getDocuments { (querySnapshot, err) in
if let err = err {
print(err.localizedDescription)
} else if querySnapshot!.documents.count != 1 {
print("More than one document or none")
} else {
let document = querySnapshot!.documents.first
let dataDescription = document?.data()
guard let firstname = dataDescription?["firstname"] else { return }
self.welcomeLabel.text = "Hey, \(firstname) welcome!"
}
}
It works, but am not sure if it is the most optimal solution.
First I should say firstname is not really the best way to store a var. I would recommend using firstName instead for readability. I also recommend getting single documents like I am, rather than using a whereField.
An important thing to note is you should create a data model like I have that can hold all of the information you get.
Here is a full structure of how I would get the data, display it, and hold it.
struct UserModel: Identifiable, Codable {
var id: String
var firstName: String
private enum CodingKeys: String, CodingKey {
case id
case firstName
}
}
import SwiftUI
import FirebaseAuth
import FirebaseFirestore
import FirebaseFirestoreSwift
class UserDataManager: ObservableObject {
private lazy var authRef = Auth.auth()
private lazy var userInfoCollection = Firestore.firestore().collection("users")
public func getCurrentUIDData(completion: #escaping (_ currentUserData: UserModel) -> Void) {
if let currentUID = self.authRef.currentUser?.uid {
self.userInfoCollection.document(currentUID).getDocument { (document, error) in
if let document = document {
if let userData = try? document.data(as: UserModel.self) {
completion(userData)
}
} else if let error = error {
print("Error getting current UID data: \(error)")
}
}
} else {
print("No current UID")
}
}
}
struct ContentView: View {
#State private var userData: UserModel? = nil
private let
var body: some View {
ZStack {
if let userData = self.userData { <-- safely unwrap data
Text("Hey, \(userData.firstName) welcome!")
}
}
.onAppear {
if self.userData == nil { <-- onAppear can call more than once
self.udm.getCurrentUIDData { userData in
self.userData = userData <-- pass data from func to view
}
}
}
}
}
Hopefully this can point you in a better direction of how you should be getting and displaying data. Let me know if you have any further questions or issues.
Originally wrote my Firebase/Firestore code in a separate project and am now beginning to manually integrate that code into the main tree. The exact code snippet throws no errors in separate project but does in the main:
import Foundation
import Firebase
import FirebaseFirestore
struct Title: Codable, Equatable {
var id: Int
var type: String
var title: String
var overview: String?
var imagePath: String?
}
class Titles: ObservableObject {
#Published var content: [Title]
#Published var title: Title = Title(id: -999, type: "init", title: "...")
private var db = Firestore.firestore()
private var listenerRegistration: ListenerRegistration?
init() {
self.content = []
}
deinit {
unsubscribe()
}
func unsubscribe() {
if listenerRegistration != nil {
listenerRegistration?.remove()
listenerRegistration = nil
}
}
func subscribe(_ uid: String) {
if listenerRegistration == nil {
listenerRegistration = db.collection("Lib").document(uid).collection("userLib").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.content = documents.compactMap { queryDocumentSnapshot in
// throwing: Argument passed to call that takes no arguments
// & Cannot convert value of type '()' to closure result type 'Title?'
try? queryDocumentSnapshot.data(as: Title.self)
}
}
}
}
func writeLibrary(_ uid: String) {
let titleDoc = db.collection("Lib").document(uid).collection("userLib").document(UUID().uuidString)
do {
// throwing: No exact matches in call to instance method 'setData
try titleDoc.setData(from: title.self)
print("writeLibrary() created document")
} catch {
print("Error writing from writeLirbary() \(error)")
}
}
}
As noted with the comments in above, the errors are thrown at the try? data(as: ) and try setData(from: ) methods.
Not sure if there is an issue with how Swift Package Manager imported dependancies or if I am missing an import. For example, under the imported package in Firebase master/Firestore/Swift/Source/Codable/ the DocumentSnapsot+ReadDecodable.swift is there but for some reason isn't imported.
Any community perspectives on this would be helpful.
In my Firestore database, I have "favorites" getting stored like this:
How can I get the values "S1533" and "S2017" based on itemActive = true?
Here is the Swift code I have, but I am stuck on how to look at itemActive and then go back and return the values that have that field as set to true.
db.collection("users").document(userId!).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
}
print(data["favorites"])
}
The easiest way to map Firestore documents is to use Codable. This article to learn about the basics.
For your model, the following code should get you started:
Model
struct Favourite: Codable, Identifiable {
var itemActive: Bool
var itemAdded: Date
}
struct UserPreference: Codable, Identifiable {
#DocumentID public var id: String?
var displayName: String
var email: String
var favourites: [Favourite]?
}
Fetching data
public class UserPreferenceRepository: ObservableObject {
var db = Firestore.fireStore()
#Published var userPreferences = [UserPreference]()
private var listenerRegistration: ListenerRegistration?
public func subscribe() {
if listenerRegistration == nil {
var query = db.collection("users")
listenerRegistration =
query.addSnapshotListener { [weak self] (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
self?.logger.debug("No documents")
return
}
self?.userPreferences = documents.compactMap { queryDocumentSnapshot in
try? queryDocumentSnapshot.data(as: UserPreference.self)
}
}
}
}
I'm using a list to display injury data that the user has inputted via a form, that is successfully added to Cloud Firestore. I now want to add a delete function that deletes the injury selected in the list.
Here is my Injury Struct:
import SwiftUI
import FirebaseFirestoreSwift
struct Injury: Identifiable, Codable {
#DocumentID var id: String? = UUID().uuidString
var userId: String?
var specificLocation: String
var comment: String
var active: Bool
var injuryDate: Date
var exercises: String
var activity: String
var location: String
}
My InjuriesViewModel:
import SwiftUI
import Firebase
import FirebaseFirestore
import FirebaseFirestoreSwift
class InjuriesViewModel: ObservableObject {
#Published var injuries = [Injury]()
private var db = Firestore.firestore()
func fetchData () {
let userId = Auth.auth().currentUser?.uid
db.collection("injuries")
.order(by: "injuryDate", descending: true)
.whereField("userId", isEqualTo: userId)
.addSnapshotListener{ (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("no documents")
return
}
self.injuries = documents.compactMap { (queryDocumentSnapshot) -> Injury? in
return try? queryDocumentSnapshot.data(as: Injury.self)
}
}
}
}
My InjuryViewModel (here is where the add and delete injury functions are, however I'm not sure how to fill in the document field):
import SwiftUI
import Firebase
class InjuryViewModel: ObservableObject {
#Published var injury: Injury = Injury(id: "", userId: "", specificLocation: "", comment:
"", active: false, injuryDate: Date(), exercises: "", activity: "", location: "")
private var db = Firestore.firestore()
func addInjury(injury: Injury) {
do {
var addedInjury = injury
addedInjury.userId = Auth.auth().currentUser?.uid
let _ = try db .collection("injuries").addDocument(from: addedInjury)
}
catch {
print(error)
}
}
func deleteInjury(injury: Injury) {
db.collection("injury").document(??).delete() { err in
if let err = err {
print("Error removing document: \(err)")
}
else {
print("Document successfully removed!")
}
}
}
func save () {
addInjury(injury: injury)
}
func delete () {
deleteInjury(injury: injury)
}
}
Thanks in advance for your help!
Here's where I'm at:
func addInjury(injury: Injury) {
do {
var addedInjury = injury
addedInjury.userId = Auth.auth().currentUser?.uid
let documentRef = try db.collection("injuries").addDocument(from: addedInjury)
addedInjury.id = documentRef.documentID
print(documentRef.documentID)
}
catch {
print(error)
}
}
func deleteInjury(injury: Injury) {
db.collection("injuries").document(injury.id!).delete() { err in
if let err = err {
print("Error removing document: \(err)")
}
else {
print("Document successfully removed!")
}
}
}
In deleteInjury, you just need to access the documentID of the current Injury that your view model holds:
func deleteInjury(injury: Injury) {
db.collection("injury").document(injury.id).delete() { err in
if let err = err {
print("Error removing document: \(err)")
}
else {
print("Document successfully removed!")
}
}
}
So I'm trying to learn some Firestore basic functionality and have watched "Kilo Locos" videos on YouTube explaining CRUD operations. I want to take his method of code and create subcollections from it. Basically, how can I add a collection and make the 'User' collection a sub collection from this new collection. Any help is greatly appreciated, many thanks!!
Here is a link to download the project:
https://kiloloco.com/courses/youtube/lectures/3944217
FireStore Service
import Foundation
import Firebase
import FirebaseFirestore
class FIRFirestoreService {
private init() {}
static let shared = FIRFirestoreService()
func configure() {
FirebaseApp.configure()
}
private func reference(to collectionReference: FIRCollectionReference) -> CollectionReference {
return Firestore.firestore().collection(collectionReference.rawValue)
}
func create<T: Encodable>(for encodableObject: T, in collectionReference: FIRCollectionReference) {
do {
let json = try encodableObject.toJson(excluding: ["id"])
reference(to: collectionReference).addDocument(data: json)
} catch {
print(error)
}
}
func read<T: Decodable>(from collectionReference: FIRCollectionReference, returning objectType: T.Type, completion: #escaping ([T]) -> Void) {
reference(to: collectionReference).addSnapshotListener { (snapshot, _) in
guard let snapshot = snapshot else { return }
do {
var objects = [T]()
for document in snapshot.documents {
let object = try document.decode(as: objectType.self)
objects.append(object)
}
completion(objects)
} catch {
print(error)
}
}
}
func update<T: Encodable & Identifiable>(for encodableObject: T, in collectionReference: FIRCollectionReference) {
do {
let json = try encodableObject.toJson(excluding: ["id"])
guard let id = encodableObject.id else { throw MyError.encodingError }
reference(to: collectionReference).document(id).setData(json)
} catch {
print(error)
}
}
func delete<T: Identifiable>(_ identifiableObject: T, in collectionReference: FIRCollectionReference) {
do {
guard let id = identifiableObject.id else { throw MyError.encodingError }
reference(to: collectionReference).document(id).delete()
} catch {
print(error)
}
}
}
FIRCollectionReference
import Foundation
enum FIRCollectionReference: String {
case users
}
User
import Foundation
protocol Identifiable {
var id: String? { get set }
}
struct User: Codable, Identifiable {
var id: String? = nil
let name: String
let details: String
init(name: String, details: String) {
self.name = name
self.details = details
}
}
Encodable Extensions
import Foundation
enum MyError: Error {
case encodingError
}
extension Encodable {
func toJson(excluding keys: [String] = [String]()) throws -> [String: Any] {
let objectData = try JSONEncoder().encode(self)
let jsonObject = try JSONSerialization.jsonObject(with: objectData, options: [])
guard var json = jsonObject as? [String: Any] else { throw MyError.encodingError }
for key in keys {
json[key] = nil
}
return json
}
}
Snapshot Extensions
import Foundation
import FirebaseFirestore
extension DocumentSnapshot {
func decode<T: Decodable>(as objectType: T.Type, includingId: Bool = true) throws -> T {
var documentJson = data()
if includingId {
documentJson!["id"] = documentID
}
let documentData = try JSONSerialization.data(withJSONObject: documentJson!, options: [])
let decodedObject = try JSONDecoder().decode(objectType, from: documentData)
return decodedObject
}
}
The Firestore structure cannot have collection as children of other collections.
The answer to your question (How can I add a collection and make the 'User' collection a sub collection from this new collection?) is you cannot. Instead you must put a document between those two collections.
Read this for more information.
It says: Notice the alternating pattern of collections and documents. Your collections and documents must always follow this pattern. You cannot reference a collection in a collection or a document in a document.