Improvements to Firestore for iOS App Development

By developing an extension with a group of helpers for reading, updating, listening to, and querying data from Firestore for iOS app utilizing unique requests, one can increase the efficiency of Firebase.

Firestore for iOS app developers can create and deploy web and mobile applications quickly and easily with the help of Firebase’s extensive range of tools and services. All engineers should consider it because of its simplicity, real-time database, authentication and authorization, hosting, analytics, and Cloud Functions.

Most software engineers have utilized these services, especially early in their careers. Years ago, I used Firebase in some of my initial projects.

The most important characteristic of Firebase is that it is simple to use. With a user-friendly dashboard and documentation, Firebase is simple to set up and use, making it convenient for coders to get started immediately.

However, even with all of these advantages, when working on a project that is more involved than a simple MVP, we may encounter an architectural issue: How can we set up a better connection with Firebase? In this article, I’ll solve this issue by concentrating on iOS applications that use Firebase Cloud Firestore.

Documentation in Firestore

Let’s study the recommended starting points for setting and reading the data from Firebase. Firestore for iOS app can be used for establishing and reading when installed, configured, and initialized.

	
import FirebaseCore 
import FirebaseFirestore 

FirebaseApp.configure() 
let db = Firestore.firestore()

The idea of establishing data is easy. Set the documents there with some data and create a reference to the collection.

	
let citiesRef = db.collection("cities")

citiesRef.document("SF").setData([
    "name": "San Francisco",
    "state": "CA",
    "country": "USA",
    "capital": false,
    "population": 860000,
    "regions": ["west_coast", "norcal"]
])

citiesRef.document("LA").setData([
    "name": "Los Angeles",
    "state": "CA",
    "country": "USA",
    "capital": false,
    "population": 3900000,
    "regions": ["west_coast", "socal"]
])

Leveraging a reference to a specific document from the collection makes retrieving that document relatively simple.

	
let docRef = db.collection("cities").document("SF")
docRef.getDocument { (document, error) in
  if let document = document, document.exists {
    let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
    print("Document data: \(dataDescription)")
  } else {
    print("Document does not exist")
  }
}

What may be concluded from the previous? As you can see, path components, such as the names of collections and documents, are necessary to retrieve some data by reference. The actual document should be designated as a dictionary. However, it might be challenging to maintain and test such an approach in reality (in actual app development), particularly if your app is scaling. Therefore, we require some structure, a company, and assistance. To fix this, let’s create an extension for the Firestore for iOS app object.

Response

Since you know that Firestore responses (documents) may be codeable, let’s name our new protocol Response.

	
protocol Response: Codable { }

This simple struct can be used if a response is empty.

	
struct EmptyResponse: Response { init() {} }

Request

Create our next protocol, Request, once we know of a response. We may consider any reference to access data from Firestore a path, so let’s add this property as a String data type. The Response itself, as one of the associated types, will be the output.

	
protocol Request {
  associatedtype Output: Response
  var path: String { get set }
  var output: Output? { get }
}

extension Request {
  var output: Output? {
    nil
  }
}

Extension for Firestore

Start with the easy section. We will have static access to the Firestore for iOS app database right inside the extension. It is required for the next  procedures that we intend to use in this expansion.

	
extension Firestore { 
static var db = Firestore.firestore() 
// ... 
}

Get Document

Let’s begin by reading a document from a collection quickly. Since Firebase is uniquely delivering its methods, I advise utilizing the async/await technique.

Each method only has one argument, called request. As you may recall, the protocol request contains every component needed to create a Firestore reference and response type. We require the path and output from the request at this time.

	
extension Firestore { 
// ... 
static func get<R: Request>(request: R) async throws -> R.Output? { 
try await get(request.path) 
} // ... 
}

However, we must slightly extend the dictionary and data method from Document Snapshot to function. We must be certain that a key is intended to be a string and a value is any if we are to expand the dictionary.

	
extension Dictionary where Key: ExpressibleByStringLiteral, Value: Any {
  func toData() -> Data? {
    try? JSONSerialization.data(withJSONObject: self)
  }

  func toCodable<T: Codable>(of type: T.Type) -> T? {
    guard let data = toData() else { return nil }
    return try? JSONDecoder().decode(T.self, from: data)
  }
}

This dictionary functions toData and toCodable aid in getting a Codable object ready for the DocumentSnapshot data method. As you can see, T is being passed as a codable; it should be an output.

	
import FirebaseFirestore

extension DocumentSnapshot {
  func data<T: Codable>(as: T.Type) -> T? {
    data()?.toCodable(of: T.self)
  }
  // ...
}

To put everything together, we will receive a final method in Firestore that only requires the document’s path. However, the data function already knows what type of document it is and returns a ready-made Codable model.

	
extension Firestore {
try {
  await get(request.path) 
} 
  static func get<T: Codable>(_ path: String) async throws -> T? {
try await db.document(path)
.getDocument() 
.data(as: T.self)
} 
// ... 
}

Get Documents in a Collection

We created a request protocol to obtain a document and used it with Firestore enhanced methods. Let’s now use request when retrieving documents from a Firestore collection is required. With one exception, it’s rather simple and identical. We must use compactMap to cast all fetched documents to our actual array model type.

	
extension Firestore { 
// ... 
static func get<R: Request>(request: R) async throws -> [R.Output] { 
try await self.get(request.path)
}
static func get<T: Codable>(_ path: String) async throws -> [T] { 
try await db.collection(path)
.getDocuments()
.documents
.compactMap { $0.data(as: T.self) } 
} 
// ... 
}

Expand Request Possibilities

Our request is a little primitive in this application. Let’s increase its adaptability and usefulness. We need to obtain documents with a certain count. In the request protocol, we can define a property with a name limit as an optional number.

	
protocol Request { 
associatedtype Output: Response 
// ... 
var limit: Int? { get } 
}

This request limit can be used to enhance the Firestore extension method get.

	
extension Firestore {

    // ...

    static func get<R: Request>(request: R) async throws -> [R.Output] {
        guard let limit = request.limit else {
            return try await self.get(request.path)
        }
        
        return try await db.collection(request.path)
            .limit(to: limit)
            .getDocuments()
            .documents
            .compactMap { $0.data(as: R.Output.self) }
    }

    // ...

}

Listeners at Firestore

I need to make a little observation before discussing viewing from Firestore. You can skip to the following paragraph if you know how Firestore listeners operate and what you need to remember by using references there. I won’t demonstrate here how to deal with the issue of multiple listeners and how to maintain a tidy group of listeners. The documentation for Firestore explains how to detach listeners.

We will currently use completions rather than async/await, but the fundamental idea of using request protocol will remain the same. We will utilize the well-known and practical swift enum outcome, which has the associated values Output and Error, to manage the completion’s outcome. We will also utilize our Error type; let’s start with it for a more subtle configuration.

	
enum FirestoreError: Error {
     case error(Error) 
     case noData 
}

Listening Documents

We have everything we need to create new techniques for document listening, including the output, request protocol, and associated error types. The same two static generic helpers can be created as before. We can detect specific errors with FirestoreError, such as when data is nil.

	
extension Firestore {

    // ...

    static func listenDocument<R: Request>(request: R, completion: @escaping (Result<R.Output, FirestoreError>) -> Void) {
        let ref: DocumentReference = db.document(request.path)
        listenDocument(ref, completion: completion)
    }

    static func listenDocument<T: Codable>(_ ref: DocumentReference, completion: @escaping (Result<T, FirestoreError>) -> Void) {
        ref.addSnapshotListener { snapshot, error in
            if let error = error {
                completion(.failure(.error(error)))
            } else if let result: T = snapshot?.data(as: T.self) {
                completion(.success(result))
            } else {
                completion(.failure(.noData))
            }
        }
    }
    // ...
}

Collecting Listening

We won’t be utilizing Firestore DocumentReference at this time. Instead, we’ll use FIRQuery to prepare our methods. When we used reference, it appears to us now to be rather similar. The pattern is also clearly visible when we create a helper for reading documents from the Firestore collection (read above). Success or failure will result in an outcome. We return success with an empty array if there is no data.

	
extension Firestore {

    // ...

    static func listenDocuments<R: Request>(request: R, completion: @escaping (Result<[R.Output], FirestoreError>) -> Void) {
        let query: Query = db.collection(request.path)
        listenDocuments(query, completion: completion)
    }
    
    static func listenDocuments<T: Codable>(_ query: Query, completion: @escaping (Result<[T], FirestoreError>) -> Void) {
        query.addSnapshotListener { snapshot, error in
            if let error = error {
                completion(.failure(.error(error)))
            } else if let result: [T] = snapshot?.documents.compactMap({ $0.data(as: T.self) }) {
                completion(.success(result))
            } else {
                completion(.success([]))
            }
        }
    }
    // ...
}

If you look up what a Query is, the documentation will provide the following information:

A query is an argument that you can read or listen. Refined Query objects can also be created by including filters and ordering.

Let’s expand our options from a Request perspective since we use the Query protocol to listen to Firestore collections. We have a variety of tactics for querying a collection, but let’s utilize NSPredicate instead because it’s more intriguing!

An explanation of logical specifications for limiting a fetch search or in-memory filtering.

	
protocol Request {
    // ...    
    var queryPredicate: NSPredicate? { get }
}

We can send this constraint directly to our Firestore Query so that it can query a certain set of database data. We may check for predicate existence in the method where we pass a request and use it in the query filter method.

	
extension Firestore {
    static func listenDocuments<R: Request>(request: R, completion: @escaping (Result<[R.Output], FirestoreError>) -> Void) {
        var query: Query = db.collection(request.path)
        if let predicate = request.queryPredicate {
            query = query.filter(using: predicate)
        }
        listenDocuments(query, completion: completion)
    }
    // ...
}

Updating a Document

First, I firmly advise against modifying your data directly from the client. Using specific APIs to update data in Firestore is preferable if you are creating a more robust architecture using the backend component. But let’s think about upgrading documents as well to complete our notion.

Let’s add a new property to the Request protocol: the data fields that should be submitted to the database. Although I gave it updatedDataFields, you can think of anything else.

	
protocol Request {
    // ...    
    var updatedDataFields: Codable? { get }
}

Although the request only contains a Codable element, the Original Firestore’s DocumentReference function updateData accepts fields as NSDictionaries. This means that to provide access to the dictionary from the Codable object; we need to introduce another modification to the Encodable protocol. We can prepare whatever we need, including dictionaries and optional data properties, using the wonderful Foundation API JSONEncoder and JSONSerialization tools.

	
extension Encodable {
    var dictionary: [String: Any]? {
        guard let data = self.data else { return nil }
        return (
            try? JSONSerialization.jsonObject(
                with: data,
                options: .allowFragments
            )
        )
        .flatMap { $0 as? [String: Any] }
    }
    
    var data: Data? {
        try? JSONEncoder().encode(self)
    }
}

In the example below, we can use async/await methods from Firestore to condense updateDataFields into a dictionary and compress our code.

	
extension Firestore {

    // ...

    static func update<R: Request>(request: R) async throws {
        try await update(
            data: request.updatedDataFields?.dictionary ?? [:],
            path: request.path
        )
    }
    
    static func update(data: [AnyHashable: Any], path: String) async throws {
        try await db.document(path).updateData(data)
    }
    
    // ...

}

Examples

We can test them once we have practically all the major helpers prepared for the Firestore extension. Let’s begin with a straightforward request—you can think of an example yourself.

As you can see, the structure Preferences only complies with the Response protocol.

	
struct Preferences: Response {
    let name: String
    let message: String?
    let version: Int
}

The time to craft a request then arrives. The result of this new struct, which complies with the Request protocol, is the Preferences type and the location of this document in Firestore.

	
struct PreferencesRequest: Request {
    typealias Output = Preferences
    var path: String = "config/preferences"
}

Example of Reading a Document

Now that the request has been prepared use it. Create a method called fetchPreferences.
	
func fetchPreferences() async throws -> Preferences? {
    let request = PreferencesRequest()
    return try await Firestore.get(request: request)
}

Example of a Listening Document

The approach can be modified in the following ways if you need to listen to this document:

	
func observePreferences() {
    let request = PreferencesRequest()
    Firestore.listenDocument(request: request) { result in
        switch result {
        case .success(let output):
            print(output)
        case .failure(let error):
            print(error)
        }
    }
}

Using NSPredicate to query

Let’s imagine that we have a group of users in the database. The first step is to build a model of the Firestore data that is anticipated.

	

struct User: Response, Identifiable {
    let id: String
    let username: String
    let userpic: String
    let followers: [String]
}

We need to filter people using the Firestore API, but only inside the parameters of our request because we will retrieve an array of users but not all. Here, we can utilize the queryPredicate we prepared. To do that, we must create an object called NSPredicate with an argument (id) that can be substituted for predicateFormat’s format value, which is a String.

	
struct UserFollowersRequest: Request {
    typealias Output = User
    var path: String
    var queryPredicate: NSPredicate?
    
    init(id: String) {
        self.path = "users/"
        self.queryPredicate = NSPredicate(format: "followers CONTAINS %@", id)
    }
}

The procedure for fetching users will eventually be like this.

	
func observeUserFollowers() {
    guard let id = Auth.auth().currentUser?.uid else { return }
    let request = UserFollowersRequest(id: id)
    Firestore.listenDocuments(request: request) { result in
        switch result {
        case .success(let output):
            print(output)
        case .failure(let error):
            print(error)
        }
    }
}

Schedule an interview with IOS developers

Conclusion

Naturally, I only covered some aspects and potential scenarios of using Firestore in this article. For example, reading arrays or dictionaries from a document, managing multiple listening references, and, eventually, more querying options are outside this article’s scope.

The coolest aspect of a Request protocol is that it can be used outside of Firestore. It can be used, for instance, to create requests for URLSession and update database documents. This strategy has worked well for me in several projects as I work to enhance client-Firestore database communication.

Therefore, by adopting this methodology, you can enhance communication between the client and the Firestore for iOS app database, leading to more efficient and streamlined processes. If you’re looking for professional assistance in iOS app development or need guidance in implementing Firestore for iOS app development project, contact our experienced iOS app development company for expert support and tailored solutions.

Frequently Asked Questions (FAQs)

1. What function does Firebase provide in firestore for iOS app?

Utilise Real-time Database or Cloud Firestore to store data, such as user information. Utilise cloud storage to keep files, including pictures and movies. Use Cloud Functions to activate backend code that runs in a safe environment. Notifications are sent using Cloud Messaging.

2. What benefits does firestore for iOS app offer?

You can use Firestore to do complex ACID transactions on your document data. As a result, you have more freedom in organizing your data. Focus on developing your applications by using the Firestore client-side development libraries for Web, iOS, Android, Flutter, C++, and Unity.

3. Firestore is a what kind of database?

Cloud A document-oriented NoSQL database is called Firestore. There are no tables or rows, in contrast to a SQL database. Data is instead kept in documents that are arranged into collections. A collection of key-value pairs are present in every document.

4. Which encryption is being used by Firestore for iOS app?

The 256-bit Advanced Encryption Standard is used to encrypt the contents and metadata of each Firestore object, and each encryption key is also encrypted using a set of master keys that is periodically rotated Client-side encryption and server-side encryption can both be utilized.


Book your appointment now

Request a Quote