Firebase is a powerful tool for app developers. It allows developers to focus in on app development without having to manage a lot of infrastructure like data storage on the cloud and user authentication, management and security.
Firestore stores data in collections, which are similar to database tables. You can have several collections in your database, in contrast to Realtime database which stored all the data in one big JSON tree, which made it difficult for complex data structuring. Each collection can have several documents. Documents have an identifier and contain many key-value pairs; the values of which can be strings, number, boolean, arrays of these, or sub-collections.
Using Firestore you can make complex queries and get filtered data by multiple where clauses. Firestore can also perform batch write operations.
Let's prepare account and create a project.
Open the Firebase Website and sign up for a free account. Then, log into your new account and go to your Firebase Dashboard.
Next, click Add Project. In the dialogs that appear, set a project name.
Finally, click Create Project. Then, in the screen that appears, choose Add Firebase to your iOS app.
Another dialog appears. Choose the following settings:
Finally, click Register App. In the next screen that appears, click the big Download GoogleService-Info.plist button. You’ll now download a .plist file, so save it in a convenient location (like ~/Downloads).
Add the .plist file to your Xcode project. Drag-and-drop the file from Finder into Xcode, and add it to the Project Navigator. When a dialog appears, make sure to tick the checkbox for Copy items if needed, and tick the checkbox next to Target.
Finally, click Continue.
Install Firebase SDK
The easiest way to add third-party libraries to your project is with CocoaPods. CocoaPods is a package manager, and it helps you to manage your libraries, download new versions, and integrate them with Xcode.
Open the Podfile file, and add the following lines:
pod 'Firebase/Core' pod 'Firebase/Firestore' pod 'ObjectMapper', '~> 3.4'
Configure Firebase in AppDelegate. Start by making sure the Firebase module is imported.
import Firebase
Use the configure
method in FirebaseApp inside the application:didFinishLaunchingWithOptions
function to configure underlying Firebase services from your .plist file.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { FirebaseApp.configure() return true }
Connect Firebase Database. Open up your app in Firebase and click the Database option on the left hand panel. Next you’ll be presented with an option to use Cloud Firestore or Realtime database. For this article, we’re going to use Cloud Firestore. Once you’ve selected Cloud Firestore, you’ll be presented with Security rules dialog. We’re going to select to Start in test mode. This will allow us to quickly develop without worrying about setting up permissions. Click Enable, Firebase will then set up your Cloud Firestore.
Model.
import Foundation import ObjectMapper import Firebase class Movie: Mappable, CustomStringConvertible { var title: String? var year: Int? public var description: String { return "Movie: title=\(title)" } init(title: String, year: Int) { self.title = title self.year = year } func mapping(map: Map) { title <- map["title"] year <- map["year"] } required init?(map: Map) { } } extension BaseMappable { init?(document: [String: Any]) { self.init(JSON: document) } }
Save object.
Add a new document with a generated ID
import Firebase ... let db = Firestore.firestore() var ref: DocumentReference? = nil ref = db.collection("items").addDocument(data: [ "name": "item 1", ]) { err in if let err = err { print("Error adding document: \(err)") } else { print("Document added with ID: \(ref!.documentID)") } }
Add a new document using ObjectMapper
let db = Firestore.firestore() var ref: DocumentReference? = nil let item = Movie(title: "The Movie", year: 2019) ref = db.collection("movies").addDocument(data: item.toJSON()) { err in if let err = err { print("Error adding document: \(err)") } else { print("Document added with ID: \(ref!.documentID)") } }
Add a new document with a specified ID
db.collection("items").document("1").setData([ "name": "item 2", ])
Retrieving Data.
Read all documents from a collection
import Firebase ... let db = Firestore.firestore() db.collection("items").getDocuments() { (querySnapshot, err) in if let err = err { print("Error getting documents: \(err)") } else { for document in querySnapshot!.documents { print("\(document.documentID) => \(document.data())") } } }
Read a particular document from a collection, by ID
db.collection("items").document("1") .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") } }
Using closure with status
// somewhere in ViewModel func loadList(completion: @escaping (Bool, [Movie]) -> ()){ var tasks = [Movie]() db.collection("movies").getDocuments() { (querySnapshot, err) in if let err = err { print("Error getting documents: \(err)") completion(false, tasks) } else { for document in querySnapshot!.documents { print("\(document.documentID) => \(document.data())") } completion(true, tasks) } } } // somewhere in Controller func loadMovies() { viewModel.loadList(completion: { (status, items) in print(status) }) }
Add listener to specific reference
class ViewController: UITableViewController { @IBOutlet weak var addButton: UIBarButtonItem! public var movies: [Movie] = [] private var listener : ListenerRegistration! fileprivate func baseQuery() -> Query { return Firestore.firestore().collection("movies").limit(to: 50) } fileprivate var query: Query? { didSet { if let listener = listener { listener.remove() } } } override func viewDidLoad() { super.viewDidLoad() self.query = baseQuery() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) self.listener.remove() } override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) self.listener = query?.addSnapshotListener { (documents, error) in guard let snapshot = documents else { print("Error fetching documents results: \(error!)") return } let results = snapshot.documents.map { (document) -> Movie in if let movie = Movie(dictionary: document.data()) { return movie } else { fatalError("Unable to initialize type \(Movie.self) with dictionary \(document.data())") } } self.movies = results self.tableView.reloadData() } } // TableView stuff }
Filtering Data
You can perform simple and compound queries on the Firestore database. The query is equivalent to an SQL where clause. For example, the below code will return all documents that have the given field value as true.
db.collection("movies").whereField("year", isEqualTo: 2019) .getDocuments() { (querySnapshot, err) in if let err = err { print("Error getting documents: \(err)") } else { for document in querySnapshot!.documents { print("\(document.documentID) => \(document.data())") } } }
Order and limit
You can specify the order and limit of your result.
itemsRef.order(by: "title").limit(to: 3)
Save and retrieve data using model inherited from Codable
Model.
struct Movie: Codable, CustomStringConvertible { var title: String var year: Int public var description: String { return "Movie: title=\(title)" } init(title: String, year: Int) { self.title = title self.year = year } } extension Encodable { func getDictionary() -> [String: Any]? { let encoder = JSONEncoder() guard let data = try? encoder.encode(self) else { return nil } return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any]} } } extension Decodable { init?(dictionary value: [String:Any]) { guard JSONSerialization.isValidJSONObject(value) else { return nil } guard let jsonData = try? JSONSerialization.data(withJSONObject: value, options: []) else { return nil } guard let newValue = try? JSONDecoder().decode(Self.self, from: jsonData) else { return nil } self = newValue } }
Saving.
let db = Firestore.firestore() var ref: DocumentReference? = nil let item = Movie(title: "The Movie", year: 2019) if let json = item.getDictionary() { ref = db.collection("movies").addDocument(data: json) { err in if let err = err { print("Error adding document: \(err)") } else { print("Document added with ID: \(ref!.documentID)") } } }
Retrieving.
let db = Firestore.firestore() db.collection("movies").getDocuments() { (querySnapshot, err) in if let err = err { print("Error getting documents: \(err)") } else { for document in querySnapshot!.documents { if (document.exists) { let movie = Movie(dictionary: document.data()) print("Movie => \(movie)") } print("\(document.documentID) => \(document.data())") } } }
Useful links