How to download/map date from Firestore into a Swift Collection View - swift

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

How to grab the current users "firstname" from firebase store. Swift 5

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.

Unable to access certain Firestore methods but exact code has no problem in separate project

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.

How to map fields from Firestore documents in Swift

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

How can I delete a specific Cloud Firestore document using the documentID in SwiftUI?

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!")
}
}
}

Firestore - Subcollections Swift

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.