Firebase Real-Time Database Tutorial for iOS

Learn how to use Firebase Real-Time Database to seamlessly store and fetch data in real time, while supporting offline mode and secure access. By Yusuf Tör.

See course reviews 4.3 (3) · 2 Reviews

Download materials
Save for later
Share

Learn how to use Firebase Real-Time Database to seamlessly store and fetch data in real time, while supporting offline mode and secure access.

When developing an app, you might need to store data in a database. Additionally, you’ll probably need an API to interact with it. This can become complex and distract you from your main goal of producing a killer app. Fortunately, the Firebase Real-Time Database has you covered. This database supports storing and fetching data seamlessly as well as listening for data in real-time. On top of this, the Firebase Real-Time Database handles authenticated flows to protect your data. You also can opt into local storage to store user data when the device is offline and sync it up to the database when the device returns online.

In this tutorial, you’ll build a journaling app that logs a daily thought and syncs it with the Real-Time Database. Along the way, you’ll learn:

  • How to implement authentication with Firebase.
  • How to protect data access with security rules.
  • The differences between the Real-Time Database and Firestore.
  • How to implement, listen to and store data with the Real-Time Database.
  • How to persist data offline.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

Once downloaded, open DailyThoughts.xcodeproj from the starter folder and look around. You’ll see the code split into Sign Up and Journal folders. App users have to sign up before they can post in the journal. Additionally, data is segmented on a per-user basis because you don’t want others stealing the genius thoughts and ideas you store in the database.

Before you run the app, you’ll need to configure Firebase.

Creating a Firebase Account and Project

If you’re new to Firebase, you’ll need to create an account. Don’t worry! It’s easy and free.

Head to the Firebase signup site and create an account. Then, click the Create a project button on the Welcome to Firebase screen. Alternatively, if you already have a Firebase account, click Add project instead. Next, type the project name DailyThoughts into text field. If new to Firebase, click the box to accept the Firebase terms. Then, click the Continue button. On the next screen, disable support for Google Analytics because it isn’t needed for this tutorial. Now, click the Create project button. If successful, you’ll see a screen informing you that your new project is ready. Finally, press the Continue button.

Note: For a detailed walk-through on setting up Firebase, see the Getting Started with Firebase Tutorial.

Return to Xcode. Click the app name in the Project navigator pane and again under Targets. Now, click the General tab and change the Bundle Identifier to any value you like. Finally, click the Signing & Capabilities tab and select a Team from the pull-down menu.

Linking the Firebase Database to Your App

Having set up the project, return to the Firebase console. Click the Project Overview home button in the left navigation menu and then the iOS+ button under the text “Get started by adding Firebase to your app”:

Add Firebase to iOS app

After clicking the button, you’ll see instructions to add Firebase to your iOS app:

Register iOS app with Firebase

Next, enter the app’s bundle ID (the one you chose earlier in Xcode) and name (DailyThoughts) into the form and click the Register app button.

Download and add GoogleService-Info.plist to your project under the Supporting Files group as shown in the Firebase instructions. This file contains the configuration information you need to integrate Firebase with your app:

Download config file

Note: Do only steps one and two of the instructions. The rest is already done in the starter project.

Now that you’ve completed some preliminary steps, switch back to Xcode. Build and run the app. You’ll see the signup screen for your journaling app:

Daily Thoughts signup screen

Fill in the email address and password fields and tap Sign Up. Nothing happened! That’s because you need to set up Firebase Authentication.

Setting Up Authentication With Firebase

If the user’s identity is important in your app, you’ll need to implement authentication before allowing access to sensitive data. Fortunately, Firebase provides an Authentication SDK supporting a seamless setup. In addition, Firebase supports authentication via several methods:

  • Email and password: Authenticates with an email and password provided by the user.
  • Phone Number: Authenticates by sending SMS messages to the user’s phone.
  • Federated Identity Provider: Authenticates via popular social media services such as Facebook or Twitter as well as Sign In with Apple and Google.
  • Anonymous Authentication: Authenticates by creating a temporary account for the user’s device without the user having to specifically sign up.
  • Custom Authentication: Connects an app’s existing sign-in system to the Firebase Authentication SDK to gain access to Firebase’s services.

Each has its advantages, and it’s typical for an app to offer a few of these methods for users to sign up. For this app, you’ll use email and password authentication.

To set this up, open the Firebase console (and, if necessary, click the DailyThoughts tile if a list of projects is displayed), select Authentication on the left and click Get Started:

Authentication tab on Firebase

Then, select Email/Password. Toggle Enable and click Save:

Enable email/password authentication

That’s all you need to do on the Firebase console side to set up authentication! Easy, wasn’t it? If you build and run now, though, you still can’t sign up. That’s because you need to make some modifications on the client-side (i.e., your DailyThoughts app).

Authenticating on the Client

In Xcode, open AuthenticationModel.swift and add the following code to signUp(emailAddress:password:):

Auth.auth().createUser(withEmail: emailAddress, password: password)

Then, add the following code to signIn(emailAddress:password:):

Auth.auth().signIn(withEmail: emailAddress, password: password)

These methods asynchronously sign up and sign in to Firebase using the user’s provided credentials.

Note: You need to use a properly formatted email, and a password that’s at least 6 characters long. For better error handling, you want to implement the completion handlers of createUser(withEmail:, password:, completion:) and signIn(withEmail:, password:, completion:).

In signOut(), add the following:

try? Auth.auth().signOut()

This signs out the current user.

Next, add the following code to listenToAuthState():

Auth.auth().addStateDidChangeListener { [weak self] _, user in
  guard let self = self else {
    return
  }
  self.user = user
}

This listens to the user’s authentication state. When the state changes, it receives a callback with the Firebase user object, which may or may not be nil. Upon successful authentication, the user object will exist. This causes ContentView to update to show JournalListView as its root view.

Build and run the app. Then fill out the Email Address and Password fields and tap Sign Up again:

The journal list view

You’ve now signed up! :]

With the signup phase completed, the app will display a new view with all the thoughts posted to your database. It’s looking a bit empty, so tap the + button in the bottom right corner to bring up the ThoughtView, then type a thought:

Thought composition view

Once you’ve finished composing your deep thought, tap the Post button at the top right of the screen. The view dismisses. Unfortunately, nothing has changed. That’s because you need to set up the Firebase Real-Time Database.

Choosing a Firebase Database

Firebase comes with two NoSQL JSON, cloud-hosted databases: Firestore and Real-Time Database. Initially, Firebase had only the Real-Time Database: an efficient, low-latency database that stores data in one big JSON tree. However, this wasn’t the best solution for all use cases. So the Firebase team introduced a new database with a different data model called Firestore. Each database has its strengths and weaknesses.

Real-Time Database

Strengths

  • Supports user presence to monitor when a user is online or offline.
  • Has extremely low latency.
  • Charges for bandwidth and storage but not for operations performed in the database.
  • Scales to 200k concurrent connections.

Weaknesses

  • Has no multi-region support. Data is available in regional configurations only.
  • Has limited sorting and filtering functionality.

Firestore Database

Strengths

  • More structured than the Real-Time Database and can perform more complex queries on the data.
  • Designed to scale better than the Real-Time Database. The scaling limit is currently around 1 million concurrent connections.
  • Has multiple data centers storing data in distinct regions and can support multi-regional configurations.
  • Charges primarily on operations performed in the database and, at a lower rate, bandwidth and storage.

Weaknesses

  • Doesn’t allow documents to update at a rate greater than once per second.
  • Doesn’t support user presence.

For this tutorial, you’ll use the Real-Time Database as your database. But in a production app, you might choose to use Firestore to reduce storage costs. Another option is to use both the Firestore and Real-time databases in your app. For more information about these databases, look at Firebase’s documentation.

Now that you know a little about the Real-Time Database, it’s time to learn about the structure of the data you’ll store in it.

Firebase Data Structure

The Real-Time Database is a NoSQL JSON data store, but what is that? Essentially, the data in the Real-Time Database is one large JSON tree. When you add data to this tree, it becomes a node in the existing data structure with an associated identifier or key.

Here’s a sample of how your data could look as a JSON object:

{
  "users" : {
    "5CbvKMuKArWEAFYiuTEOrwI4dxY2" : { // user key
      "thoughts": {
        "-ModIUirN3Irg68qk4Zt" : { // thought key
          "text" : "I like Ray Wenderlich tutorials!"
        },
        "-ModJWbp4o70cZ4Hvu5x" : { // thought key
          "text" : "No wait, I LOVE Ray Wenderlich tutorials!!"
        }
      }
    },
    "j9ksKJ70IbAg52nvj7M8xbb8TsQa" : { // user key
      "thoughts": {
        "-LopSJuhwbN8s4abNS8z" : { // thought key
          "text" : "I wonder what others are writing on this app"
        },
        "-LopNlkA3fV6jau8i0a1" : { // thought key
          "text" : "Apparently smiley faces are square :]"
        }
      }
    }
  }
}

The code above has a main JSON object with a single key called users. The users value contains a list of user objects. These user objects have a user key corresponding to the ID of the user who is writing the thoughts. Each user object contains a thoughts key. The thoughts value contains a list of thought objects having a single text field. This field contains the thought the user wrote in the app. This way, the data is segmented by user ID, so different users’ thoughts are maintained separately.

As you can see, this data structure is a composition of nested objects. Although the Real-Time Database supports nesting up to 32 levels deep, this isn’t recommended. It’s always best to keep the structure as flat as possible because fetching data at a specific node also fetches all its child nodes. This can quickly lead to huge bandwidth usage, which can be expensive!

The data structure looks good. Accordingly, the next step is to set up the Real-Time Database!

Setting Up Firebase’s Real-Time Database

In the Firebase console, select Realtime Database on the left and click Create Database:

Setting up the Realtime Database

Choose a location for your database and click Next. For the best performance, the location should reflect wherever you expect the majority of your users to be:

Choosing a database location

Leave the security rules set to Start in locked mode and click Enable:

Setting security rules for the database

Configuring Database Rules

At this point, you see an empty database in front of you. Before you start filling it with data, configure the database rules. Click the Rules tab at the top:

Selecting the database rules tab

Firebase’s Security Rules are there to determine who has read-and-write access to your database, the structure of your data and what indexes exist. These rules automatically apply when you try to interact with the database from the client. By default, your rules look like this:

{
  "rules": {
    ".read": false,
    ".write": false
  }
}

This javascript-like syntax means reading and writing to the database isn’t allowed, so you’ll need to change that!

Four types of rules exist:

  • .read: Describes the permission for reading data.
  • .write: Describes the permission for writing data.
  • .validate: Defines what a correctly formatted value will look like, whether it has child attributes and the data type.
  • .indexOn: Specifies a child to index to support ordering and querying.

Customizing the Rules

Think back to the structure of the data you plan to hold in the database. It makes sense that your app’s users should have access only to the data they post to the database. Fortunately, Firebase’s security rules let you do just that.

Replace the default rules with the following:

{
  "rules": {
    "users": { // 1
      "$uid": { // 2
        "thoughts": { // 3
          ".read": "auth != null && auth.uid == $uid", // 4
          ".write": "auth != null && auth.uid == $uid"
        }
      }
    }
  }
}

Then, click Publish.

You read the above rules as acting on the path /users/$uid/thoughts. Here’s what’s happening in detail:

  1. This makes sure the rules apply to data inside the users object.
  2. The $ variable captures a portion of the data path and stores it in a variable of the defined name. In this instance, it captures the value of the key of a user object — i.e., the user ID — and stores it in a variable named $uid.
  3. This applies the rules to the data inside the thoughts object.
  4. Firebase specifies predefined variables for use within the rules. Here, it uses the predefined auth variable holding the authentication data for the client requesting data. It checks whether this exists to verify the client’s authentication. It also checks whether the user ID of the authenticated user equals the user ID in the path of the requested data. This means the user is accessing their own data and so Firebase allows them to read it. The same applies when writing to the path.

Test your new rules by selecting the Rules Playground:

The rules playground

Make sure Simulation type shows set. This will simulate a write on a specified location.

In the Location field, paste the following:

/users/5CbvKMuKArWEAFYiuTEOrwI4dxY2/thoughts/-MohverU9Gvs4Z1uaE-z

Move the Authenticated toggle to the on position and set UID to

5CbvKMuKArWEAFYiuTEOrwI4dxY2

Now, click Run.

You’ll see a green notice at the top saying Simulated set allowed and a green check mark next to the .write row in your rules. This means writing to that location in real life via your client will work:

Successfully simulated write to the database

Now, test the read. Change the Simulation type to read and hit Run again:

Successfully simulated read

Huzzah! Your rules are working as expected. Now, it’s time to integrate the database into the client.

Posting Data to the Real-Time Database

Because you’ve added the Real-Time Database to your Firebase account, your GoogleService-Info.plist will have updated with your database URL. You’ll need to re-download it.

Return to the Firebase console. Click the cog wheel next to Project Overview in the top left and select Project settings:

Project settings

Scroll down to Your apps and click the download button for GoogleService-Info.plist:

Downloading the Google plist file

In Xcode, delete the old GoogleService-Info.plist file and drag and drop the new one into your project.

Open JournalModelController.swift. Under import SwiftUI, add:

import FirebaseAuth
import FirebaseDatabase

Then, add the following variables below the newThoughtText variable:

private lazy var databasePath: DatabaseReference? = {
  // 1
  guard let uid = Auth.auth().currentUser?.uid else {
    return nil
  }

  // 2
  let ref = Database.database()
    .reference()
    .child("users/\(uid)/thoughts")
  return ref
}()

// 3
private let encoder = JSONEncoder()

This:

  1. Gets the user ID of the authenticated user.
  2. Returns a reference to the path in your database where you want to store data.
  3. Defines an encoder variable to encode JSON data.

Inside postThought(), add the following:

// 1
guard let databasePath = databasePath else {
  return
}

// 2
if newThoughtText.isEmpty {
  return
}

// 3
let thought = ThoughtModel(text: newThoughtText)

do {
  // 4
  let data = try encoder.encode(thought)

  // 5
  let json = try JSONSerialization.jsonObject(with: data)

  // 6
  databasePath.childByAutoId()
    .setValue(json)
} catch {
  print("an error occurred", error)
}

This code:

  1. Gets the previously defined database path.
  2. Returns immediately if there’s no text to post to the database.
  3. Creates a ThoughtModel object from the text.
  4. Encodes the ThoughtModel into JSON data.
  5. Converts the JSON data into a JSON Dictionary.
  6. Writes the dictionary to the database path as a child node with an automatically generated ID.

To encode the ThoughtModel, you’ll need to make it conform to the Codable protocol. Open ThoughtModel.swift and replace struct ThoughtModel: Identifiable { with:

struct ThoughtModel: Identifiable, Codable {

Build and run the app. Tap the + button, write a thought and then tap Post. You won’t notice anything on the app because you haven’t added code to read from the database.

But fear not! Return to the Real-Time Database on the Firebase console. On the left navigation panel, select the Realtime Database menu item under the Build menu item. Select the Data tab under the Real-Time Database text at the top of the screen. The message you typed on the app should now display as stored in your database:

Database data

Post another message, and you’ll see it appear in real-time in the database. Now, it’s time to get that data to display in your app!

Reading Data from the Real-Time Database

To read data at a path and listen for changes, you attach an observer to the path reference to listen to events. Types of observers include:

  • .value: Read and listen for changes to the entire contents of a path.
  • .childAdded: Retrieve lists of items or listen for additions to a list of items.
  • .childChanged: Listen for changes to the items in a list.
  • .childRemoved: Listen for items removed from a list.
  • .childMoved: Listen for changes to the order of items in an ordered list.

Which one you choose depends on your use case. In this app, you’ll only ever add new items to the list of thoughts. Therefore, it makes sense to use .childAdded.

Open JournalModelController.swift. Under private let encoder = JSONEncoder(), add:

 private let decoder = JSONDecoder()

You’ll use this to decode data retrieved from the database.

Then, add the following in listenForThoughts():

// 1
guard let databasePath = databasePath else {
  return
}

// 2
databasePath
  .observe(.childAdded) { [weak self] snapshot in

    // 3
    guard
      let self = self,
      var json = snapshot.value as? [String: Any]
    else {
      return
    }

    // 4
    json["id"] = snapshot.key

    do {

      // 5
      let thoughtData = try JSONSerialization.data(withJSONObject: json)
      // 6
      let thought = try self.decoder.decode(ThoughtModel.self, from: thoughtData)
      // 7
      self.thoughts.append(thought)
    } catch {
      print("an error occurred", error)
    }
  }

That code:

  1. Gets the database path that was previously defined.
  2. Attaches a .childAdded observer to the database path.
  3. When a child node gets added to the database path, it returns a snapshot. This contains a value Dictionary and a key string, which store the data and the ID from the child node, respectively. Here, the mutable variable json stores the snapshot value.
  4. The id key of json stores the key variable of the snapshot.
  5. The json Dictionary converts into a JSON Data object.
  6. The data decodes into a ThoughtModel object.
  7. The new ThoughtModel object appends to the array of thoughts for display on screen.
Note: In a production app, you might want to sort and limit the query to prevent retrieving large amounts of data, which could get expensive.

Build and run the app. You’ll see your previously saved thoughts appear!

Thoughts list

Now, tap Sign Out. Look at the console log:

Listener at /users/5CbvKMuKArWEAFYiuTEOrwI4dxY2/thoughts
failed: permission_denied

This happens because you still have an observer attached to your database path, despite signing out. To fix this, open JournalModelController.swift and add the following in stopListening():

databasePath?.removeAllObservers()

This removes the observer at the database path reference before signing out. Note that this method doesn’t remove any observers at child references. So, if you were observing child nodes of this path, you’d need to call removeAllObservers() for each reference.

Build and run again. Sign in to your account, then sign out again to confirm you no longer receive the warning in the console.

Persisting Data Offline

So your app is working. Great! But what happens if you’re at a cozy retreat nestled in the woods of a remote island with dodgy internet and you want to post a thought? You could lose your thought data! But Firebase has thought of this (no pun intended) and has a feature called Disk Persistence. This automatically caches your data to local storage while your device is offline and will save it to the database when you reconnect to the internet.

Open AppMain.swift. Inside init(), add the following below FirebaseApp.configure():

Database.database().isPersistenceEnabled = true

And that’s all you need to do! Isn’t that great?

Build and run your app, but make sure to do this on a device. Sign in and then turn on airplane mode to disconnect the device from the internet. Now, post a thought.

Notice the thought displays in the list on your mobile device as if your device is online. But if you view the Real-Time Database on your computer, you see the thought isn’t stored in the database yet.

Now, turn off airplane mode to reconnect your device to the internet and watch the database. You’ll see the thought data appear like magic. Clever, eh?

Where to Go From Here?

Download the completed project using the Download Materials button at the top or bottom of this tutorial.

In this Real-Time Database tutorial, you learned:

  • How to implement authentication with Firebase.
  • How to protect data access with security rules.
  • The differences between the Real-Time Database and Firestore.
  • How to implement, listen to and store data with the Real-Time Database.
  • How to persist data offline.

This project is only the beginning! There’s plenty more you can do, such as query and sort data, manage presence and, if needed, delete data.

Check out the Firebase iOS documentation for more information. Alternatively, review our 22 part video course on Beginning Firebase and/or our pro video course Beginning Firebase for iOS. For an intermediate-level project, check out Firebase Tutorial Real-Time Chat.

I hope you enjoyed this tutorial, and if you have any questions or comments, please join the forum discussion below!