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.

4.6 (5) · 4 Reviews

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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?