Firestore in SwiftUI
Firestore makes use of collections as storage structure. Each collection has one or more documents which are made up of attributes that contain the actual data.

Structure of firestore storage

Structure with examples
Similarly, firestore can store many collections.
References
A reference can be created for collection as well as document. Creating a reference does not perform any network operation, it only point to a location in the database.
For e.g reference to a collection
let mealCollectionRef = db.collection("Meal")
For e.g reference to a document
let mealOneDocumentRef = db.collection("Meal").document("Meal 1")
Note: A document can have a collection too. For instance, a review needs data from the reviewer and the review itself.
-
Meals
-
Meal one
-
title : "Burgers" -
reviews
- review one
from : "alex"review : "Good"- review two
from : "tom"review : "bad"
-
-
Meal Two
-
In the above, we have reviews as a sub-collection of document “Meal one” since reviews contain large number of data.
Referencing a sub-collection
let reviewRef = db.collection("Meals").document("Meal one").collection("reviews").document("review one")
Note: sub-collections need to be deleted manually, even if the parent document and collection are deleted
Create references
Document reference with generated id
var docRef: DocumentReference? = nil
Collection Reference with generated id
var colRef: CollectionReference? = nil
Read Documents
Multiple documents
To fetch multiple documents in a collection, we use getDocuments().
getDocuments() returns documents in querySnapshot as a list.
querySnapshots contain several documents, from which we can extract several or one document.
db.collection("meals").getDocuments() { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
let documents = querySnapshot!.documents
print("\(documents)")
}
}
// OUTPUT
//[,
// ,
// ,
// ,
// ,
// ,
// ,
// ,
// ,
//]
Single document
To fetch a single document, we loop through the documents obtained from querySnapshots .documents.
db.collection("meals").getDocuments() { (querySnapshot, error) in
if let error = error {
print(" Cannot fetch documents")
} else {
for document in querySnapshots!.documents {
print("\(document.data())")
}//for
}//else
}//getDocument
Get data
Fetching data from a single document using .data from document
db.collection("meals").getDocuments() { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
let documents = querySnapshot!.documents
print("Documents list: \(documents)")
for document in documents {
print("Data from document: \(document.data())")
}
print("\(documents)")
}//else
}//getDocuments()
// OUTPUT
//Data from document: ["title": Mac and cheese, "price": 125, "quantity": 4]
//Data from document: ["title": burger, "price": 125.5, "quantity": 2]
//Data from document: ["title": Mac and cheese, "price": 125, "quantity": 4]
//Data from document: ["title": Mac and cheese, "price": 125, "quantity": 4]
//Data from document: ["title": Pasta, "price": 467, "quantity": 6]
//Data from document: ["title": Mac and cheese, "price": 125, "quantity": 4]
//Data from document: ["title": Mac and cheese, "price": 125, "quantity": 4]
//Data from document: ["title": Pizza, "price": 400, "quantity": 2]
//Data from document: ["title": Mac and cheese, "price": 125, "quantity": 4]
//Data from document: ["title": Mac and cheese, "price": 125, "quantity": 4]
Extracting single attributes
db.collection("meals").getDocuments() { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
let documents = querySnapshot!.documents
//print("Documents list: \(documents)")
for document in documents {
//print("Data from document: \(document.data())")
let data = document.data()
let title = data["title"]
let price = data["price"]
let quantity = data["quantity"]
print("title: \(title ?? "No title")")
print("price: \(price ?? "No price")")
print("quantity: \(quantity ?? "No quantity")")
}//for
}//else
}//getDocuments
// OUTPUT
//title: Mac and cheese
//price: 125
//quantity: 4
//title: burger
//price: 125.5
//quantity: 2
//title: Pizza
//price: 400
//quantity: 2
Add data
Adding data with specific document id
let data: [String: Any] = [
"title": "Grilled lobster",
"price": 30,
"quantity": 2,
"type": "Dinner"
]
func pushData() {
db.collection("meals").document("five").setData(data) { err in
if let err = err {
print("Error writing to document: \(err)")
} else {
print("Data successfully written to doc")
}//else
}//setData
}//Push data
Adding data with auto-generated document id
docRef = db.collection("meals").addDocument(data: data) { err in
if let err = err {
print("Error adding document: \(err)")
} else {
print("Document \(self.docRef!.documentID) added").
}
}
Adding data from classes/struct
public struct Place: Codable {
let name: String
let isCapital: Bool?
let population: Int?
enum CodingKeys: String, CodingKey {
case name
case isCapital = "capital"
case population
}//enum
}//struct
// In ObservableObject class
let places = Place(name: "China", isCapital: false, population: 1449323776)
do {
try db.collection("Places").document("Asia").setData(from: places) } catch let error {
print("Error writing ro firestore \(error)")
}
Note: You may get an error “Extraneous argument label 'from:' in call”. This is because firestore makes use of an extension (FirebaseFirestoreSwift) to be able to save custom objects such as struct.
Fix: import FirebaseFirestoreSwift.
In case this is missing from your imports, just go to Targets → Frameworks, Libraries, and Embedded Content. Click on +, search for the dependency: “FirestoreFirebaseSwift” and click on add

Adding dependency in targets

FirebaseFirestoreSwift dependency
If using pods:
pod 'FirebaseFirestoreSwift'
Updating data
db.collection("Places").document("Asia").updateData(["name": "Australia", "capital":false]) { error in
if let error = error {
print("Error updating document: \(error)")
} else {
print("Document successfully updated")
}
}
Update data using a timeStamp
db.collection("Places").document("Asia").updateData(["lastUpdated":FieldValue.serverTimestamp()]) { error in
if let error = error {
print("Error updating document: \(error)")
} else {
print("Document successfully updated")
}
}
Update element in an array
db.collection("Places").document("Asia").updateData(["Ocean": FieldValue.arrayUnion(["Pacific"]) { error in
if let error = error {
print("Error updating document: \(error)")
} else {
print("Document successfully updated")
}
}
Incrementing number value
Passing no argument will increase the value by 1
db.collection("Places").document("Asia").updateData(["population": FieldValue.increment(Int64(10))]) { error in
if let error = error {
print("Error updating document: \(error)")
} else {
print("Document successfully updated")
}
}
Note: FieldValue.increment works only on Int64 or Double
Deleting a field
To mark a field for deletion we use updateData
db.collection("Places").document("Asia").updateData(["population": FieldValue.delete()]) { error in
if let error = error {
print("Error updating document: \(error)")
} else {
print("Document successfully updated")
}
}
Delete a document
db.collection("Places").document("Asia").delete() { err in
if let err = err {
print("Error removing document: \(err)")
} else {
print("Document successfully removed!")
}
}
Example
Step 1: Initialize firestore
import FirebaseCore
import FirebaseFirestore
FirebaseApp.configure()
let db = Firestore.firestore()
Step 2: Create a model
import Foundation
struct Meal: Identifiable {
var id = UUID()
var title: String
var price: Double
var quantity: Int
}
Step 3: Create a view
struct MealView: View {
@StateObject var viewModel = MealsViewModel()
var body: some View {
List(viewModel.meals) { meal in
VStack(alignment: .leading) {
Text(meal.title)
.font(.headline)
Text("\(meal.price)")
.font(.subheadline)
Text("\(meal.quantity)")
.font(.footnote)
}//VStack
}//List
.navigationTitle("Meals")
.onAppear() {
self.viewModel.fetchData()
}
}//body
}//MealView
Step 4: Create a viewModel
import Foundation
import FirebaseFirestore
class MealsViewModel: ObservableObject {
@Published var meals = [Meal]()
private var db = FirebaseManager.shared.firestore
private var docRef : DocumentReference? = nil
func fetchData() {
db.collection("meals").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.meals = documents.map {
queryDocumentSnapshot -> Meal in
let data = queryDocumentSnapshot.data()
let title = data["title"] as? String ?? ""
let price = data["price"] as? Double ?? 0.0
let quantity = data["quantity"] as? Int ?? 0
return Meal(id: .init(), title: title, price: price, quantity: quantity)
}
}
}// Fetch data
func pushData() {
docRef = db.collection("meals").addDocument(data: ["title": "Mac and cheese", "price": 125, "quantity": 4]) { err in
if let err = err {
print("Error adding document: \(err)")
} else {
print("Document \(self.docRef!.documentID) added")
}
}
}//Push data
}
The meals variable is wrapped with the @Published property. We use @Published property wrapper for all variable we want our view(UI) to listen to.
To be able to use the @Published property wrapper, the class must conform to the ObservableObject protocol.