Firebase 101

firebase-copy

 

Why firebase?

Making a stand alone application is fun, we developers have access to a rich API provided from Apple to make beautiful design and intricate, elegant logic. However, for a modern app a way to persist data outside the actual device is needed.

Devices get lost or broken, apps can be uninstalled and so on. No developer wants users to lose their data. It may be a substantial collection of effort or memories. For this particular problem, Apple provides us with iCloud which is an elegant way of storing data outside the physical device.

Modern apps usually need to store data and be able to share it with friends so they can appreciate each other’s progress and accomplishments.. A network service used by an app used by more users is called a back end service. If we take a while to think about apps, it can be determine that there isn’t a back end service that fits all needs. Some apps are image oriented and need elaborate ways to store images, tag them, keep lists of persons portrayed in them, etc.. Other apps can be games, that need to store the player’s accomplishments, level of experience and other data that is particular to the specific game. Yet another segment of apps may be enterprise apps that log and help workers process actual commercial activity.

Firebase client server

The usual way to create a back end service would be to acquire hosting on a physical computer that is always connected to the internet and write software that handles traffic. The preferred protocol for this task is HTTP and the most used programming language is PHP. This is a substantial endeavor, it can take up more time than writing the actual app. This approach is less suited for individual mobile developers than for teams that include dedicated back end developers.

In the past few years back end as a service options became available. These are great for reducing the time to market of mobile apps and have back end development time all but gone.

Firebase is a back end as a service provider. It’s also one of the best because of integrations bundled with it and ease of use. Firebase is free for development and affordable pricing plans are available once the app is used by more users. Other back end providers are available, but this one is ours for this tutorial.

A few words about JSON

Firebase uses JSON to store data. JSON stands for JavaScript Object Notation and is a data storage and transmission format. In a nutshell, a JSON object can be thought of as a dictionary in the way that it contains keys and values for those keys. Types that can be stored in JSON are:

  • strings (eg. "John Appleseed")
  • numbers (eg. "3.14", but not “NaN”)
  • boolean (true, false, True, False)
  • null the absence of value [represented by nil in Swift]
  • arrays (a list of basic types, arrays, or dictionaries, eg. ["red", "blue", "yellow"]) [represented as Array in swift]
  • dictionaries (a key-value collection of basic types, arrays, or dictionaries)

An example JSON that can be used to represent a person could be:

{
    "name": "Andrei",
    "age": 20,
    "favourite_colors": [
       "red", 
       "blue"
    ],
    "memories": {
        "best": [
          "Riding the bicycle", 
          "Building model boats"
        ],
        "worst": null
    },
    "is_friendly": true
}

Another example with a list of todos:

[
    {
        "task" : "Buy milk",
        "done" : false      
    },
    {
        "task" : "Eat a cookie",
        "done" : true
    }
]

To make a quick check that it’s a valid JSON, an online validator can be used such as JSON lint. Mismatching parentheses or missing quotes will yield malformed JSON errors. If you create your own keys, they must be UTF-8 encoded, can be a maximum of 768 bytes, and cannot contain ., $, #, [, ], /, or ASCII control characters 0-31 or 127.

Arrays or dictionaries contained in other arrays or dictionaries are called nested objects. Firebase limits the nesting level to 32. This is more of a safety limit than an obstacle, and shouldn’t be reached if we consider our objects reasonably.

In Swift, JSON objects are represented by Dictionary objects. To keep the Swift code clean, it’s good to define constants when dealing with any string type keys. It may be a bit of an overhead for small projects, but the amount of avoided spelling errors and clarity of the code is well worth it. Working with Firebase, parsing and validating JSON isn’t necessary, this discussion was to help grasp how grateful we should be.

App Overview

To demonstrate Firebase, we will present the making of “Task Burner”, a minimal todo list app. In the interest of time, the focus will be more on Firebase and less on UIView fundamentals, Auto Layout, Table Views and Object Oriented Programming techniques.

A minimum prerequisite is to have built an app that uses a UITableView before. If you haven’t, go ahead and check out our awesome tutorials and return for some swift Firebase action. This is not to say that it’s absolutely mandatory, but it’s best not to get side tracked by details.

Task Burner architecture

The best way to understand the works is to follow the steps described below. If you want to fast track through it or if you get stuck, feel free to try the sample project.

The best way to learn is by practice. If you get stuck or you just want to get you hand dirty with Firebase you can download the finished app:

Task Burner

Firebase iOS SDK Overview

The classes that we are going to work with are:

  • FIRApp configures the Firebase SDK in the app. It reads the GoogleService-info.plist and starts up the required services. It can also take other values than the ones in the plist via direct calls.
  • FIRAuth handles the user’s signed in state. It holds a reference to FIRApp. It can also sign out the current user.
  • FIRDatabase provides database access for the specific app. It produces database references, which are objects to be used when interacting with data. It also supports pausing and resuming operation (goOffline and goOnline). It also has a setting to back up operations on disk so that intermittent connections can be handled, the persistenceEnabled property.
  • FIRDatabaseReference provides access to a section of the database. It coordinates reads and writes within that particular section. It provides elegant observation of changes with callback blocks, it creates, updates and deletes values.
  • FIRDataSnapshot is a vehicle through witch Firebase provides JSON data to your app. Data is accessed in the same way like a normal dictionary. This complements FIRDatabaseReference, so the basic operations of create, read, update, delete are covered.

Step 1: Create a New Xcode Project

Firebase uses the app’s bundle identifier to associate your app with firebase.

  1. Create a new Xcode project as a single view application.
  2. Set a unique bundle identifier so your application won’t conflict with the sample application or other reader’s applications.Task Burner project
  3. Click Next to complete the process.You should have an empty, running application. To make sure that everything works out, Run it on your device to make sure everything is in order.

Step 2: Create a new Firebase Project

  1. Open the Firebase Console, sign in, and create a new project:Create a project in firebase
  2. Fill in the project’s name and region if applicable.

Once your Firebase project is set up, you will be able to add and remove one or more apps, as needed. All the apps that will be added to this particular project, will be able to access the data it holds.

Step 3: Add an iOS App to the Firebase Project

The Firebase project holds a database for one or more apps. This can be useful if a number of apps share the same data. For example the Android version of your app.

Another example would be a blogging platform composed of two apps: the Writer app is used to create and edit articles, it shares data with the Reader app which is used to rate them and comment on them.

Add the iOS app

  1. Set an unique bundle identifier for your app.
  2. Fill in the app’s bundle identifier that you selected when creating the project. You can obtain it from the Xcode project navigator.Location of the bundle identifierThe bundle id, is a unique identifier to an iOS application, so you should create a new one so as not to conflict with the one used as example.
  3. A “GoogleService-Info.plist” file download will be started, make sure to save it in a known place. This file contains all the settings Firebase needs to start and work. If you look in it, it contains keys for all the different services that Google provides bundled with Firebase.
  4. Click Continue to see the pod instructions. No need to save the Podfile contents, it will be provided shortly.And the same for the AppDelegate code sample.
  5. Check to make sure that the email sign in method is enabled (auth > sign in method > email)Enable email sign in

Step 4: Integrate Firebase intro the iOS App

Firebase uses CocoaPods by default, which is a dependency manager. If you are not familiar with it, take a quick introto set it up.

  1. To make sure you have CocoaPods installed, open a terminal window, type pod –version and press Enter. You should get an output such as:
    [email protected] ~ $ pod --version
    1.0.0
    [email protected] ~ $
  2. Create a new empty file and name it Podfile and save it next to your .pbxproj file.Empty Podfile
  3. Paste the following in the Podfile:
    inhibit_all_warnings!
    
    target "YourAppTargetHere" do
        pod 'Firebase/Core'
        pod 'Firebase/Database'
        pod 'Firebase/Auth'
    end
  4. Replace YourAppTargetHere with your actual app target.This Podfile is used by CocoaPods to determine which dependencies are needed for a project. It downloads code from trusted repositories and adds them to your project. This is to keep the actual app code short. Over a few months, when updates to installed pods will be released, developers can update pods to make sure they always use the latest and greatest versions.
  5. Close Xcode and open a terminal in the project’s folder. The easiest way is to drag and drop the project folder over the terminal icon in the dock.Open Terminal to Folder
  6. Run “pod install” in the project’s directory using a terminal. You should see the output similar to:
    [email protected] Task Burner$ pod install
    
    Analyzing dependencies
    Downloading dependencies
    Using Firebase (3.7.0)
    Using FirebaseAnalytics (3.4.3)
    Using FirebaseAuth (3.0.5)
    Using FirebaseCore (3.4.3)
    Using FirebaseDatabase (3.0.3)
    Using FirebaseInstanceID (1.0.8)
    Using GoogleInterchangeUtilities (1.2.2)
    Using GoogleNetworkingUtilities (1.2.2)
    Using GoogleSymbolUtilities (1.1.2)
    Using GoogleUtilities (1.3.2)
    Generating Pods project
    Integrating client project
    Sending stats
    Pod installation complete! There are 3 dependencies from the Podfile and 10
    total pods installed.
    [email protected] Task Burner $

    This will take a minute or two, and it will generate an .xcworkspace file. This is an Xcode Workspace and it can contain multiple .pbxproj files. You should use only the .xcworkspace in Xcode from now on.

    Xcworkspace

  7. Xcode should be able to build the .xcworkspace project now.
  8. Once the project builds, drag and drop the GoogleService-info.plist you downloaded when creating the Firebase project in the Xcode Project.Import GoogleService-Info.plist
  9. Add the following lines to AppDelegate.swift in the applicationDidFinishLaunching method:
    FIRApp.configure()
  10. Add the keychain sharing entitlement to the build target in the Xcode project:Add the keychain entitlementThe keychain services API is a way for developers to use built in security features in Apple devices. To store secure information about users, applications call the keychain API to leverage specialized hardware and encrypt data. The keychain entitlement allows the iOS app (and Firebase auth) to store the authentication token and perform user registration and sign in.

Xcode should be able to build and run the project without errors. When running on the simulator, issues similar to nw_socket_set_common_sockopts setsockopt SO_NOAPNFALLBK failed: [42] Protocol not available, dumping backtrace: can be ignored.

Step 5: Login Screen

Firebase provides authentication for the most common services: email, facebook, google, github and others. It provides efficient wrapping for the developers so we don’t need to bother with very low level details about the implementation.

  1. To create an authentication screen, create a new UIViewController subclass and name it MainViewController.swiftMain View Controller
  2. In Interface Builder add a navigation controller and set MainViewController as the root view controller of the navigation controller.
  3. Set the Navigation Controller as the initial view controller from the Storyboard:Interface Builder View Controllers
  4. Add two buttons and two text fields to create the interface:login screen
  5. Add the MainViewController code:
    import UIKit
    import Firebase
    
    class MainViewController: UIViewController {
    
        static let kUserLoggedInSegueIdentifier = "userLoggedIn"
    
        /* @IBOutlets create references so strings can be read from
         the UITextFields
         */
        @IBOutlet weak var emailField: UITextField!
        @IBOutlet weak var passwordField: UITextField!
    
        /* Have a reference to the last signed in user to compare
         changes
         */
        weak var currentUser: FIRUser?
    
        @IBAction func login() {
            print("login")
        }
    
        @IBAction func register() {
            print("register")
        }
    
        @IBAction func signOut(segue: UIStoryboardSegue) {
    
        }
    }
  6. Going to the Connections inspector in the interface builder now shows the @IBAction members and @IBOutletproperties and they can be linked to the elements.
  7. Connect emailField and passwordField references to the respective interface builder elements.
  8. Connect the login() and register() actions to the buttons. Make sure to select “Touch Up Inside”.Connecting Interface BuilderRunning the project now, it should build without errors and pressing the Login and Register buttons should print “login” and “register” in the debugger.
  9. A UITableViewController is the perfect component to show tasks in. The Table View Controller works by interrogating a data source to show cells and calls back a delegate to track the user’s actions. To add a table view controller in the interface builder, drag and drop one from the Component Library.
  10. Create a push segue, by control-dragging from the source view controller to the new table view controller.
  11. Set the segue identifier to userLoggedIn.

Interface builder table view

Step 6: Handle User Registration

To create a user in Firebase, there’s a member to be called with the email and password parameters.

let email = “userEmail@email.comlet password = “LoNgPaSsWoRd”
FIRAuth.auth()!.createUser(withEmail: email,password: password) {
    user, error in
}

The call is asynchronous. This means that the callback closure will be called when it will be completed and the actual Firebase call won’t freeze the UI. An important note is to keep in mind that a login is performed after a user creation.

Step 7: Handle User Sign In

The sign in call syntax is similar:

FIRAuth.auth()!.signIn(withEmail: email, password: password)

As there isn’t any callback closure when the signIn call is completed, a state change listener should be in place when the call is performed.

if let auth = FIRAuth.auth() {
    auth.addStateDidChangeListener() { auth, user in
        if user != nil {
        }
    }
}

To get all the parts working in code, follow this example to fill in your MainViewController:

MainViewController

import UIKit
import Firebase

class MainViewController: UIViewController {

    static let kUserLoggedInSegueIdentifier = "userLoggedIn"

    /* @IBOutlets create references so strings can be read from
     the UITextFields
     */
    @IBOutlet weak var emailField: UITextField!
    @IBOutlet weak var passwordField: UITextField!

    /* Have a reference to the last signed in user to compare
     changes
     */
    weak var currentUser: FIRUser?

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        if let auth = FIRAuth.auth() {

            /* Add a state change listener to firebase
             to get a notification if the user signed in.
            */
            auth.addStateDidChangeListener({ (auth, user) in
                if user != nil && user != self.currentUser {
                    self.currentUser = user
                    self.performSegue(withIdentifier: MainViewController.kUserLoggedInSegueIdentifier,
                                      sender: self)
                }
            })
        }
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)

        if let auth = FIRAuth.auth() {
            /* Following the balance principle in iOS,
             Stop listening to user state changes while not on screen.
            */
            auth.removeStateDidChangeListener(self)
        }
    }

    @IBAction func login() {
        if let email = self.emailField.text,
            let password = self.passwordField.text,
            let auth = FIRAuth.auth() {

            /* If both email and password fields are not empty,
             call firebase signin
            */
            auth.signIn(withEmail: email,
                        password: password)
        }
    }

    @IBAction func register() {
        if let email = self.emailField.text,
            let password = self.passwordField.text,
            let auth = FIRAuth.auth() {
            /* Note: creating a user automatically signs in.
            */
            auth.createUser(withEmail: email,
                            password: password) { user, error in
                                if error != nil {
                                    self.present(UIAlertController.withError(error: error as! NSError),
                                                 animated: true,
                                                 completion: nil)
                                }
            }
        }
    }

    @IBAction func signOut(segue: UIStoryboardSegue) {
        /* When Sign out is pressed, and the task list controller closes,
         call Firebase sign out.
        */
        if let auth = FIRAuth.auth() {
            do {
                try auth.signOut()
            } catch {
                self.present(UIAlertController.withError(error: error as NSError),
                             animated: true,
                             completion: nil)
            }
        }
    }
}

[collapse]

Once you have all this, you can follow users creation and login on the firebase console.

Login Video

Step 8: Tasks View Controller

  1. Click on the Table View Cell in interface builder and set it’s reuse identifier to TasksTableViewCellCell Reuse Identifier
  2. Create a new Cocoa Touch Class, subclassed from UITableViewController and name it TasksTableViewController.

TasksTableViewController code:

TasksTableViewController

import UIKit
import Firebase

class TasksTableViewController: UITableViewController {

    static let kTasksListPath = "tasks-list"
    static let kTaskViewControllerSegueIdentifier = "TaskViewController"

    let tasksReference = FIRDatabase.database().reference(withPath: kTasksListPath)
    var tasks = [Task]()
    var selectedTask: Task? = nil
    weak var currentUser = FIRAuth.auth()?.currentUser

    override func viewDidLoad() {
        super.viewDidLoad()

        /* Query tasks from firebase when this view controller is instantiated
        */
        self.tasksReference.queryOrdered(byChild: Task.kTaskCompletedKey).observe(.value, with: { snapshot in

            /* The callback block will be executed each and every time the value changes.
            */
            var items: [Task] = []

            for item in snapshot.children {
                let task = Task(snapshot: item as! FIRDataSnapshot)
                items.append(task)
            }

            self.tasks = items
            self.tableView.reloadData()
        })
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == TasksTableViewController.kTaskViewControllerSegueIdentifier {
            /* Pass along the selected task to the particular task view controller.
            */
            if let taskViewController = segue.destination as? TaskViewController {
                taskViewController.taskTitle = self.selectedTask?.title
                taskViewController.taskCompleted = self.selectedTask?.completed
            }
        }
    }

    @IBAction func prepareForUnwind(segue: UIStoryboardSegue) {
        if let taskViewController = segue.source as? TaskViewController {
            taskViewController.updateValues()

            if let task = self.selectedTask {
                /* This particular task was an existing one
                    it should be updated in Firebase.
                */
                if let title = taskViewController.taskTitle,
                    let completed = taskViewController.taskCompleted,
                    let email = self.currentUser?.email {

                    let taskFirebasePath = task.firebaseReference
                    taskFirebasePath?.updateChildValues([
                        Task.kTaskTitleKey: title,
                        Task.kTaskUserKey: email,
                        Task.kTaskCompletedKey: completed
                        ])
                }
            } else {
                /* This task is new,
                 it should be created in Firebase
                */
                if let title = taskViewController.taskTitle,
                    let completed = taskViewController.taskCompleted,
                    let email = self.currentUser?.email {

                    let task = Task(title: title, user: email, completed: completed)
                    let taskFirebasePath = self.tasksReference.ref.child(title.lowercased())
                    taskFirebasePath.setValue(task.toDictionary())
                }
            }
        }
        self.selectedTask = nil
    }

    @IBAction func addTask() {
        self.performSegue(withIdentifier: TasksTableViewController.kTaskViewControllerSegueIdentifier, sender: self)
    }

    // MARK: - Table view data source

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.tasks.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TasksTableViewCell", for: indexPath)

        if let c = cell as? TasksTableViewCell {
            let task = self.tasks[indexPath.row]
            c.titleLabel.text = task.title
            c.completedSwitch.setOn(task.completed, animated: true)
        }
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let task = self.tasks[indexPath.row]

        /* Keep a reference to the task that is currently edited.
        */
        self.selectedTask = task
        self.performSegue(withIdentifier: TasksTableViewController.kTaskViewControllerSegueIdentifier, sender: self)
    }

    override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }

    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            let task = self.tasks[indexPath.row]
            /* Delete this task.
             No tableview updates should be done here because a callback notification
             will be provided.
            */
            task.firebaseReference?.removeValue()
        }
    }
}

[collapse]

Step 9: Add the Navigation Bar Items

To get all the code in place and the interface builder work completed, you need to create the remaining components. This will ensure all the navigation is complete and focus only on Firebase code after that.

A Navigation Bar is always a nice place to have buttons. It’s always visible while the table view scrolls and it’s dependent on the shown view controller. For the tasks app, a Sign Out button is needed and a button to add a new task. To get them set up, we already have the @IBAction links in code in place, so only Interface Builder work is needed.

  1. Add the Sign Out button. Drag and drop a Bar Button Item in the left slot of the Navigation Bar.
  2. Control-drag from the Sign Out button to the Exit outlet represented by a red square at the top of the TasksTableViewController.
  3. Select signOutWithSegue.Unwind SegueAll this was to use so called Segues Unwinding. Notice that @IBAction func signOut(segue: UIStoryboardSegue)is in the MainViewController. When the user will press the Sign Out button, the TasksListViewController will signal the MainViewController that it needs to disappear. This functionality may not be very intuitive, but it helps us to keep all the user authentication code in the MainViewController.
  4. Add an Add Task button similarly, just this time we need to call @IBAction func addTask() instead of Unwind Segue.Add Task

Step 10: Task Cell

Arguably, work can be done without creating a cell subclass, but it’s a good practice to have components separated. Once the project grows in size and more customization is needed, it’s much more manageable to have separate classes each with it’s own concern.

  1. Create a new Cocoa Touch Class, name it TasksTableViewCell and set it to be a subclass of UITableViewCell.
  2. Code for TasksTableViewCell:
import UIKit

class TasksTableViewCell: UITableViewCell {
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var completedSwitch: UISwitch!
}

Elements will be linked in Interface Builder so that the rest of the code has easy access to them. In a more elaborate project, code would be added to handle cell states and operations. For this tutorial a conscious decision has been made to keep the Firebase code in the tasks list, so it’s easier to follow.

  1. Go to Interface Builder, and select the Table View Cell in the tasks list created previously. Check that it already has the reuse identifier already set to TasksTableViewCell (check by selecting the cell and going to the Attributes Inspector tab).
  2. In the Identity Inspector for the cell, fill in the Custom Class field to contain TaskTableViewCell. This associates the class you just made to the stock cell.Cell Subclass
  3. Add the UILabel and UISwitch to the cell and comment the referencing outlets to the properties from TaskTableViewCell:Link IBOutlets

The app will handle the event of the user selecting a cell. This will create a problem for the Switch on it because it’s tap area will be small enough to miss. A solution for this would be to make the switch larger, so the user can set the task completed. Another solution, the one chosen for this tutorial, was to disable the user interaction with the UISwitch. So, the only option for the user to set the task completed is to go to the next screen and edit it.

Step 11: Task ViewController

To show the details of a Task, a decision has been made to use a dedicated view controller. This helps in the case when more features will be needed in the future. For the example in our tutorial the Table View Cell with a switch would be enough, but completing chores is not as easy as we want it to be. Sometimes it’s actually a drag.

  1. Create a new Cocoa Touch Class, set it to be a subclass of UIViewController and name it TaskViewController.
  2. Fill in TaskViewController code:
import UIKit

class TaskViewController: UIViewController {

    var taskTitle: String?
    var taskCompleted: Bool?

    @IBOutlet weak var titleField: UITextField!
    @IBOutlet weak var completedSwitch: UISwitch!

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        self.titleField.text = self.taskTitle ?? ""
        self.completedSwitch.setOn(taskCompleted ?? false, animated:false)
    }

    func updateValues() {
        self.taskTitle = self.titleField.text
        self.taskCompleted = self.completedSwitch.isOn
    }
}

The functionality we want from this component is to read and update the UI with values in viewDidAppear(_:). When it is dismissed, we want to read any changes the user has made in updateValues(). There can be other ways to accomplish this, but this is one of the shortest. The most obvious way to improve it is to have a Task property later, and have the view controller read data straight from the task and update it in Firebase when dismissed.

To add it in interface builder: 3. Add a new ViewController 4. Control-drag from the TasksListViewController to it and select Present Modally. 5. Set the storyboard identifier to TaskViewController. 6. Set the view controller’s custom class to TaskViewController.

Task View Controller Segue

  1. Add two labels, an UITextField, an UISwitch and an UIButton. Set their titles as appropriate.
  2. Using the Connections inspector, connect the UITextField and UISwitch to their respective outlets.
  3. Control-drag from the button to the exit button at the top of the view controller and select prepareForUnwindWithSegue to enable Unwind Segue back to the TasksListViewController.

Task View Controller UI

Right now the app should build without errors and have the navigation intact. You should be able to login, have a tasks list that shows one task. When you select that task or click the add button, the TaskViewController is shown. You should be able to exit screens all the way to the MainViewController.

Step 12: Task Model

Now that all the UI is out of the way, we can focus on more pertinent tasks. Firebase stores data in JSON format and it can be up to 32 levels deep. We should try to keep data structures as flat as possible. Keys for JSON values must be UTF-8 encoded, can be a maximum of 768 bytes, and cannot contain ., $, #, [, ], /, or ASCII control characters 0-31 or 127. If this sounds too pedantic, follow a rule of the thumb to use only letters, numbers, and underscores for keys. Eg. *my_precious_key_number_1″ instead of “my_◈#_1″. It’s more expressive and an established naming convention.

All applications have data structures that developers call models. A model is used in code as a source and storage for information. It’s possible to avoid having dedicated model classes, but it would lead to mayhem as developers would no longer be able to understand and keep track of code.

Because our application is a task manager, it makes sense to model our representation of a task following what we know from daily life. One could jot down a quick list of properties a task could have, similar to this:

Property NameSwift TypeExplanation
idStringAn unique identifier. Other objects may have the same title or equal values, better to have a unique identifier at hand always.
firebaseReferenceFIRDatabaseReferenceA firebase type used to reference our Swift object to the Firebase store.
titleStringMakes sense to have a title.
completedBoolNeed to be able to complete tasks.
userStringA reference to the user that created it.
priorityDoubleHow important is this task. eg. 0 — not important, 10 — very important
urgencyDoubleSome tasks may not be important, but very urgent.
friends[String]Array of user identifiers who may participate in the same task.
dateNumberThe number of seconds since 1970. We can’t use Date to send to firebase, so conversion to a standard is needed.

And many more properties could be added. For the purpose of our tutorial, we present the implementation for the first four. Arguably, the absolute minimal representation of a task: unique id, a firebase reference, a title and wether or not it’s completed.

  1. To create the Task model struct, create a new Swift File from Xcode and name it Task.swift.
  2. Fill in the Task code:
import Foundation
import Firebase

struct Task {

    /* Have keys as constants to prevent spelling errors
     * and avoid confusion. eg. "title" can be found in
     * ViewControllers too, so places can exist where the
     * particular string is used for something else.
     */
    static let kTaskTitleKey = "title"
    static let kTaskCompletedKey = "completed"
    static let kTaskUserKey = "user"

    let title: String
    let user: String
    let firebaseReference: FIRDatabaseReference?
    var completed: Bool

    /* Initializer for instantiating a new object in code.
    */
    init(title: String, user: String, completed: Bool, id: String = "") {
        self.title = title
        self.user = user
        self.completed = completed
        self.firebaseReference = nil
    }

    /* Initializer for instantiating an object received from Firebase.
    */
    init(snapshot: FIRDataSnapshot) {
        let snapshotValue = snapshot.value as! [String: Any]
        self.title = snapshotValue[Task.kTaskTitleKey] as! String
        self.user = snapshotValue[Task.kTaskUserKey] as! String
        self.completed = snapshotValue[Task.kTaskCompletedKey] as! Bool
        self.firebaseReference = snapshot.ref
    }

    /* Method to help updating values of an existing object.
    */
    func toDictionary() -> Any {
        return [
            Task.kTaskTitleKey: self.title,
            Task.kTaskUserKey: self.user,
            Task.kTaskCompletedKey: self.completed
        ]
    }
}

We chose a struct instead of a class to deter too many classes making changes to the same object. The main difference is that a struct is passed by value, so when a change occurs, another object that has a copy won’t be affected.

A task has properties that hold actual data that are or may be useful for the app. There’s a initializer used to create a Task object from user input and another one from a firebase snapshot.

The FIRDataSnapshot contains data from a Firebase Database location. Any time you read Firebase data, you receive the data as a FIRDataSnapshot. A FIRDatabaseReference can be thought of as an accessor to the firebase representation. It can be used for reading or writing data. The dictionary representation function is needed to communicate changes to firebase.

After creating this file, the app should work like before, but no noticeable change is present because the model isn’t integrated in code yet.

Step 13: Get the list of tasks from Firebase

As the “CRUD” principle goes, any useful data has the same lifecycle of creation, reading, updating and deletion.

  • Creation: With Firebase, we define a database reference to the array of tasks, and then we create a task representation from a dictionary.
  • Reading: We create a database reference for the list of tasks and register to observe changes.
  • Updating: On the particular task’s FIRDatabaseReference, call updateChildValues().
  • Deletion: Call removeValue() on the specific task. For creating, updating and deleting the task list observer will call back and update the app model.

 

 

  1. Add the following properties are needed in the TasksTableViewController:
    var tasks = [Task]()
    var selectedTask: Task? = nil

This creates an array of Task objects which will be used to fill the Table View. The selectedTask property will be used to keep track of which task the user selected.

2. To observe changes to the tasks list, query and observe it when the TasksTableViewController is initialized.Place this code in a convenient place such as viewDidLoad()

self.tasksReference.queryOrdered(byChild: kTaskCompletedKey).observe(.value, with: { 
    snapshot in
    var items: [Task] = []
    for item in snapshot.children {
        let task = Task(snapshot: item as! FIRDataSnapshot)
        items.append(task)
    }
    self.tasks = items
    self.tableView.reloadData()
})

The first line runs a query on the tasks list database reference. It then orders the results by child attribute, which returns a FIRDatabaseQuery object. Then, the view controller registers as an observer to changes in this list’s values. Other FIRDataEventTypes are available and can be used for more fine grained control. The closure is instantiating models from the data and feeding them to the UI.

Running right now, still wouldn't show created tasks in the table view because the tasks array is not integrated yet.

3. Replace the following code in the TasksListViewController:

class TasksTableViewController {
    ... 

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.tasks.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TasksTableViewCell", for: indexPath)

        if let c = cell as? TasksTableViewCell {
            let task = self.tasks[indexPath.row]
            c.titleLabel.text = task.title
            c.completedSwitch.setOn(task.completed, animated: true)
        }
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let task = self.tasks[indexPath.row]

        /* Keep a reference to the task that is currently edited.
        */
        self.selectedTask = task
        self.performSegue(withIdentifier: kTaskViewControllerSegueIdentifier, sender: self)
    }

    ...
 }

The two members func tableView(_:numberOfRowsInSection:) and tableView(_:cellForRowAt:) are required by the UITableViewDataSource protocol. They provide the number of rows and the cells for those rows.

The tableView(_:didSelectRowAt:) member is required by the UITableViewDelegate. It is called when the user tapped on a table view cell. The project is using it to present the TaskViewController.

Running the project now, would in theory be able to show tasks, but the code to create them is still missing.

Step 14: Add new Task

Thanks to the elegant model already created, the code snippet creating the task is 3 lines long.

let task = Task(title: title, user: email, completed: completed)
let taskFirebasePath = self.tasksReference.ref.child(title.lowercased())
taskFirebasePath.setValue(task.toDictionary())

First, a task struct instance is created with the user’s options. Then, the lowercase title is used as an identifier. It could be a UUID or some other value. The new reference is uploaded with all the task’s values.

Step 15: Edit Task

Updating done with updateChildValues() on an existing reference. A partial dictionary can also be supplied.

task.firebaseReference?.updateChildValues([
    kTaskTitleKey: title,
    kTaskUserKey: email,
    kTaskCompletedKey: completed
])

It’s always nice to have constants defined to take advantage of autocomplete and defeat little spelling errors.

Step 16: Integrating tasks creation and editing

To communicate between the TasksViewController and the single TaskViewController, the segues features can be used. prepare(for segue: UIStoryboardSegue, sender: Any?) is called just before a segue is performed (read: before a View Controller is shown). It can be used to pass data to it. prepareForUnwind(segue: UIStoryboardSegue) is the action you linked to the button previously. It’s called just before the segue is unwound and it can be used to read the user’s input.

  1. Replace the existing implementation with the code below in TasksListViewController:
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == kTaskViewControllerSegueIdentifier {
            /* Pass along the selected task to the particular task view controller.
            */
            if let taskViewController = segue.destination as? TaskViewController {
                taskViewController.taskTitle = self.selectedTask?.title
                taskViewController.taskCompleted = self.selectedTask?.completed
            }
        }
    }

    @IBAction func prepareForUnwind(segue: UIStoryboardSegue) {
        if let taskViewController = segue.source as? TaskViewController {
            taskViewController.updateValues()

            if let task = self.selectedTask {
                /* This particular task was an existing one
                    it should be updated in Firebase.
                */
                if let title = taskViewController.taskTitle,
                    let completed = taskViewController.taskCompleted,
                    let email = self.currentUser?.email {

                    let taskFirebasePath = task.firebaseReference
                    taskFirebasePath?.updateChildValues([
                        kTaskTitleKey: title,
                        kTaskUserKey: email,
                        kTaskCompletedKey: completed
                        ])
                }
            } else {
                /* This task is new,
                 it should be created in Firebase
                */
                if let title = taskViewController.taskTitle,
                    let completed = taskViewController.taskCompleted,
                    let email = self.currentUser?.email {

                    let task = Task(title: title, user: email, completed: completed)
                    let taskFirebasePath = self.tasksReference.ref.child(title.lowercased())
                    taskFirebasePath.setValue(task.toDictionary())
                }
            }
        }
        self.selectedTask = nil
    }

Running the code now should allow you to create tasks, set them completed or not, update titles. When swiping left on a cell and then tapping the delete button, nothing happens.

Firebase Task Create And Update

Step 17: Deleting Tasks

Call removeValue() to end the object’s cloud ambitions. 1. Replace the tableView(_ tableView: UITableView, commit editingStyle: implementation with the one below:

    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            let task = self.tasks[indexPath.row]
            /* Delete this task.
             No tableview updates should be done here because a callback notification
             will be provided.
            */
            task.firebaseReference?.removeValue()
        }
    }

A callback will be received in self.tasksReference.queryOrdered(byChild: kTaskCompletedKey).observe closure and the table view will update.

The task deletion should work at this point.

Final TasksTableViewController

For reference or in case you missed a step, you can compare your work to the TasksTableViewController full code:

TasksTableViewController

import UIKit
import Firebase

let kTasksListPath = "tasks-list"
let kTaskViewControllerSegueIdentifier = "TaskViewController"

class TasksTableViewController: UITableViewController {

    let tasksReference = FIRDatabase.database().reference(withPath: kTasksListPath)
    var tasks = [Task]()
    var selectedTask: Task? = nil
    weak var currentUser = FIRAuth.auth()?.currentUser

    override func viewDidLoad() {
        super.viewDidLoad()

        /* Query tasks from firebase when this view controller is instantiated
        */
        self.tasksReference.queryOrdered(byChild: kTaskCompletedKey).observe(.value, with: { snapshot in

            /* The callback block will be executed each and every time the value changes.
            */
            var items: [Task] = []

            for item in snapshot.children {
                let task = Task(snapshot: item as! FIRDataSnapshot)
                items.append(task)
            }

            self.tasks = items
            self.tableView.reloadData()
        })
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == kTaskViewControllerSegueIdentifier {
            /* Pass along the selected task to the particular task view controller.
            */
            if let taskViewController = segue.destination as? TaskViewController {
                taskViewController.taskTitle = self.selectedTask?.title
                taskViewController.taskCompleted = self.selectedTask?.completed
            }
        }
    }

    @IBAction func prepareForUnwind(segue: UIStoryboardSegue) {
        if let taskViewController = segue.source as? TaskViewController {
            taskViewController.updateValues()

            if let task = self.selectedTask {
                /* This particular task was an existing one
                    it should be updated in Firebase.
                */
                if let title = taskViewController.taskTitle,
                    let completed = taskViewController.taskCompleted,
                    let email = self.currentUser?.email {

                    let taskFirebasePath = task.firebaseReference
                    taskFirebasePath?.updateChildValues([
                        kTaskTitleKey: title,
                        kTaskUserKey: email,
                        kTaskCompletedKey: completed
                        ])
                }
            } else {
                /* This task is new,
                 it should be created in Firebase
                */
                if let title = taskViewController.taskTitle,
                    let completed = taskViewController.taskCompleted,
                    let email = self.currentUser?.email {

                    let task = Task(title: title, user: email, completed: completed)
                    let taskFirebasePath = self.tasksReference.ref.child(title.lowercased())
                    taskFirebasePath.setValue(task.toDictionary())
                }
            }
        }
        self.selectedTask = nil
    }

    @IBAction func addTask() {
        self.performSegue(withIdentifier: kTaskViewControllerSegueIdentifier, sender: self)
    }

    // MARK: - Table view data source

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.tasks.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TasksTableViewCell", for: indexPath)

        if let c = cell as? TasksTableViewCell {
            let task = self.tasks[indexPath.row]
            c.titleLabel.text = task.title
            c.completedSwitch.setOn(task.completed, animated: true)
        }
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let task = self.tasks[indexPath.row]

        /* Keep a reference to the task that is currently edited.
        */
        self.selectedTask = task
        self.performSegue(withIdentifier: kTaskViewControllerSegueIdentifier, sender: self)
    }

    override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }

    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            let task = self.tasks[indexPath.row]
            /* Delete this task.
             No tableview updates should be done here because a callback notification
             will be provided.
            */
            task.firebaseReference?.removeValue()
        }
    }
}

[collapse]

Persistence

Firebase has an option for persistence, which is a major prize for us developers. It doesn’t save data between app restarts, but saves changes in memory when the connection to the servers is unstable. This is compassionate for us because we don’t need to spend vast amounts of time to think and implement good solutions to keep our app stable. However, for persistence across restarts when the connection is offline, custom solutions are required.

To turn on persistence, add the following line in AppDelegate:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    ...

    FIRDatabase.database().persistenceEnabled = true
}

Take a look at our example project

  1. Download the finished project: Task Burner
  2. Drag and drop your GoogleService-Info.plist generated from Firebase Console. (See Step 3)
  3. Replace the bundle id so you can follow the changes in your own firebase console.

The largest parts of the project and their functions:

  • Task.swift contains the model object
  • MainViewController is responsible for signup and login.
  • TasksTableViewController shows the list of tasks and triggers actions with them.
  • TaskViewController is used to create a new task or edit an existing one. When you feel confident enough, have a stab at our exercises below.

Conclusion

This tutorial covered the basic authentication and database aspects of Firebase. These are the basic notions to use in any app that relies on Firebase. Firebase itself is a modern, out of the box service for application developers. It’s free for 100 simultaneous connections and it’s affordable as your apps become App Store hits.

Other Firebase features are:

  • Firebase Cloud Messaging (FCM) is a cross-platform messaging solution that lets you reliably deliver messages at no cost.
  • Firebase Analytics is a free app measurement solution that provides insight on app usage and user engagement.
  • Firebase Storage is built for app developers who need to store and serve user-generated content, such as photos or videos.
  • Firebase Hosting provides fast and secure static hosting for your web app.
  • Firebase Test Lab: Test your app on devices hosted in a Google datacenter.
  • Firebase Crash Reporting: Comprehensive and actionable information to help diagnose and fix problems in your app.
  • Firebase Notifications is a free service that enables targeted user notifications for mobile app developers.
  • Firebase Remote Config: Change the behaviour and appearance of your app without publishing an app update.
  • Firebase App Indexing gets your app into Google Search.
  • Firebase Dynamic Links are deep links that survive the install process, which can convert your mobile web users to native app users, and increase conversion for user-to-user sharing.
  • Firebase Invites are an out-of-the-box solution for app referrals and sharing via email or SMS.
  • Google AdWords: Reach potential customers with online ads.
  • AdMob by Google is an easy way to monetize mobile apps with targeted, in-app advertising.

Read more on Firebase Docs.

Exercises

To further develop your new Firebase skills try implementing the following:

1. Add property for the task to keep track of importance and show it in the table cell and edit screen.

2. Notice in the TasksTableViewController that the tasks ordered query doesn’t take into account the task’s user property. Filter them out to show only the current user’s tasks.

3. Updating tasks on the firebase console while the app is showing the TaskViewController doesn’t reflect changes in the app. Update the TaskViewController to respond to remote changes.

Thanks for reading!

Andrei Nagy

Thank you for taking the time to read my first tutorial!

My name is Andrei Nagy and I’ve been a iOS developer for the past 4 years. In 2015 I founded Logic Pods a small iOS development company located in Cluj Napoca.
We focus on quality, responsiveness and continuous improvement in order to make beautiful apps for our customers.

I hope this tutorial will help you get started with Firebase. Feel free to ask any questions you might have or send feedback in the comments section below.

If you enjoyed this tutorial please take a few seconds and share it with your friends! :)

  10 comments for “Firebase 101

  1. November 10, 2016 at 1:34 pm

    Great job Andrei!

    Enumerations are also good for modeling constants. And an enum with a raw type of String will automatically assign the case name as the raw value if you don’t manually assign it.

    Also, have you checked out RxSwift? I think of RxSwift as a natural extension of Firebase (and other socket-based services), used together to create completely reactive apps, from device, to cloud, and back.

    • November 10, 2016 at 2:47 pm

      Hi Scott,

      Enums are indeed a sound way to manage constants in apps. The choice here was to keep it easy to follow for learners.

      RxSwift can be the subject of it’s own tutorial :)

      Thank you for your interest in my tutorial!

  2. Najir
    November 12, 2016 at 2:23 pm

    Excellent tutorial Andrei. I plan on using it for a iOS course I am teaching in the college.

  3. Marcus
    November 14, 2016 at 10:50 am

    Excellent tutorial Andrei. Congratulations

  4. Polk
    November 17, 2016 at 3:29 am

    There is a typo where Let constant starts with capital L.

  5. John James
    December 29, 2016 at 4:01 pm

    Good explanation of the basics but does not address sharing private data

    The Table has a variable for an array of friends but the app does not use the array.

    All of the tutorials on firebase I have seen do not address the issue of how to restrict the data to an invited group of friends. All todo type apps need data to be restricted to invited friends. How this issue is addressed is very poorly documented in Firebase.

    • January 5, 2017 at 7:47 am

      Hi John,

      Thank you so much for your feedback. The post is more of a primer to get started and basic notions are explained.

      About restricting data, you can check out the firebase security docs. A quick and straight forward implementation would be to have tasks per user and a user to have a “shared tasks” array where invited tasks are stored to. But this depends on the way you want your app to work, are tasks owned forever or do they get transferred at some point.

  6. jude
    January 16, 2017 at 6:50 pm

    Probably one of the most thoughtful tutorial I have seen in terms of its organization, its easy step through
    at a glance. This is well appreciated.

Leave a Reply

Your email address will not be published. Required fields are marked *

Subscribe
We send about one email per week with our latest tutorials and updates
Never display this again :)