Storing and Retrieving Data Using Cloud Firestore
Course: iOS App Development
Topic: Storing and Retrieving Data Using Cloud Firestore
Course Link: iOS Training at JustAcademy
Demo Registration: Register for a Free Course Demo
JustAcademy Website: Visit JustAcademy
1. Introduction to Cloud Firestore
Cloud Firestore is a flexible, scalable, cloud-hosted NoSQL database provided by Firebase. It allows iOS applications to store, retrieve, update, and delete data without requiring developers to build and maintain their own database server.
In an iOS application, Firestore can be used to manage user profiles, notes, product information, chat messages, task lists, booking records, and other application data. The Firebase SDK provides Swift APIs for communicating with Firestore.
Firestore supports real-time listeners, queries, offline caching, and integration with Firebase Authentication and Firebase Security Rules. These capabilities make it useful for applications that need synchronized data across multiple devices.
2. Learning Objectives
- Understand the structure and purpose of Cloud Firestore.
- Learn the difference between collections and documents.
- Configure Firebase and Firestore in an iOS project.
- Store data from Swift and SwiftUI applications.
- Retrieve individual documents and collections.
- Perform queries using filters, sorting, and limits.
- Update and delete stored documents.
- Listen for real-time changes in database data.
- Handle errors and understand offline behavior.
- Protect application data using Firebase Security Rules.
3. What Is Cloud Firestore?
Cloud Firestore stores information in documents organized into collections. A document contains fields and values, such as strings, numbers, Boolean values, timestamps, arrays, maps, and references to other documents.
Unlike a traditional relational database, Firestore does not require tables with a fixed schema or SQL joins. Applications organize information into collections and documents and retrieve data using Firestore queries.
Key Features
| Feature |
Description |
| NoSQL data model |
Stores data in collections and documents instead of relational tables. |
| Real-time listeners |
Notifies the application when observed documents change. |
| Offline support |
Can cache data locally and synchronize supported changes when connectivity returns. |
| Flexible queries |
Supports filtering, ordering, and limiting query results. |
| Scalability |
Managed cloud infrastructure supports growing application workloads. |
| Security Rules |
Controls which database operations users are authorized to perform. |
| Firebase integration |
Works with services such as Firebase Authentication and Cloud Functions. |
4. Understanding Collections and Documents
4.1 Collections
A collection is a group of documents. For example, a notes application might have a collection named notes that contains individual note documents.
notes
├── document_001
├── document_002
└── document_003
4.2 Documents
A document is a record containing fields and values. A note document might contain a title, description, creation timestamp, and owner identifier.
{
"title": "Learn Swift",
"description": "Practice asynchronous programming",
"isCompleted": false,
"ownerId": "USER_ID",
"createdAt": "Firestore Timestamp"
}
The example illustrates the document structure. In an actual Firestore database, the timestamp field should be stored as a Firestore timestamp value rather than as the literal text shown above.
4.3 Example Database Structure
users
└── USER_ID
├── name: "Aarav"
├── email: "[email protected]"
└── notes
├── NOTE_ID_1
│ ├── title: "Swift Basics"
│ └── isCompleted: false
└── NOTE_ID_2
├── title: "Firestore"
└── isCompleted: true
Here, each user has a document in the users collection, and the user's notes are stored in a subcollection named notes.
5. Prerequisites for Using Firestore in iOS
Before integrating Cloud Firestore, make sure you have:
- A Mac computer with a compatible version of Xcode.
- An iOS project created using Swift or SwiftUI.
- A Firebase project in the Firebase Console.
- The Firebase Apple-platform SDK added to the Xcode project.
- A registered iOS application with its bundle identifier.
- A Firestore database created in the Firebase Console.
- Firebase Security Rules appropriate for the application's data.
6. Setting Up Firebase and Cloud Firestore
Step 1: Create a Firebase Project
- Open the Firebase Console.
- Sign in with your Google account.
- Select the option to create a project.
- Enter a project name and follow the setup instructions.
- Enable Google Analytics if it is useful for your application.
Step 2: Register Your iOS App
- Open the Firebase project.
- Add an Apple or iOS application.
- Enter the bundle identifier from your Xcode project.
- Download the
GoogleService-Info.plist configuration file.
- Add the file to the appropriate target in Xcode and verify that it is included in the application target.
The bundle identifier entered in Firebase must match the bundle identifier used by the application.
Step 3: Add the Firebase SDK
For a new project, Swift Package Manager is the recommended installation method for the Firebase Apple-platform SDK. In Xcode, select File > Add Package Dependencies and enter the official Firebase Apple SDK repository:
https://github.com/firebase/firebase-ios-sdk
Select the Firebase products required by your application, including FirebaseFirestore. If you use Firebase Authentication, add the appropriate Authentication product as well.
Step 4: Initialize Firebase
For a SwiftUI application, Firebase is commonly initialized through an application delegate attached to the app lifecycle.
import SwiftUI
import FirebaseCore
class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
FirebaseApp.configure()
return true
}
}
@main
struct FirestoreDemoApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self)
var delegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
For a UIKit application, initialize Firebase in the application delegate's launch method before using Firebase services. Follow the current Firebase setup instructions if your application uses a different lifecycle or configuration.
Step 5: Create a Cloud Firestore Database
- Open the Firestore Database section in the Firebase Console.
- Select the option to create a database.
- Choose a database location appropriate for your application and users.
- Configure the database's initial security settings.
- Publish suitable Security Rules before releasing the application.
Do not leave production data accessible through unrestricted test rules.
7. Importing and Accessing Firestore in Swift
After adding the SDK and configuring Firebase, import the Firestore module and obtain a database instance.
import FirebaseFirestore
let db = Firestore.firestore()
Access the database only after Firebase has been configured. In a larger application, a repository or service object can manage Firestore access instead of repeatedly creating database references throughout the UI.
8. Storing Data in Cloud Firestore
Firestore provides methods for creating documents and writing field values. Common operations include addDocument(data:), setData(_:), and updateData(_:).
8.1 Add a Document with an Automatically Generated ID
Use addDocument(data:) when you want Firestore to generate a unique document ID.
import FirebaseFirestore
let db = Firestore.firestore()
func addNote() {
let note: [String: Any] = [
"title": "Learn Swift",
"description": "Practice Firestore operations",
"isCompleted": false,
"createdAt": FieldValue.serverTimestamp()
]
db.collection("notes").addDocument(data: note) {
error in
if let error = error {
print("Failed to save note: \(error.localizedDescription)")
} else {
print("Note saved successfully")
}
}
}
Explanation:
collection("notes") identifies the collection.
addDocument(data:) creates a document with an automatically generated ID.
FieldValue.serverTimestamp() requests a server-generated timestamp for the write.
- The completion handler reports whether the write succeeded or failed.
8.2 Store Data with a Specific Document ID
Use document("documentID").setData(...) when your application already knows the document ID, such as a user ID.
func saveUserProfile(userID: String) {
let profile: [String: Any] = [
"name": "Aarav",
"email": "[email protected]",
"updatedAt": FieldValue.serverTimestamp()
]
db.collection("users")
.document(userID)
.setData(profile) { error in
if let error = error {
print("Profile save failed: \(error.localizedDescription)")
} else {
print("Profile saved successfully")
}
}
}
If a document with that ID already exists, setData without merge options replaces its existing field data with the supplied data. Use merge options when you want to preserve other existing fields.
8.3 Set Data Without Replacing Existing Fields
func updateProfileName(userID: String) {
db.collection("users")
.document(userID)
.setData(
["name": "Aarav Sharma"],
merge: true
) { error in
if let error = error {
print("Update failed: \(error.localizedDescription)")
} else {
print("Profile name updated")
}
}
}
The merge: true option merges the provided fields into the existing document instead of replacing all its fields.
8.4 Store Strongly Typed Swift Data
For larger applications, representing data with a Swift structure makes the code easier to understand and maintain.
import Foundation
import FirebaseFirestore
struct Note: Identifiable, Codable {
@DocumentID var id: String?
var title: String
var description: String
var isCompleted: Bool
var createdAt: Date?
}
Firestore supports Codable-based serialization through its Apple SDK. The document ID property can be populated from the Firestore document ID during decoding. If your model uses a server timestamp, remember that the timestamp may not have resolved to a final value immediately after a local write.
9. Retrieving a Single Document
Use getDocument to fetch a particular document. This is suitable when you need a single read rather than a continuously updated result.
func fetchNote(noteID: String) {
db.collection("notes")
.document(noteID)
.getDocument { snapshot, error in
if let error = error {
print("Read failed: \(error.localizedDescription)")
return
}
guard let snapshot = snapshot,
snapshot.exists,
let data = snapshot.data() else {
print("Note does not exist")
return
}
let title = data["title"] as? String ?? "Untitled"
let description =
data["description"] as? String ?? ""
print("Title: \(title)")
print("Description: \(description)")
}
}
Important checks: handle request errors, verify that the document exists, and safely convert field values into the expected Swift types.
10. Retrieving All Documents from a Collection
Use getDocuments to retrieve the documents that match a collection query at the time of the request.
func fetchAllNotes() {
db.collection("notes")
.getDocuments { snapshot, error in
if let error = error {
print("Unable to retrieve notes: \(error.localizedDescription)")
return
}
guard let documents = snapshot?.documents else {
print("No documents returned")
return
}
for document in documents {
let data = document.data()
let title = data["title"] as? String ?? "Untitled"
print("\(document.documentID): \(title)")
}
}
}
This approach is useful for one-time reads. If the interface must remain synchronized with database changes, use a snapshot listener instead.
11. Retrieving Data with Codable
Using a model type instead of manually reading every field reduces repeated conversion code. The following example decodes one Firestore document into the Note model defined earlier.
func fetchTypedNote(noteID: String) {
db.collection("notes")
.document(noteID)
.getDocument { snapshot, error in
if let error = error {
print("Firestore error: \(error.localizedDescription)")
return
}
guard let snapshot = snapshot,
snapshot.exists else {
print("Note not found")
return
}
do {
let note = try snapshot.data(as: Note.self)
print("Note title: \(note.title)")
} catch {
print("Decoding failed: \(error.localizedDescription)")
}
}
}
Codable decoding may fail if stored field names, types, or required values do not match the Swift model. Keep the model and Firestore document schema consistent.
12. Querying Data with Filters
Firestore queries allow an application to retrieve only documents matching specified conditions.
12.1 Filter by a Boolean Field
func fetchCompletedNotes() {
db.collection("notes")
.whereField("isCompleted", isEqualTo: true)
.getDocuments { snapshot, error in
if let error = error {
print("Query failed: \(error.localizedDescription)")
return
}
let documents = snapshot?.documents ?? []
for document in documents {
print(document.data())
}
}
}
12.2 Filter by a String Field
func fetchNotesByTitle(_ title: String) {
db.collection("notes")
.whereField("title", isEqualTo: title)
.getDocuments { snapshot, error in
if let error = error {
print(error.localizedDescription)
return
}
for document in snapshot?.documents ?? [] {
print(document.documentID)
}
}
}
Equality queries match stored field values. Firestore does not automatically provide unrestricted full-text search; for advanced text search, consider a dedicated search service.
12.3 Order Results and Limit the Number of Documents
func fetchLatestNotes() {
db.collection("notes")
.order(by: "createdAt", descending: true)
.limit(to: 10)
.getDocuments { snapshot, error in
if let error = error {
print("Unable to load recent notes: \(error.localizedDescription)")
return
}
for document in snapshot?.documents ?? [] {
print(document.data())
}
}
}
Ordering by a field excludes documents that do not contain that field. Queries that combine certain filters and ordering may require a composite index. Firestore normally provides a console link when a required index is missing.
13. Updating Existing Data
Use updateData to modify specific fields in an existing document without replacing all its other fields.
func markNoteCompleted(noteID: String) {
db.collection("notes")
.document(noteID)
.updateData([
"isCompleted": true,
"updatedAt": FieldValue.serverTimestamp()
]) { error in
if let error = error {
print("Update failed: \(error.localizedDescription)")
} else {
print("Note marked as completed")
}
}
}
Unlike setData, updateData fails if the target document does not exist. Choose the method based on whether you intend to create or update the record.
14. Deleting Documents
Use delete() to remove a document.
func deleteNote(noteID: String) {
db.collection("notes")
.document(noteID)
.delete { error in
if let error = error {
print("Delete failed: \(error.localizedDescription)")
} else {
print("Note deleted successfully")
}
}
}
Deleting a Firestore document does not automatically delete documents in its subcollections. If a document has nested collections, design a separate cleanup process when required.
15. Listening for Real-Time Updates
A snapshot listener keeps the application informed about changes to a document or query. It is useful for chat applications, live dashboards, shared task lists, and collaborative applications.
import FirebaseFirestore
var notesListener: ListenerRegistration?
func listenToNotes() {
notesListener = db.collection("notes")
.addSnapshotListener { snapshot, error in
if let error = error {
print("Listener error: \(error.localizedDescription)")
return
}
guard let documents = snapshot?.documents else {
return
}
for document in documents {
let title = document.data()["title"] as? String ?? "Untitled"
print("Updated note: \(title)")
}
}
}
func stopListeningToNotes() {
notesListener?.remove()
notesListener = nil
}
The listener can emit an initial result and subsequent updates. Retain its registration and remove it when updates are no longer needed, such as when a screen or service is no longer active.
16. Understanding Offline Data and Synchronization
Cloud Firestore supports offline persistence on Apple platforms. The SDK can serve eligible cached data and queue supported local writes for synchronization when network connectivity returns.
- Online: the SDK communicates with the backend and receives current data subject to connectivity and permissions.
- Offline: reads may use cached documents; data that has never been cached may be unavailable.
- Pending writes: local changes can appear in the UI before the server confirms them.
- Reconnect: queued writes synchronize when connectivity returns, subject to security rules and write conflicts.
Offline persistence is not a substitute for a backup strategy. Avoid assuming that every read is guaranteed to return fresh server data while the device is disconnected.
17. Integrating Firestore with SwiftUI
SwiftUI interfaces often use an observable view model to separate UI state from database operations. This example demonstrates loading a list of note titles using a snapshot listener.
import SwiftUI
import FirebaseFirestore
struct SimpleNote: Identifiable {
let id: String
let title: String
}
@MainActor
final class NotesViewModel: ObservableObject {
@Published var notes: [SimpleNote] = []
@Published var errorMessage: String?
private var listener: ListenerRegistration?
func startListening() {
listener?.remove()
listener = Firestore.firestore()
.collection("notes")
.addSnapshotListener { [weak self] snapshot, error in
Task { @MainActor in
guard let self = self else { return }
if let error = error {
self.errorMessage = error.localizedDescription
return
}
self.notes = (snapshot?.documents ?? []).map {
document in
SimpleNote(
id: document.documentID,
title: document.data()["title"] as? String
?? "Untitled"
)
}
self.errorMessage = nil
}
}
}
func stopListening() {
listener?.remove()
listener = nil
}
deinit {
listener?.remove()
}
}
struct NotesListView: View {
@StateObject private var viewModel = NotesViewModel()
var body: some View {
NavigationStack {
List(viewModel.notes) { note in
Text(note.title)
}
.navigationTitle("My Notes")
.overlay {
if let message = viewModel.errorMessage {
Text(message)
.foregroundStyle(.red)
.padding()
}
}
.task {
viewModel.startListening()
}
.onDisappear {
viewModel.stopListening()
}
}
}
}
This is an introductory pattern. In a production application, avoid registering duplicate listeners during repeated view lifecycle events, and make the view model's loading, empty, and error states explicit.
18. Using Firebase Authentication with Firestore
When data belongs to individual users, Firebase Authentication can identify the signed-in user. The application can store the authenticated user's UID in a document path and use Security Rules to enforce ownership.
import FirebaseAuth
import FirebaseFirestore
func savePrivateNote(title: String) {
guard let userID = Auth.auth().currentUser?.uid else {
print("Please sign in first")
return
}
let data: [String: Any] = [
"title": title,
"ownerId": userID,
"createdAt": FieldValue.serverTimestamp()
]
Firestore.firestore()
.collection("users")
.document(userID)
.collection("notes")
.addDocument(data: data) { error in
if let error = error {
print("Could not save private note: \(error.localizedDescription)")
} else {
print("Private note saved")
}
}
}
Checking the current user in Swift improves the user experience, but it is not sufficient security by itself. Firestore Security Rules must independently enforce access permissions.
19. Firestore Security Rules
Security Rules determine which documents a client can read or modify. The following example allows a signed-in user to read and modify only documents under their own user document and its notes subcollection.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null
&& request.auth.uid == userId;
match /notes/{noteId} {
allow read, write: if request.auth != null
&& request.auth.uid == userId;
}
}
}
}
This example is an ownership-based starting point, not a complete validation policy. Add field validation, allowed-field checks, and appropriate create, update, and delete restrictions for your application's requirements.
Important: Security Rules are not filters. A query must be compatible with the rules for every document it could return. Test rules using the Firebase Emulator Suite before deploying them.
20. Error Handling and Troubleshooting
| Problem |
Possible Cause |
Solution |
| Permission denied |
Security Rules reject the operation or the user lacks authorization. |
Verify authentication, document ownership, query constraints, and rules. |
| Document not found |
The requested document ID does not exist. |
Check the collection name, ID, and existence of the document. |
| Missing index |
A query requires a composite index. |
Use the error's index creation link and review the query design. |
| Decoding error |
Stored values do not match the Swift model. |
Verify field names, types, optional fields, and timestamp handling. |
| Data appears stale |
The device may be offline or displaying cached data. |
Check connectivity and account for Firestore's offline behavior. |
| Writes appear pending |
The device has not yet synchronized the write with the server. |
Check connectivity and handle server-side failures. |
| Listener keeps running |
The listener registration was not removed. |
Retain the ListenerRegistration and call remove() when appropriate. |
| Firebase app configuration error |
The configuration file or initialization is incorrect. |
Verify the bundle identifier, target membership, and initialization order. |
21. Best Practices for Cloud Firestore
- Use descriptive collection names and consistent field names.
- Represent application records with well-defined Swift model types.
- Use document IDs deliberately and avoid depending on generated IDs for business meaning.
- Retrieve only the documents and fields your application needs.
- Use filters, limits, pagination, and suitable indexes for large collections.
- Use snapshot listeners only when ongoing updates are needed.
- Remove listeners when they are no longer required.
- Use server timestamps for reliable server-side event timing.
- Handle permission errors, network failures, and decoding problems.
- Use Firebase Authentication and restrictive Security Rules for private data.
- Never place server credentials or service-account private keys inside an iOS app.
- Monitor reads, writes, deletes, storage, and query patterns to manage cost.
- Test database rules and application behavior before production release.
22. Practical Project: Build a Firestore Notes App
Create a small iOS application where users can add notes, view saved notes, mark notes as completed, and delete notes.
Project Requirements
- Create a Firebase project and register the iOS application.
- Add the Firebase SDK and initialize the app.
- Create a Firestore collection or a user-specific notes subcollection.
- Build a SwiftUI form with title and description fields.
- Save new notes using
addDocument(data:).
- Retrieve notes using a collection query or snapshot listener.
- Display notes in a SwiftUI
List.
- Update completion status using
updateData.
- Delete a selected note using
delete().
- Show useful loading, empty, and error states.
- Protect private notes using Firebase Authentication and Security Rules.
- Test the app with multiple records and simulated network interruptions.
Sample Data for Testing
| title |
description |
isCompleted |
| Learn Swift |
Practice Swift language fundamentals |
false |
| Study Firestore |
Implement database operations |
true |
| Build an iOS app |
Create and test a sample project |
false |
23. Practice Exercises
- Create a
products collection and save three product documents.
- Retrieve a product using its document ID.
- Query all notes whose
isCompleted field is false.
- Order notes by creation time and retrieve only the five most recent records.
- Update a document's title without overwriting its other fields.
- Delete one selected document and verify the result.
- Use a snapshot listener to update a list when a document changes.
- Write Security Rules that restrict private notes to their owners.
- Test what happens when a user is signed out or offline.
24. Interview Questions and Answers
Q1. What is Cloud Firestore?
Cloud Firestore is a cloud-hosted NoSQL database that stores application data in collections and documents and supports queries, real-time listeners, and offline persistence.
Q2. What is the difference between a collection and a document?
A collection groups documents, while a document stores fields and values representing an individual record.
Q3. What is the difference between addDocument and setData?
addDocument(data:) creates a document using an automatically generated ID. setData(_:) writes to a specified document reference and can replace existing fields unless merge behavior is requested.
Q4. What is the difference between getDocument and addSnapshotListener?
getDocument performs a one-time document read. addSnapshotListener observes changes and delivers updated snapshots while the listener remains active.
Q5. What does updateData do?
updateData changes specified fields in an existing document and fails if that document does not exist.
Q6. How can you retrieve documents matching a condition?
Use Firestore query methods such as whereField, optionally combined with ordering, limits, and other supported query constraints.
Q7. How does Firestore support offline access?
Firestore can use cached documents and synchronize supported local writes when connectivity returns. The availability and freshness of data depend on the local cache and network state.
Q8. How do you secure user-specific data?
Use Firebase Authentication to identify the user and Firestore Security Rules to enforce that the user can access only authorized documents.
Q9. What is a Firestore index?
An index helps Firestore efficiently locate documents that satisfy a query. Some combinations of filters and sorting require a composite index.
Q10. Why should listeners be removed?
Removing unneeded listeners stops further observation and helps prevent unnecessary resource usage and duplicate updates.
Q11. Does deleting a document delete its subcollections?
No. Subcollection documents are not automatically removed when their parent document is deleted. They require a separate cleanup strategy.
Q12. Why might a Firestore query fail even when the user has access to some documents?
Security Rules evaluate whether a query's potential result set is permitted. The query constraints must be compatible with the rules; rules do not filter unauthorized results automatically.
25. Quick Revision Checklist
- Cloud Firestore stores data in collections and documents.
- Use
addDocument for automatically generated document IDs.
- Use
setData to write to a known document reference.
- Use
getDocument or getDocuments for one-time reads.
- Use
whereField to filter query results.
- Use
updateData to update selected fields.
- Use
delete() to remove a document.
- Use
addSnapshotListener for real-time updates.
- Remove listeners when they are no longer needed.
- Use Codable models for structured Swift data where appropriate.
- Understand offline caching and pending writes.
- Secure private data using Authentication and Firestore Security Rules.
- Test queries, permissions, and error handling before releasing the app.
26. Official Learning Resources
27. Conclusion
Cloud Firestore provides iOS developers with a practical way to store, retrieve, query, update, and delete cloud-hosted application data. By understanding collections, documents, Swift data models, real-time listeners, offline behavior, and Security Rules, developers can build more responsive and reliable applications.
Practice these concepts by building a small notes application, testing database operations, and protecting user data with Firebase Authentication and restrictive Security Rules.
Explore the iOS course: iOS App Development Training
Register for a demo: JustAcademy Course Demo Registration
Visit the website: JustAcademy