Cloud Firestore Data Recording Process

Complete guide to writing and reading user data to Cloud Firestore database with Firebase integration

Swift Tutorial #8 — Cloud Firestore Data Recording Process

Cloud Firestore Data Recording

Note:

This tutorial covers writing and reading user data to Cloud Firestore database. Cloud Firestore is a NoSQL document database that provides real-time data synchronization and offline support.

Overview

The Cloud Firestore service provided by Firebase quickly performs writing and reading operations of user data to the database.

In the code block I will show as an example, we will ensure that the user logged in with Firebase Auth creates a collection in the database containing their email, the URL of the profile photo file they selected in Firebase Storage, the text they entered in the TextField, and the timestamp.

Reference:

Step 1: Firebase Console Setup

Go to the Firebase console and activate Cloud Firestore. In the options it suggests, while read and write operations cannot be performed in production mode, the rule you set for reading and writing operations will be valid until the date you specify in test mode.

Firebase Console Firestore Setup

Step 2: Creating Data Directory Variable

First, let's assign a variable to use Cloud Firestore.

let firestoreDatabase = Firestore.firestore()

Let's create an array to add the data we want to add. In the next stage, we will show this array as data. Since this data array is requested to be in String: Any format, we add as [String: Any] at the end.

let firestoreDatabase = Firestore.firestore()

// Create data dictionary
let dataDictionary = [
    "userEmail": Auth.auth().currentUser?.email ?? "",
    "profileImageURL": "https://firebasestorage.googleapis.com/...",
    "userText": textField.text ?? "",
    "timestamp": FieldValue.serverTimestamp()
] as [String: Any]

Step 3: Adding Data to Cloud Firestore Collection

We can perform the data recording operation by showing the collection name and data with collection.addDocument in the database variable.

firestoreDatabase.collection("users").addDocument(data: dataDictionary) { error in
    if let error = error {
        print("Error adding document: \(error)")
    } else {
        print("Document added successfully")
    }
}

Step 4: Complete Implementation

Here's a complete implementation for Cloud Firestore data recording:

import UIKit
import Firebase
import FirebaseFirestore

class FirestoreViewController: UIViewController {
    
    @IBOutlet weak var nameTextField: UITextField!
    @IBOutlet weak var emailTextField: UITextField!
    @IBOutlet weak var profileImageView: UIImageView!
    @IBOutlet weak var saveButton: UIButton!
    
    private let firestoreDatabase = Firestore.firestore()
    private var profileImageURL: String?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        loadUserData()
    }
    
    private func setupUI() {
        saveButton.setTitle("Save to Firestore", for: .normal)
        saveButton.backgroundColor = .systemBlue
        saveButton.setTitleColor(.white, for: .normal)
        saveButton.layer.cornerRadius = 8
        
        // Configure text fields
        nameTextField.placeholder = "Enter your name"
        emailTextField.placeholder = "Enter your email"
        
        // Load current user email
        if let user = Auth.auth().currentUser {
            emailTextField.text = user.email
        }
    }
    
    @IBAction func saveButtonTapped(_ sender: Any) {
        saveUserDataToFirestore()
    }
    
    private func saveUserDataToFirestore() {
        guard let name = nameTextField.text, !name.isEmpty else {
            showAlert(title: "Error", message: "Please enter your name")
            return
        }
        
        guard let email = emailTextField.text, !email.isEmpty else {
            showAlert(title: "Error", message: "Please enter your email")
            return
        }
        
        // Show loading state
        saveButton.isEnabled = false
        saveButton.setTitle("Saving...", for: .normal)
        
        // Create data dictionary
        var dataDictionary: [String: Any] = [
            "name": name,
            "email": email,
            "timestamp": FieldValue.serverTimestamp(),
            "lastUpdated": FieldValue.serverTimestamp()
        ]
        
        // Add profile image URL if available
        if let profileImageURL = profileImageURL {
            dataDictionary["profileImageURL"] = profileImageURL
        }
        
        // Add user ID if available
        if let userID = Auth.auth().currentUser?.uid {
            dataDictionary["userID"] = userID
        }
        
        // Save to Firestore
        firestoreDatabase.collection("users").addDocument(data: dataDictionary) { [weak self] error in
            DispatchQueue.main.async {
                self?.saveButton.isEnabled = true
                self?.saveButton.setTitle("Save to Firestore", for: .normal)
                
                if let error = error {
                    self?.showAlert(title: "Error", message: "Failed to save data: \(error.localizedDescription)")
                } else {
                    self?.showAlert(title: "Success", message: "Data saved to Firestore successfully!")
                }
            }
        }
    }
    
    private func loadUserData() {
        // Load existing user data if available
        if let userID = Auth.auth().currentUser?.uid {
            firestoreDatabase.collection("users")
                .whereField("userID", isEqualTo: userID)
                .order(by: "timestamp", descending: true)
                .limit(to: 1)
                .getDocuments { [weak self] snapshot, error in
                    if let error = error {
                        print("Error loading user data: \(error)")
                        return
                    }
                    
                    if let document = snapshot?.documents.first {
                        let data = document.data()
                        
                        DispatchQueue.main.async {
                            self?.nameTextField.text = data["name"] as? String
                            self?.emailTextField.text = data["email"] as? String
                            
                            if let imageURL = data["profileImageURL"] as? String {
                                self?.loadProfileImage(from: imageURL)
                            }
                        }
                    }
                }
        }
    }
    
    private func loadProfileImage(from url: String) {
        // Load profile image from URL
        // This would typically use a library like SDWebImage or Kingfisher
        print("Loading profile image from: \(url)")
    }
    
    private func showAlert(title: String, message: String) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
        alert.addAction(okAction)
        present(alert, animated: true, completion: nil)
    }
}

Step 5: Advanced Firestore Features

Real-time Data Synchronization

private func setupRealTimeListener() {
    guard let userID = Auth.auth().currentUser?.uid else { return }
    
    firestoreDatabase.collection("users")
        .whereField("userID", isEqualTo: userID)
        .addSnapshotListener { [weak self] snapshot, error in
            if let error = error {
                print("Error listening for updates: \(error)")
                return
            }
            
            guard let documents = snapshot?.documents else {
                print("No documents found")
                return
            }
            
            for document in documents {
                let data = document.data()
                print("Document updated: \(document.documentID)")
                // Handle real-time updates
                self?.updateUI(with: data)
            }
        }
}

private func updateUI(with data: [String: Any]) {
    DispatchQueue.main.async {
        self.nameTextField.text = data["name"] as? String
        self.emailTextField.text = data["email"] as? String
    }
}

Batch Operations

private func performBatchOperations() {
    let batch = firestoreDatabase.batch()
    
    // Add multiple documents
    let userData1 = ["name": "John", "email": "john@example.com"]
    let userData2 = ["name": "Jane", "email": "jane@example.com"]
    
    let doc1Ref = firestoreDatabase.collection("users").document()
    let doc2Ref = firestoreDatabase.collection("users").document()
    
    batch.setData(userData1, forDocument: doc1Ref)
    batch.setData(userData2, forDocument: doc2Ref)
    
    batch.commit { error in
        if let error = error {
            print("Error performing batch operation: \(error)")
        } else {
            print("Batch operation completed successfully")
        }
    }
}

Querying Data

private func queryUserData() {
    // Query by email
    firestoreDatabase.collection("users")
        .whereField("email", isEqualTo: "user@example.com")
        .getDocuments { snapshot, error in
            if let error = error {
                print("Error querying data: \(error)")
                return
            }
            
            for document in snapshot?.documents ?? [] {
                print("User: \(document.data())")
            }
        }
    
    // Query with multiple conditions
    firestoreDatabase.collection("users")
        .whereField("name", isGreaterThan: "A")
        .whereField("timestamp", isGreaterThan: Date().addingTimeInterval(-86400)) // Last 24 hours
        .order(by: "timestamp", descending: true)
        .limit(to: 10)
        .getDocuments { snapshot, error in
            // Handle query results
        }
}

Step 6: Data Models

User Model

struct User {
    let id: String
    let name: String
    let email: String
    let profileImageURL: String?
    let timestamp: Date
    let lastUpdated: Date
    
    init(id: String, name: String, email: String, profileImageURL: String? = nil, timestamp: Date = Date(), lastUpdated: Date = Date()) {
        self.id = id
        self.name = name
        self.email = email
        self.profileImageURL = profileImageURL
        self.timestamp = timestamp
        self.lastUpdated = lastUpdated
    }
    
    func toDictionary() -> [String: Any] {
        var dict: [String: Any] = [
            "name": name,
            "email": email,
            "timestamp": timestamp,
            "lastUpdated": lastUpdated
        ]
        
        if let profileImageURL = profileImageURL {
            dict["profileImageURL"] = profileImageURL
        }
        
        return dict
    }
    
    static func fromDocument(_ document: QueryDocumentSnapshot) -> User? {
        let data = document.data()
        
        guard let name = data["name"] as? String,
              let email = data["email"] as? String else {
            return nil
        }
        
        let profileImageURL = data["profileImageURL"] as? String
        let timestamp = (data["timestamp"] as? Timestamp)?.dateValue() ?? Date()
        let lastUpdated = (data["lastUpdated"] as? Timestamp)?.dateValue() ?? Date()
        
        return User(
            id: document.documentID,
            name: name,
            email: email,
            profileImageURL: profileImageURL,
            timestamp: timestamp,
            lastUpdated: lastUpdated
        )
    }
}

Step 7: Error Handling

Comprehensive Error Handling

enum FirestoreError: Error {
    case noUserAuthenticated
    case invalidData
    case networkError(Error)
    case permissionDenied
    case documentNotFound
    
    var localizedDescription: String {
        switch self {
        case .noUserAuthenticated:
            return "No user is currently authenticated"
        case .invalidData:
            return "Invalid data provided"
        case .networkError(let error):
            return "Network error: \(error.localizedDescription)"
        case .permissionDenied:
            return "Permission denied to access Firestore"
        case .documentNotFound:
            return "Document not found"
        }
    }
}

private func handleFirestoreError(_ error: Error) {
    let firestoreError: FirestoreError
    
    if let error = error as? FirestoreError {
        firestoreError = error
    } else {
        firestoreError = .networkError(error)
    }
    
    showAlert(title: "Firestore Error", message: firestoreError.localizedDescription)
}

Step 8: Security Rules

Firestore Security Rules

Configure your Firestore security rules in the Firebase Console:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Allow users to read and write their own data
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
    
    // Allow authenticated users to create documents in users collection
    match /users/{document=**} {
      allow create: if request.auth != null;
      allow read, update, delete: if request.auth != null && 
        request.auth.uid == resource.data.userID;
    }
    
    // Public read access for certain collections
    match /public/{document=**} {
      allow read: if true;
      allow write: if request.auth != null;
    }
  }
}

Note:

Cloud Firestore is a powerful NoSQL database that provides real-time synchronization and offline support. Remember to implement proper security rules, handle errors gracefully, and optimize for performance when working with large datasets.