Mobile - - By Joyce Echessa

Creating a Backend for Your iOS App Using Firebase

Firebase is a Backend as a Service platform that can help you quickly develop and deploy your application. It provides a myriad of features including Realtime Database, Authentication (with email and password, Facebook, Twitter, GitHub and Google), Cloud Messaging, Storage, Hosting, Remote Config, Test Lab, Crash Reporting, Notification, App Indexing, Dynamic Links, Invites, AdWords and AdMob.

In this article, we’ll create a simple To Do app that will show how to save and retrieve data from Firebase, how to authenticate users, set read/write permissions on the data and validate the data on the server.

Getting Started

To get started, first download the repository that contains the starter project that we’ll use in the tutorial and then head over to Firebase and create an account if you don’t already have one.

When you run the downloaded project, you will see a Login view.

Login View

The Login view has a Register button that takes you to the Sign Up view when tapped; and on this view, there is a Sign Up form as well as a Login button that takes you back to the Login view.

Sign Up View

To add the Firebase libraries to the project, we’ll use CocoaPods. First make sure that you have CocoaPods installed on your computer.

Open Terminal and navigate to the root of the downloaded project with cd Path/To/ToDo\ App (the \ escapes the whitespace in the directory name).

Create a Podfile with the following command.

pod init

Then open the Podfile with:

open -a Xcode Podfile

Modify the file’s content as shown below.

platform :ios, '10.0'

target 'ToDo App' do
  use_frameworks!

  pod 'Firebase/Auth'
  pod 'Firebase/Database'

end

The Firebase library consists of different subspecs. In the above, we include the /Auth subspec which is used for authentication and the /Database subspec that is needed to work with the Firebase realtime database. The /Core subspec is required for your app to work with Firebase, but we don’t have to include it in the Podfile because the subspecs we added depend on it. Therefore, when we run pod install to fetch the project’s dependencies, the /Core library will be fetched as well.

Run pod install to fetch the project’s dependencies. After installation is complete, close the Xcode project and open ToDo App.xcworkspace.

Next, head over to the Firebase console and click on the Create a New Project button. A dialog window will pop up for you to input the project’s details. Enter a name and country/region for the project.

Create Firebase Project

The country/region represents the country/region of your organisation/company. Your selection also sets the appropriate currency for your revenue reporting. After setting a name (I used ToDoApp) and region for the project, click on the Create Project button. A project will be created and its console opened. From the project’s console, click on the Add Firebase to your iOS App option. Then enter your iOS project data in the window that pops up.

Add Firebase to iOS App

We’ll leave the App Store ID field blank, but if you are integrating Firebase to an app that is on the App Store, you can find the ID in your app’s URL. In the example below, 123456789 is the App Store ID. https://itunes.apple.com/gb/app/yourapp/id123456789

Click on the Add App button and a GoogleService-Info.plist file will be downloaded to your machine. Move the file into the root of your Xcode project and add it to all targets.

The plist file contains configuration settings that the iOS app needs to communicate with the Firebase servers. It contains such things as the URL to the Firebase project, the API key, e.t.c. In the previous version of Firebase, you had to store these manually in your app’s code, but now the process has been simplified with the use of one file that contains the needed data.

If you are using versioning and storing your code in a public repository, you should consider not making the GoogleService-Info.plist publicly accessible. If it’s used beyond its limits, it will stop working and if you are on a paid plan, you wouldn’t want anyone abusing that and running up your costs.

On the Firebase console, press Continue to move forward in the project setup.

Add Firebase to iOS App

We’ve already done the above steps, so click on Continue.

Follow the instructions on the last page of the dialog and add the Firebase initialisation code to your AppDelegate class. This connects to Firebase when the app starts.

Add Firebase to iOS App

Add the following import to AppDelegate.swift

import Firebase

Then add the following to application(_: didFinishLaunchingWithOptions:), before the return statement.

FIRApp.configure()

On the Firebase console, click on Finish to complete the project setup.

Security and Rules

Before retrieving and saving data from the Firebase server, we’ll set up authentication and add rules that restrict access to data and validate user input before it’s saved.

Authentication

The Firebase API enables you to setup email/password, Facebook, Twitter, GitHub, Google and anonymous authentication. In our app, we’ll use email/password authentication.

To enable email/password authentication, select Authentication from the left panel of the Firebase console and navigate to the Sign-In Method tab. Enable the Email/Password authentication provider and click on Save.

Enable Email Password Authentication

Back in Xcode, modify LoginViewController.swift as shown.

import UIKit
import FirebaseAuth

class LoginViewController: UIViewController {

    @IBOutlet weak var emailField: UITextField!
    @IBOutlet weak var passwordField: UITextField!

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

        if let _ = FIRAuth.auth()?.currentUser {
            self.signIn()
        }
    }

    @IBAction func didTapSignIn(_ sender: UIButton) {
        let email = emailField.text
        let password = passwordField.text
        FIRAuth.auth()?.signIn(withEmail: email!, password: password!, completion: { (user, error) in
            guard let _ = user else {
                if let error = error {
                    if let errCode = FIRAuthErrorCode(rawValue: error._code) {
                        switch errCode {
                        case .errorCodeUserNotFound:
                            self.showAlert("User account not found. Try registering")
                        case .errorCodeWrongPassword:
                            self.showAlert("Incorrect username/password combination")
                        default:
                            self.showAlert("Error: \(error.localizedDescription)")
                        }
                    }
                    return
                }
                assertionFailure("user and error are nil")
            }

            self.signIn()
        })
    }

    @IBAction func didRequestPasswordReset(_ sender: UIButton) {
        let prompt = UIAlertController(title: "To Do App", message: "Email:", preferredStyle: .alert)
        let okAction = UIAlertAction(title: "OK", style: .default) { (action) in
            let userInput = prompt.textFields![0].text
            if (userInput!.isEmpty) {
                return
            }
            FIRAuth.auth()?.sendPasswordReset(withEmail: userInput!, completion: { (error) in
                if let error = error {
                    if let errCode = FIRAuthErrorCode(rawValue: error._code) {
                        switch errCode {
                        case .errorCodeUserNotFound:
                            DispatchQueue.main.async {
                                self.showAlert("User account not found. Try registering")
                            }
                        default:
                            DispatchQueue.main.async {
                                self.showAlert("Error: \(error.localizedDescription)")
                            }
                        }
                    }
                    return
                } else {
                    DispatchQueue.main.async {
                        self.showAlert("You'll receive an email shortly to reset your password.")
                    }
                }
            })
        }
        prompt.addTextField(configurationHandler: nil)
        prompt.addAction(okAction)
        present(prompt, animated: true, completion: nil)
    }

    func showAlert(_ message: String) {
        let alertController = UIAlertController(title: "To Do App", message: message, preferredStyle: UIAlertControllerStyle.alert)
        alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.default,handler: nil))
        self.present(alertController, animated: true, completion: nil)
    }

    func signIn() {
        performSegue(withIdentifier: "SignInFromLogin", sender: nil)
    }

}

In the above code, we first check whether the user is signed in in viewDidAppear(). FIRAuth.auth()?.currentUser gives the authenticated user (if any). The user is represented by a FIRUser object. If they are signed in, we call signIn() which performs a segue that is already created in the storyboard file. This segue navigates to the ItemsTableViewController which is where our list of items will be shown.

didTapSignIn() sends the user’s input to Firebase for authentication. FIRAuth.auth()?.signIn(withEmail: password: completion:) is used to sign in a user using the email and password combination. Each of the authentication providers (e.g. Google, Facebook, Twitter, Github, e.t.c) uses a different method call.

When the user is successfully authenticated, signIn() is called, otherwise an error message will be shown to the user. If authentication fails, an NSError object is returned back from the server. It carries an error code that you can use to determine the cause of the error and give the user an appropriate error message. In the above code, we give different error messages when the user enters a non existent account or when they enter the wrong password. For other errors, we show the user a description of the error with error.localizedDescription. In a real app, you might not want to show this to the user. The message returned by error.localizedDescription is best suited for the developer when debugging, but for your users, you should use a better catch-all error message.

didRequestPasswordReset() is called when the Forgot Password button is tapped. Here, we create an alert box that the user can use to enter an email address where the Password Reset email will be sent. This is another of the many advantages offered by Firebase. Password reset functionality has already been set up; you don’t have to code it up yourself.

If you look at the Firebase control under Authentication > Email templates, you can modify the email message sent to users for Email address verification, Password reset and Email address change.

Email Templates

Note that in the code above, the alert is called in the main thread by using DispatchQueue.main.async and not in the background thread that handles the callback function. Any code that updates the app’s UI should be run in the main queue.

To test the password reset functionality, you can create a User on the Firebase console under Authentication > Users > Add User. Enter an email and password for the User (use a real email if you want to receive the password reset email).

Back in Xcode, run the project. You should be able to enter an email and have the password reset email sent to it.

Reset Password

Reset Password

With the Login functionality done, let us now add the Sign Up functionality. Modify SignUpViewController as shown.

import UIKit
import FirebaseAuth

class SignUpViewController: UIViewController {

    @IBOutlet weak var emailField: UITextField!
    @IBOutlet weak var passwordField: UITextField!

    @IBAction func didTapSignUp(_ sender: UIButton) {
        let email = emailField.text
        let password = passwordField.text
        FIRAuth.auth()?.createUser(withEmail: email!, password: password!, completion: { (user, error) in
            if let error = error {
                if let errCode = FIRAuthErrorCode(rawValue: error._code) {
                    switch errCode {
                    case .errorCodeInvalidEmail:
                        self.showAlert("Enter a valid email.")
                    case .errorCodeEmailAlreadyInUse:
                        self.showAlert("Email already in use.")
                    default:
                        self.showAlert("Error: \(error.localizedDescription)")
                    }
                }
                return
            }
            self.signIn()
        })
    }

    @IBAction func didTapBackToLogin(_ sender: UIButton) {
        self.dismiss(animated: true, completion: {})
    }

    func showAlert(_ message: String) {
        let alertController = UIAlertController(title: "To Do App", message: message, preferredStyle: UIAlertControllerStyle.alert)
        alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.default,handler: nil))
        self.present(alertController, animated: true, completion: nil)
    }

    func signIn() {
        performSegue(withIdentifier: "SignInFromSignUp", sender: nil)
    }

}

In the above code, when the user taps on the Create Account button didTapSignUp() is called. Here, we take the user’s input and call FIRAuth.auth()?.createUser(withEmail: password: completion:) to try and create an account with the entered details. If signup fails, an error message is shown, otherwise signIn() is called which performs a segue to the ItemsTableViewController. This segue had already been created in the storyboard file of the starter project.

Run the app. You should be able to create an account and confirm its creation on the Firebase console. If you had previously logged in, and thus can’t get to the Login view, you can reset the simulator with Simulator > Reset Content and Settings.

By default, Firebase sets a rule on the accepted length of the password – it must be at least 6 characters long. You can set more rules on this, on the Firebase Console. Shortly, we’ll see how to set up rules. More on this here.

Authorization and Data Validation

The Firebase Realtime Database provides an expression-based rules language with JavaScript-like syntax to easily define how your data should be structured, how it should be indexed, and when your data can be read from and written to. Combined with our authentication services, you can define who has access to what data and protect your users’ personal information from unauthorized access.

We’ve set up authentication for our project, but we can make users data even more secure by defining some rules for it.

By default, Firebase has the following rules viewable by navigating to Database > Rules on the console.

{
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null"
  }
}

The above security rules require users to be authenticated to read or write any data to the database. Modify the rules as shown and hit Publish.

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "auth != null && auth.uid == $uid",
        ".write": "auth != null && auth.uid == $uid",
        "items": {
          "$item_id": {
            "title": {
              ".validate": "newData.isString() && newData.val().length > 0"
            }
          }
        }
      }
    }
  }
}

In the above, auth != null && auth.uid == $uid has been set on the read and write permissions. With this rule, not only will the user need to be authenticated to read or write any data to the database, but they will also only have access to their own data.

Firebase stores data in JSON format. In our database, each user will have an array of to-do items named items. Each item will have a title. In the above, we add some validation that ensures that an item with an empty title won’t be saved.

Saving Data

With authentication and authorization done, we’ll now see how data can be saved and retrieved to and from Firebase.

First create a new file called Item.swift and modify it as shown. This will be the Item model class Each Item will have a title and ref which will hold a FIRDatabaseReference object. A FIRDatabaseReference represents a particular location in your Firebase Database and can be used for reading or writing data to that Firebase Database location.

import Foundation
import FirebaseDatabase

class Item {

    var ref: FIRDatabaseReference?
    var title: String?

    init (snapshot: FIRDataSnapshot) {
        ref = snapshot.ref

        let data = snapshot.value as! Dictionary<String, String>
        title = data["title"]! as String
    }

}

Then modify ItemsTableViewController as shown.

import UIKit
import Firebase

class ItemsTableViewController: UITableViewController {

    var user: FIRUser!
    var items = [Item]()
    var ref: FIRDatabaseReference!
    private var databaseHandle: FIRDatabaseHandle!

    override func viewDidLoad() {
        super.viewDidLoad()

        user = FIRAuth.auth()?.currentUser
        ref = FIRDatabase.database().reference()
        startObservingDatabase()
    }

    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

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

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        let item = items[indexPath.row]
        cell.textLabel?.text = item.title
        return cell
    }

    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            let item = items[indexPath.row]
            item.ref?.removeValue()
        }
    }

    @IBAction func didTapSignOut(_ sender: UIBarButtonItem) {
        do {
            try FIRAuth.auth()?.signOut()
            performSegue(withIdentifier: "SignOut", sender: nil)
        } catch let error {
            assertionFailure("Error signing out: \(error)")
        }
    }

    @IBAction func didTapAddItem(_ sender: UIBarButtonItem) {
        let prompt = UIAlertController(title: "To Do App", message: "To Do Item", preferredStyle: .alert)
        let okAction = UIAlertAction(title: "OK", style: .default) { (action) in
            let userInput = prompt.textFields![0].text
            if (userInput!.isEmpty) {
                return
            }
            self.ref.child("users").child(self.user.uid).child("items").childByAutoId().child("title").setValue(userInput)
        }
        prompt.addTextField(configurationHandler: nil)
        prompt.addAction(okAction)
        present(prompt, animated: true, completion: nil);

    }

    func startObservingDatabase () {
        databaseHandle = ref.child("users/\(self.user.uid)/items").observe(.value, with: { (snapshot) in
            var newItems = [Item]()

            for itemSnapShot in snapshot.children {
                let item = Item(snapshot: itemSnapShot as! FIRDataSnapshot)
                newItems.append(item)
            }

            self.items = newItems
            self.tableView.reloadData()

        })
    }

    deinit {
        ref.child("users/\(self.user.uid)/items").removeObserver(withHandle: databaseHandle)
    }
}

In the above, we start off by instantiating some variables in viewDidLoad(). We set user with the value of the logged in user and then set ref with a FIRDatabaseReference object. FIRDatabase.database().reference() gets a FIRDatabaseReference for the root of your Firebase Database. We then call startObservingDatabase().

In startObservingDatabase() we set a listener for any changes to the database. Firebase data is retrieved by attaching an asynchronous listener to a FIRDatabase reference. The listener is triggered once for the initial state of the data and again anytime the data changes. To add an event listener, we use the observeEventType() method to specify an event type and callback block. You can listen for the following types of events:

  • FIRDataEventTypeValue – Read and listen for changes to the entire contents of a path.
  • FIRDataEventTypeChildAdded – Retrieve lists of items or listen for additions to a list of items. Suggested use with FIRDataEventTypeChildChanged and FIRDataEventTypeChildRemoved to monitor changes to lists.
  • FIRDataEventTypeChildChanged – Listen for changes to the items in a list. Use with FIRDataEventTypeChildAdded and FIRDataEventTypeChildRemoved to monitor changes to lists.
  • FIRDataEventTypeChildRemoved – Listen for items being removed from a list. Use with FIRDataEventTypeChildAdded and FIRDataEventTypeChildChanged to monitor changes to lists.
  • FIRDataEventTypeChildMoved – Listen for changes to the order of items in an ordered list. FIRDataEventTypeChildMoved events always follow the FIRDataEventTypeChildChanged event that caused the item’s order to change (based on your current order-by method).

We listen for the FIRDataEventTypeValue event. You use the FIRDataEventTypeValue event to read the data at a given path, as it exists at the time of the event. This method is triggered once when the listener is attached and again every time the data, including any children, changes. The event callback is passed a snapshot containing all data at that location, including child data. If there is no data, the value of the snapshot returned is nil.

Note that the FIRDataEventTypeValue event is fired every time data is changed at the specified database reference, including changes to children. To limit the size of your snapshots, you should attach only at the highest level needed for watching changes. For example, attaching a listener to the root of your database is not recommended. In our code, we attach the listener to /users/{user id}/items which is the path that will hold a user’s items.

The listener receives a FIRDataSnapshot that contains the data at the specified location in the database at the time of the event in its value property. If no data exists at the location, the value is nil.

From the snapshot, we create Item objects set them to the items array that is used to populate the table view. We then reload the table view to reflect the data change.

numberOfSectionsInTableView(), tableView(_: numberOfRowsInSection:) and tableView(_: cellForRowAtIndexPath) are the usual table view functions used to set the table view’s data.

The app contains an Add button in its navigation bar, which when tapped, calls didTapAddItem(). Here, we show the user an Alert box that they can use to add an item to the table view. We save the value added by the user to /users/{user id}/items/{item id}/title/.

There are four methods for writing data to the Firebase Realtime Database:

  • setValue – Write or replace data to a defined path, such as users/<user-id>/<username>.
  • childByAutoId – Add to a list of data. Every time you call childByAutoId, Firebase generates a unique key that can also be used as a unique identifier, such as user-posts/<user-id>/<unique-post-id>.
  • updateChildValues – Update some of the keys for a defined path without replacing all of the data.
  • runTransactionBlock – Update complex data that could be corrupted by concurrent updates.

The child() function grabs the reference of a particular node if it exists or creates it if it doesn’t exist. We use childByAutoId() to set a unique ID to every item that will be created. We then use setValue() to add the user’s input as the value for item’s title.

tableView(_: commitEditingStyle: forRowAtIndexPath:) makes the table view editable. With this, the user will be able to swipe on an item to delete it. We call removeValue() with a reference to the swiped on item. This removes the data at that location in the database. After deleting an item, we don’t have to make any changes to the table view. Remember that we set a listener to any changes made on the ../items/ path on the database, so the callback we set for this will be responsible for updating the table.

didTapSignOut() is called when the Sign Out button on the app’s navigation bar is tapped. Here we sign out the user and perform a segue back to the Login screen.

Run the app and you should be able to add an item.

Add Item

Any item you add will be added to the table view.

Item List

Check the Firebase Console, and you will see the added data.

Item List on Firebase Console

To delete an item, swipe on it to reveal a Delete button.

Delete Item

That brings us to the end of the tutorial. We’ve seen how to save and retrieve data to and from the Firebase realtime database, and how to set up authentication and data authorization. The data saving we’ve looked at is ideal for simple data types such as NSString, NSNumber, NSArray and NSDictionary. If you want to save files like images or documents to Firebase, then look into Firebase Storage.

The tutorial was a quick intro to using Firebase, it was not an exhaustive look into all that Firebase does. For this, be sure to read through the documentation. You can download the completed project here. Remember to add the GoogleService-Info.plist file generated from Firebase to the project.

Note

You might have noticed some warnings in your project after adding the Firebase libraries, or on running the completed project if you downloaded it. The warning error message states Conflicting nullability specifier on return types, 'nullable' conflicts with existing specifier 'nonnull'. This little bug came with the update of Firebase to Swift 3 and the issue has been filed. If you look at the message threads in that last link, the issue has been fixed, but changes won’t be seen in Firebase until the next release. The app still works despite this.

Sponsors