Chapter 37: Firebase Chat App

Introduction and Building a Realtime Chat App


What you will learn

  • What Firebase is
  • Components that make up Firebase
  • Firebase Limitations
  • Set up a Firebase Project
  • Using the Firebase Console
  • Build an Authentication Service
  • Build a Realtime Data Service
  • Displaying Realtime Data in a TableView

Key Terms

  • API
  • Firebase
  • (BaaS) Backend as a Service
  • Authentication Service
  • Data Service
  • Data Model
  • UITableView

Introduction to Firebase

Firebase is a Backend as a service (BaaS) company offering a robust collection of cloud services geared toward making backend services easier for developers.

Firebase was founded in 2011 and released with a realtime database component in April of 2012. Firebase was subsequently purchased by Google in 2014 and has since reached version 3, in which several new components were introduced.

With the version 3 release of Firebase, the service now includes several individual components that make up the service as a whole: Analytics, Authentication, Cloud Messaging, Realtime Database, Storage, Hosting, Remote App Configuration, Test Lab for Android, Crash Reporting, Notifications, App Indexing, Dynamic Links and Invites. Many times when people refer to Firebase, they may just be referring to the realtime database component, however, by including Firebase into your project, all of these individual services are included right out of the box.

Firebase has native SDKs for iOS, Android and Web. One of the drawbacks with Firebase is that, as of Oct 2016, they do not offer native SDKs for tvOS, MacOS or Windows. If your app will be available on any of those platforms, then you may have to find an alternate solution. If, however, your app will only target one of the 3 platforms that has a native SDK, it is hard to beat Firebase for its ease of setup and cost to get started.

Firebase Chat App

In the following sections, we will walk through the process of creating a realtime chat app for iOS 10 in Swift 3 using Firebase. We will be using the Authentication and Realtime Database components of Firebase. This is a full-featured app, but fairly basic. After we build it, I will offer a challenge for you to customize it and take it in your own direction. I really look forward to see where you can take this project.

As with all projects I work on, I like to break it down into smaller components. The components we will build out are as follows:

  • Creating the Firebase project
  • Creating the Xcode project
  • Installing the Firebase SDK with Cocoapods
  • Creating our Authentication Service
  • Creating our Data Service
  • Adding View Controllers in Interface Builder
  • Adding the Authentication UI
  • Adding the code for the SignInVC
  • Adding the Chat TableView UI
  • Adding the code for the MainVC
  • Custom UITableViewCell
  • Custom UIView for our header
  • Wiring things up

Creating the Firebase project

For the first part of our project, we need to log into Firebase and set up our project. In your web browser, head over to https://firebase.google.com. If you are not signed into your Google account, the first thing to do is sign in. In the upper right corner, click on Sign in and log in with your credentials.

Figure 5.4.1 Sign In

After signing in, you should see a link in the upper right corner that says Go to console. Go ahead and click that to go the the Firebase console to set up our project.

Figure 5.4.2

Go to Console

After going to the console, you will see a list of any of your existing projects, if you have any. You will also see a Button that says CREATE NEW PROJECT. We will click this button to add our new project.

Figure 5.4.3 Create new Firebase Project

Once the new project popup form comes up, choose a name for your project. In this case we chose Devslopes-Chat. Once you fill that in, be sure the correct country is selected and then click CREATE PROJECT.

Figure 5.4.4

Enter Project Name

Once the dialog box disappears, you will be greeted once again with your list of projects. There should now be a card for your new project we just created.

Figure 5.4.5

Project Card

Next, we need to create our Xcode iOS project so that we can get the bundle id and add that to our project in the Firebase console.

Creating the Xcode project

Go ahead and open up Xcode. When the welcome screen appears, click on Create a new Xcode Project.

Figure 5.4.6

Create new Xcode Project

When the template dialog appears, make sure the project type is set to iOS, choose Single View Application and click Next.

Figure 5.4.7 New Single View Application

In the Project options dialog that appears next, choose a name for your project, devslopes-chat in my case. Make sure you have an organization identifier specified. The organization identifier is usually your company's domain name in reverse. In the case of Devslopes, our organization identifier is com.devslopes. If you do not have a domain, just use something that would be unique, like com.yourname or something similar.

The bundle identifier will be listed below the organization identifier. Take note of this, because you will need it when you set up your iOS app in Firebase. Also, be sure you specify Swift for the Language and iPhone in this case for our app as this will be for iPhone only. I have also checked the box to include unit tests which I will add in the unit testing chapter. Go ahead and click Next.

Figure 5.4.8 Project Options

In the save project dialog, choose a location to save your project. In my example, I am saving to my desktop. Also go ahead and check Create Git repository on My Mac to set up the git repo and click Create.

Figure 5.4.9 Project Location

Once our project is created in Xcode and saved, the General project page should be shown. Again, take note of the bundle id here in the project settings and uncheck landsape left and landscape right. The only option that should be checked is portrait.

Figure 5.4.10 General Settings

At this point, we need to quit Xcode and move on to installing the Firebase SDK with Cocoapods in the next section.

Installing the Firebase SDK with Cocoapods

Now that we have our Xcode project set up, it is time to set up the Firebase SDK with Cocoapods. I am currently using a Mac without Cocoapods installed, so we will quickly walk through the process. If you already have it installed, you can skip through the first part of this section.

Installing Cocoapods

To check to see if you have Cocoapods already installed, simply open up a terminal window and type which pod. If Cocoapods is installed, you should see a path to the executable. If not, follow on.

Figure 5.4.11 Check to see if Cocoapods is installed

To install Cocoapods, simply type sudo gem install cocoapods followed by return in a terminal window. You will have to enter your password and then hit return again.

Figure 5.4.12 Install Cocoapods

It will take Cocoapods a few minutes to install based on your internet connection, but it should finally finish up and report a success message similar to the following:

Figure 5.4.13

Installation Success

Adding the Firebase pod to our app

Now that cocoapods is installed, we can move on and add Firebase to our project. From your terminal window, cd to our app project folder. In my case, I had to type cd /Users/jack/Desktop/devslopes-chat. Adjust your path accordingly. Once there, type pod init to initialize Cocoapods for our project.

Figure 5.4.14

Initialize our Project

We should now have our project set up and ready to go. We need to edit our Podfile to add Firebase and then install it. With a text editor, open up the Podfile file in your project folder to edit it. I use Vim, but you can use whatever flavor of editor you like. Make sure that the Platform line is uncommented, and then add pod 'Firebase', pod 'Firebase/Auth' and pod 'Firebase/Database' under the use frameworks line.

Figure 5.4.15 Edit the Podfile

Out of the many features Firebase has to offer, Authentication and Realtime Database are the only two services we will be using in this project. We could always incorporate some of the other Firebase services in our project, but for now, these two are what we need.

Once you have your Podfile in shape, go ahead and type pod install followed by return. This will start the ball rolling and begin installing our SDKs. If this is the first time you have used Cocoapods on your Mac, it may take some time because it has to update the master repo on your machine. Once complete, you should see a message advising you to no longer use the .xcproject file to open your project, but instead to use the new .xcworkspace file.

Figure 5.4.16 pod installation complete

You can now open the new .xcworkspace file from within finder or from file->open within Xcode. From here, we need to create the iOS App in the Firebase Console and import the Google Service plist file in our Xcode project. Back over in the Firebase Console, click on the card for your app and you will be taken to a Getting Started page. We are setting up an iOS app, so click on the Add Firebase to your iOS App option.

Figure 5.4.17 Add Firebase to your iOS App

Once you do this, you are greeted with a series of steps to get your app set up. In the first dialog, you are asked to enter the bundle id of your app. This is the bundle id that I asked you to remember earlier. Type or paste that into the box and then enter a nickname for your app if you wish. Once you complete these steps, click continue.

Figure 5.4.18 Enter Bundle ID

Once you click continue, a file should download called GoogleService-info.plist. This is the file we will import into your project in just a minute as we configure it. For now, just hang on to that in a safe place.

Figure 5.4.19 GoogleService-info.plist

This screen, as well as the next two, give you some information about how to set up your project. Go ahead and glance through them, even though we are going to do all of that here in this text. When you click Continue, you are advised on the procedure for installing the SDK via Cocoapods. We have already done this, so go ahead and click Continue to carry on.

Figure 5.4.20 Cocoapods Setup Instructions

On the fourth and final dialog, we are shown the initialization code to add to our app to connect the SDK. We cover that next, so go ahead and click Finish.

Figure 5.4.21 Initialization Code

Now that we have our project set up, let's move on to writing some code.

The first thing we need to do is add a call inside our AppDelegate.swift file. Click on the AppDelegate.swift file in the Project Navigator.

Figure 5.4.22

Open the AppDelegate.swift file

The first thing is to add import Firebase in our import statements (just below import UIKit). In the code editor, find the application(application:didFinishLaunchingWithOptions) method. Inside the body of this method, before the return true statement, We need to add FIRApp.configure(). The full method should look like the following code:

Figure 5.4.23

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

    FIRApp.configure()
    return true
  }

I almost got ahead of myself. We still need to import our GoogleService-info.plist file into our project. Open a finder window and navigate to the location where you saved the plist file. Click and drag that file into your project navigator on the same level as your app delegate. An Add File dialog will appear. Make sure that copy items if needed is checked as well as the devslopes-chat target option and click Finish.

Figure 5.4.24 Add File

The GoogleService-info.plist file should now be in your Project Navigator similiar to the following image.

Figure 5.4.25

Project Navigator

Now that we have Firebase configured in our AppDelegate and our plist file imported, we can move on to creating our Authentication Service in the next section.

Creating the Authentication Service

In this section we are going to create our Authentication Service in code to be sort of a middle-man between our app and the Firebase Auth system. Before writing our code, we need to do one more thing in the Firebase Console.

In the console, click Authentication in the left menu bar. Near the top of the page, you should see Sign In Method, go ahead and click that. Finally, under Email/Password, click the little edit pencil out to the right to edit that method.

Figure 5.4.26 Editing authentication

In the dialog box that appears, toggle the Enable switch and click Save.

Figure 5.4.27 Enabling Email/Password authentication

With these steps complete, we have enabled Email/Password authentication with Firebase. Now time to move on to code...I promise.

Our Authentication Service will be a single file and structured as a singleton. A singleton is code that is instantiated one time and one time only in your app and is available everywhere within your app. There can only ever be ONE instance of a singleton throughout an app. Services are great candidates for singletons and in fact, our Auth Service as well as our Data Service will both be singletons.

First, let's create some groups. To do this, right-click on your project folder inside the Project Navigator and choose New Group.

Figure 5.4.28

Add Group

Once the new group is added (which looks like a folder), go ahead and rename it Services. While we're at it, add three more groups and name them Model, View and finally Controller.

Figure 5.4.29

Groups in Project Navigator

Next, time to create our AuthService.swift file. Right click on the Services group and then click on New File.

Figure 5.4.30

New File

In the dialog that appears, be sure iOS is selected at the top, click on Swift File and then click Next.

Figure 5.4.31 New file dialog

In the save as dialog, type AuthService.swift in the Save As: box, make sure the devslopes-chat target is checked and click Create.

Figure 5.4.32 Save As

After saving the file, you should now see our newly created AuthService.swift file in the Project Navigator under the Services group.

Figure 5.4.33

Project Navigator

With our AuthService.swift file open in our Xcode editor, let write some code. First we need to import Firebase. At the top of the file underneath import Foundation, add the line import Firebase. Next add the singleton structure to the file.

Figure 5.4.34

class AuthService {
    static let instance = AuthService()

}

Next, we will set up a couple of variables to store some data. First we need an optional variable for a username and second we need a Boolean variable to keep track of the logged in state.

Figure 5.4.35

class AuthService {
    static let instance = AuthService()

    var username: String?
    var isLoggedIn = false
}

Finally, the meat of our Auth Service comes down to one single function. Here is the code and we will walk through it below.

Figure 5.4.36

class AuthService {
  static let instance = AuthService()

  var username: String?
  var isLoggedIn = false

  func emailLogin(_ email: String, password: String, completion: @escaping (_ Success: Bool, _ message: String) -> Void) {

    FIRAuth.auth()?.signIn(withEmail: email, password: password, completion: { (user, error) in
      if error != nil {
        if let errorCode = FIRAuthErrorCode(rawValue: (error?._code)!) {
          if errorCode == .errorCodeUserNotFound {
            FIRAuth.auth()?.createUser(withEmail: email, password: password, completion: { (user, error) in
              if error != nil {
                completion(false, "Error creating account")
              } else {
                completion(true, "Successfully created account")
              }
            })
          } else {
            completion(false, "Sorry, Incorrect email or password")
          }
        }
      } else {
        completion(true, "Successfully Logged In")
      }
    })
  }
}

First we declare our function. It takes a String username and String password as parameters and includes a completion handler.

First up, we try to sign in to Firebase with our FIRAuth.auth() function call here. We first check for the presence of an error. If there was an error, we check to see if the error we received was that the user was not found. If it was, then we attempt to create a new user via the FIRAuth.auth()?.createUser(withEmail:password:completion) method.

With this method, again, we are either successful or we receive an error. If we receive an error, then we pass in false (not successful) and an error message to our completion handler. If we did not receive an error, creating the new user was successful, so we pass in true and a success message to our completion handler.

If the error received in our signIn method was not .errorCodeUserNotFound, the we know that the user exists, but the password was wrong. For this case, we pass false and another message to our completion handler.

Finally, if we did not receive an error on signIn, we are successfully authenticated. We pass in true and 'Successfully Logged In' to our completion handler.

This is all we need for our Auth Service, so let's move on to our Data Service next

Creating our Data Service

Right click on your Services group once again and add another iOS Swift file as we did above. Name this file DataService.swift. As above, under import Foundation, add your import Firebase statement. We are going to start off here by adding a protocol. Under your import statements, add the following:

Figure 5.4.37

import Foundation
import Firebase

protocol DataServiceDelegate: class {
  func dataLoaded()
}

This is our delegate protocol which will let us fire a delegate method any time our data is loaded. Next, go ahead and set up our singleton class structure.

Figure 5.4.38

import Foundation
import Firebase

protocol DataServiceDelegate: class {
  func dataLoaded()
}

class DataService {
  static let instance = DataService()
}

We will need a total of 3 variables for this file, a constant for our Firebase Database reference, a variable for storing our array of messages and a weak variable for our delegate. Let's add those now:

Figure 5.4.39

import Foundation
import Firebase

protocol DataServiceDelegate: class {
  func dataLoaded()
}

class DataService {
  static let instance = DataService()

  let ref = FIRDatabase.database().reference()
  var messages: [Message] = []

  weak var delegate: DataServiceDelegate?
}

Before continuing, we really need to create our Data Model for a Message. This being a chat app, it wouldn't be structured very well if we didn't have a Message model.

Message Data Model

Right click on your Models group and add a new file. Make this a Swift file and name it Message.swift. This model will be a structure or struct. At the top of the file, let's go ahead and set it up:

Figure 5.4.40

import Foundation

struct Message {
  fileprivate let _messageId: String
  fileprivate let _userId: String?
  fileprivate let _message: String?
}

Here, we have declared our struct and set up three private properties, a message ID, a user ID and a message. Since these are private, next we need to create some public accessors for them:

Figure 5.4.41

import Foundation

struct Message {
  fileprivate let _messageId: String
  fileprivate let _userId: String?
  fileprivate let _message: String?

  var messageId: String {
    return _messageId
  }

  var userId: String? {
    return _userId
  }

  var message: String? {
    return _message
  }
}

Now we have three private properties and three getters for those properties. We didn't include any setters as these will only be set through initializers. In fact, we will have two initializers, one to init with all three parameters and another to init with a messageId and Firebase Data.

We are also going to add a static method (a method that is called on the struct itself and not an instance of the struct) to load and return a message array from passed in Firebase Data. Let's start with the two initializers; first up, Firebase Data:

Figure 5.4.42

  init(messageId: String, messageData: Dictionary<String, AnyObject>) {
    _messageId = messageId
    _userId = messageData["user"] as? String
    _message = messageData["message"] as? String
  }

This initializer is fairly straightforward. We pass in the messageId and our Firebase data as a dictionary of type <String, AnyObject>. The messageId is set to the passed in messageId and the other two properties are set by pulling the corresponding value out of our dictionary. Next up, our initializer with all properties passed in:

Figure 5.4.43

  init(messageId: String, userId: String?, message: String?) {
    _messageId = messageId
    _userId = userId
    _message = message
  }

This initializer is even more straightforward than the first. We simply set each property equal to the passed in value of the same name. Now let's look at the code for our static function and then we will discuss it:

Figure 5.4.44

  static func messageArrayFromFBData(_ fbData: AnyObject) -> [Message] {

    var messages = [Message]()
    if let formatted = fbData as? Dictionary<String, AnyObject> {

      for (key, messageObj) in formatted {
        let obj = messageObj as! Dictionary<String, AnyObject>
        let message = Message(messageId: key, messageData: obj as Dictionary<String, AnyObject>)
        messages.append(message)
      }
    }
    return messages
  }

First up here, we create a new empty array of type Message to store our messages in. Next, we grab our passed in Firebase Data fbData and store it in a variable formatted as a Dictionary of type <String, AnyObject>.

Next, we loop through each element in the dictionary and grab the key (which is the messageId) and the object which is in turn formatted as another Dictionary of type <String, AnyObject>. We then call our init method from above that takes Firebase Data to create a new, properly initialized Message. Once we have a Message for that iteration, we append it onto our messages array. Finally, after we have looped through every element, we return our messages array. The code for the entire file should look like the following:

Figure 5.4.45

import Foundation

struct Message {
  fileprivate let _messageId: String
  fileprivate let _userId: String?
  fileprivate let _message: String?

  var messageId: String {
    return _messageId
  }

  var userId: String? {
    return _userId
  }

  var message: String? {
    return _message
  }

  init(messageId: String, messageData: Dictionary<String, AnyObject>) {
    _messageId = messageId
    _userId = messageData["user"] as? String
    _message = messageData["message"] as? String
  }

  init(messageId: String, userId: String?, message: String?) {
    _messageId = messageId
    _userId = userId
    _message = message
  }

  static func messageArrayFromFBData(_ fbData: AnyObject) -> [Message] {

    var messages = [Message]()
    if let formatted = fbData as? Dictionary<String, AnyObject> {

      for (key, messageObj) in formatted {
        let obj = messageObj as! Dictionary<String, AnyObject>
        let message = Message(messageId: key, messageData: obj as Dictionary<String, AnyObject>)
        messages.append(message)
      }
    }
    return messages
  }
}

Back to our Data Service

Now that we took care of our Message Data Model, let's get back to work on our Data Service. We will have two methods in this service. One to load all messages from Firebase and a second to save a message to Firebase. Take note of the pattern here, we are keeping our data totally separate from our UI which is always a great idea.

Our first method will load up our messages from Firebase and store them in our messages array:

Figure 5.4.46

  func loadMessages(_ completion: @escaping (_ Success: Bool) -> Void) {
    ref.observe(.value) { (data: FIRDataSnapshot) in
      if data.value != nil {
        let unsortedMessages = Message.messageArrayFromFBData(data.value! as AnyObject)
        self.messages = unsortedMessages.sorted(by: { $0.messageId < $1.messageId })
        self.delegate?.dataLoaded()
        if self.messages.count > 0 {
          completion(true)
        } else {
          completion(false)
        }
      } else {
        completion(false)
      }
    }
  }

This method takes no parameters, but includes a completion handler. First up, we call the observe method on our Firebase Reference ref. We are observing the value of that Firebase location and will receive a FIRDataSnapshot. We check to make sure data.value if not nil, otherwise we pass in false to our completion handler (not successful). If data.value is not nil, we call our static method in our Message struct, passing it in as type AnyObject. We assign the value returned from that method to a constant called unsortedMessages.

Next up, we take the unsortedMessages array and sort by the messageId. We then assign that sorted array to our messages array. Since we will be using AutoId keys that Firebase provides, this will keep our messages sorted in chronological order.

We fire off our delegate method to notify any subscribers that our data has loaded and then check to make sure our messages array actually contains data. If so, we pass true to our completion handler and false otherwise.

Finally, let's turn to our last method in this service:

Figure 5.4.47

  func saveMessage(_ user: String, message: String) {
    let key = ref.childByAutoId().key
    let message = ["user": user,
                   "message": message]
    let messageUpdates = ["/\(key)": message]
    ref.updateChildValues(messageUpdates)
  }

This method simply saves a message to Firebase. We pass in the user as a String and the message as a String. We make the key by using the Firebase childByAutoId().key method.

By using this, our messages will be sorted in chronological order because the autoId provided by Firebase incorporates a timestamp in it. We create a message Dictionary using the passed in values and then create another Dictionary containing our key and the message dictionary.

Finally we call the Firebase method updateChildValues and pass in our data to be saved to Firebase. To try and clarify just a little, what we are passing to updateChildValues would look something like this:

Figure 5.4.49

["-KUedAHaUiD9rx2BR1gY":["user":"jack","message":"What's going on?"]]

The full file for our DataService.swift file should look as follows:

Figure 5.4.48

import Foundation
import Firebase

protocol DataServiceDelegate: class {
  func dataLoaded()
}

class DataService {
  static let instance = DataService()

  let ref = FIRDatabase.database().reference()
  var messages: [Message] = []

  weak var delegate: DataServiceDelegate?

  func loadMessages(_ completion: @escaping (_ Success: Bool) -> Void) {
    ref.observe(.value) { (data: FIRDataSnapshot) in
      if data.value != nil {
        let unsortedMessages = Message.messageArrayFromFBData(data.value! as AnyObject)
        self.messages = unsortedMessages.sorted(by: { $0.messageId < $1.messageId })
        self.delegate?.dataLoaded()
        if self.messages.count > 0 {
          completion(true)
        } else {
          completion(false)
        }
      } else {
        completion(false)
      }
    }
  }

  func saveMessage(_ user: String, message: String) {
    let key = ref.childByAutoId().key
    let message = ["user": user,
                   "message": message]
    let messageUpdates = ["/\(key)": message]
    ref.updateChildValues(messageUpdates)
  }
}

Now that our Data Service is complete, we need to make a quick addition to our AppDelegate. After our app is configured, we need to set up an observer to "watch" the data and update it if it changes. Update your AppDelegate's application(application:didFinishLaunchingWithOptions) method like so:

Figure 5.4.48.1

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

        FIRApp.configure()

        DataService.instance.loadMessages({ Success in
            if !Success {
                print("Load Firebase data FAILED!")
            } else {
                print("Success!")
            }
        })

        return true
    }

All we are doing here is calling our loadMessages method in our DataService singleton. It sets up the observer to make updates when the data changes. By calling this method in the AppDelegate, we insure that the data gets downloaded on application launch and updates as necessary.

Adding View Controllers in Interface Builder

Now that we have our Data Model and Data Service set up and totally disconnected from our UI, let's turn our attention to the UI.

Let's start by ditching the ViewController.swift file in our project. Right click on the file in the Project Navigator and choose delete.

Figure 5.4.49 33.png

Now that our default ViewController is gone, let's make a new one. Right click on the Controller group and choose New File.

Figure 5.4.50 34.png

In the New File Dialog, choose iOS, Cocoa Touch Class and click Next.

Figure 5.4.51 35.png

In the New File Options dialog, Name the class SignInVC and change Subclass of to UIViewController if is not that already. Make sure the language is Swift and click Next.

Figure 5.4.52 36.png

Click on Main Storyboard and locate your main View Controller. Click on the File Owner button at the top and the in th Identity Inspector on the right, change the class in the dropdown to SignInVC.

Figure 5.4.53 37.png

While still in the Main Storyboard, drag out a new View Controller from the Object Library into interface builder. Place it to the right of our existing SignInVC.

Figure 5.4.54 38.png

Now, repeat the process for creating a new ViewController Swift file. Right click on the Controller group and choose New File. Make the class name MainVC and make sure it is a subclass of UIViewController. Also make sure the language is set to Swift. Once you have that file saved, click the file owner button at the top of the new View Controller you dragged into interface builder and set the class to MainVC in the identity inspector the way you did above.

Figure 5.4.55 39.png

The only thing left before we move on to controls is to add a couple of segues. There are multiple ways to do this, but for this example, we are going to hold down ctrl on our keyboard and click and drag from SignInVC to MainVC in our document outline.

Figure 5.4.56

40.png

Once you drag this connection, you should see a small dialog box appear asking what type of manual segue you would like. Go ahead and choose Show. Once this is done, you should see the segue connecting the two view controllers in interface builder. Click on the small circle in between the two view controllers on the segue line and in the attributes inspector on the right, add the identifier showMainVC.

Figure 5.4.57 41.png

We also need a segue back to the SignInVC from our MainVC. To do this, just reverse the order. Hold down ctrl and click and drag from MainVC to SignInVC in the document outline. Select that new segue and in the attributes inspector, set the segue identifier to showSignInVC.

Figure 5.4.58 42.png

This wraps up the basics of adding our two View Controllers and hooking up our segues. In the next section, we will start adding controls to our View Controllers and finally conclude by hooking everything up.

Adding the Authentication UI

Our SignInVC is fairly straightforward, just a Label, Image, two Text Fields and one Button. To get started, click on the SignInVC and in the attributes inspector, click the drop down box next to Background. Choose other to bring up our color picker.

Figure 5.4.59

44.png

The color I am going to use for the background is hex value FFB234. In the color picker, choose color sliders at the top and pull down the drop down box to choose RGB Sliders. Add our hex value FFB234 to the Hex Color # box.

Figure 5.4.60

43.png

Next, let's drag out a UILabel from our Object Library onto our SignInVC.

Figure 5.4.61 45.png

Place the label near the top and change the text property in the attributes inspector to Devslopes Chat or whatever you want to call your app. I also changed the font color to white, the font to Futura Medium 28 and set the alignment to center.

Adjust these values to what you would like. One thing to note here is that I am placing the controls fairly high on this sign in VC so that when the keyboard appears, it will not cover anything.

In the next section when we are dealing with our table view, the message input text field is at the bottom. In that case, we will add code to move the entire view up so that the keyboard does not interfere with the user experience.

Figure 5.4.62 46.png

The next thing to do is add a UIImageView to our VC. This will be for our app icon image. Click on the Assets.xcassets folder in the Project Navigator. In the Document Outline, click the plus sign at the bottom and then click New Image Set.

Figure 5.4.63

47.png

Double click the name of the new image set you just added and rename it to DevslopesChat or whatever you would like it to be. Find your image you want to use and drag it into the 1x location in the image set you just made. Your image is now ready to be used.

Figure 5.4.64 48.png

Go back to your Main.storyboard and let's get back to work. Drag an Image View from the Object Library onto your SignInVC. Place it right under your label and center it. In the attributes inspector pull down the Image drop down box and choose your image you just added.

Figure 5.4.65 49.png

I changed my Content Mode to Aspect Fit and then used the resize handles on the image view to adjust the size.

Figure 5.4.65 50.png

Next up, drag out two Text Fields from the object library and center those underneath your new image. In these two Text Fields, I went ahead and added placeholder text in the attributes inspector, as well as a custom font and keyboard options. For the password field, I also ticked the box next to secure text entry.

Figure 5.4.66 51.png

The only thing left to do here is to add a sign in button. Go ahead and drag out a button from the object library and center it up underneath our text fields. In the attributes inspector, I changed the text to Sign In/Up since we will be able to do either from this single button. I also changed the font color to white, the font style to Futura Medium 18 and the background to hex value 22A685. We will round the corners a little when we get to the code.

Figure 5.4.67 52.png

All that is left at this point is to add constraints to all of my controls. I will start by giving my label at the top a fixed width, pinning it to the top and centering it horizontally.

Figure 5.4.68

53.png

Figure 5.4.69

54.png

For our image, I am going to pin it to the label above, give it a fixed width and height and center it horizontally with the label above.

Figure 5.4.70

55.png

To center the image with the label above, I held down ctrl, clicked and dragged from the image view up to the label above. I was then presented with a popup where I chose center horizontally.

Figure 5.4.71

56.png

I will repeat these steps for the remaining controls. The final version should look like this:

Figure 5.4.72

57.png

That wraps up adding controls to our SignInVC, so let's now turn to the code behind it.

Adding the code for the SignInVC

To begin, we need to import Firebase and we need three @IBOutlets for our controls.

Figure 5.4.73

import UIKit
import Firebase

class SignInVC: UIViewController {

    @IBOutlet weak var emailTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var signInButton: UIButton!
}

We obviously need the text field outlets so we can grab the text out of them. The reason we added an outlet for our button is that we are going to round the corners of it now. In ViewDidLoad, add the following:

Figure 5.4.74

    override func viewDidLoad() {
        super.viewDidLoad()

        signInButton.layer.cornerRadius = 8
    }

Since we are configuring our Firebase SDK in our AppDelegate, there is a chance that since this is the first screen to load that the app will not be configured by the time viewDidLoad is called. To make sure our app is configured before we move on, I am going to add an override of viewDidAppear. Here is the code in viewDidAppear...we will discuss it in one second.

Figure 5.4.75

    override func viewDidAppear(_ animated: Bool) {

        setUsername()
        if AuthService.instance.isLoggedIn {
            performSegue(withIdentifier: "showMainVC", sender: nil)
        }
    }

In this code, we are calling a function we will create in just a moment. It is just basically checking to see if a user is logged in, and if so, it grabs the username portion of their email address and hangs onto it for later use and then immediately segues to our MainVC. It might be a better idea to have our MainVC as the initial View Controller and display a login popup if there is no user logged in, but for our example app, this will work for now.

Next we are going to create a new showAlert function. This function takes a title and a message and just displays an alert if we encounter a problem in our code.

Figure 5.4.76

    func showAlert(title: String, message: String) {
        let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
        let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
        alertController.addAction(okAction)
        self.present(alertController, animated: true, completion: nil)
    }

Now on to our setUsername() function. As I explained above, we are just storing the username for the logged in user if one is available. This way we can display that in our chat TableView instead of displaing the entire email address. It also sets the isLoggedIn Boolean variable in our AuthService. Note that if a user is loggged in, FIRAuth.auth()?.currentUser will have a value; otherwise it will be nil.

Figure 5.4.77

    func setUsername() {
        if let user = FIRAuth.auth()?.currentUser {
            AuthService.instance.isLoggedIn = true
            let emailComponents = user.email?.components(separatedBy: "@")
            if let username = emailComponents?[0] {
                AuthService.instance.username = username
            }
        } else {
            AuthService.instance.isLoggedIn = false
            AuthService.instance.username = nil
        }
    }

The last bit of code to add for this file is for our Sign in button.

Figure 5.4.78

    @IBAction func signInTapped(sender: UIButton) {

        // unwraps the textfields and stores them in constants
        guard let email = emailTextField.text, let password = passwordTextField.text else {
            showAlert(title: "Error", message: "Please enter an email and password")
            return
        }
        // check to make sure they are not an empty string
        guard email != "", password != "" else {
            showAlert(title: "Error", message: "Please enter an email and password")
            return
        }

        AuthService.instance.emailLogin(email, password: password) { (success, message) in
            if success {
                self.setUsername()
                self.performSegue(withIdentifier: "showMainVC", sender: nil)
            } else {
                self.showAlert(title: "Failure", message: message)
            }
        }
    }

This code is very straightforward. We are making sure that our text fields have values in them, or we are calling our showAlert() function and displaying an alert on the screen. If they do contain values, we call our emailLogin function we created in our Auth Service.

If the authentication was successful, we again call setUsername() and then immediately segue to our MainVC. If authentication was unsuccessful, we display an alert. Remember here that we are attempting to authenticate first and if a user with that email doesn't exist, then we try to create the account. We should only receive a failure if either the password was incorrect for an existing account, or there was a system error creating the account. The full code for this file:

Figure 5.4.79

import UIKit
import Firebase

class SignInVC: UIViewController {

    @IBOutlet weak var emailTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var signInButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        signInButton.layer.cornerRadius = 8
    }

    override func viewDidAppear(_ animated: Bool) {

        setUsername()
        if AuthService.instance.isLoggedIn {
            performSegue(withIdentifier: "showMainVC", sender: nil)
        }
    }

    func showAlert(title: String, message: String) {
        let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
        let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
        alertController.addAction(okAction)
        self.present(alertController, animated: true, completion: nil)
    }

    func setUsername() {
        if let user = FIRAuth.auth()?.currentUser {
            AuthService.instance.isLoggedIn = true
            let emailComponents = user.email?.components(separatedBy: "@")
            if let username = emailComponents?[0] {
                AuthService.instance.username = username
            }
        } else {
            AuthService.instance.isLoggedIn = false
            AuthService.instance.username = nil
        }
    }

    @IBAction func signInTapped(sender: UIButton) {

        // unwraps the textfields and stores them in constants
        guard let email = emailTextField.text, let password = passwordTextField.text else {
            showAlert(title: "Error", message: "Please enter an email and password")
            return
        }
        // check to make sure they are not an empty string
        guard email != "", password != "" else {
            showAlert(title: "Error", message: "Please enter an email and password")
            return
        }

        AuthService.instance.emailLogin(email, password: password) { (success, message) in
            if success {
                self.setUsername()
                self.performSegue(withIdentifier: "showMainVC", sender: nil)
            } else {
                self.showAlert(title: "Failure", message: message)
            }
        }
    }
}

Adding the Chat UITableView UI

Next up, we can create our UITableView to contain our messages. Let's start off by clicking on Main.storyboard and scrolling over to our MainVC. In the Object Library, search for UIView and then drag that onto the Storyboard. Anchor it at the top of our MainVC and resize it so that it stretches from edge to edge. Change the background color of the view to our yellow/orange hex value FFB234. In the size inspector, set the height to 75.

Figure 5.4.80 58.png

Figure 5.4.81

60.png

In the Object Library, find a Table View and drag one out onto our View Controller. Leave just a little space between the Table View and the UIView we added at the top because we are going to add a small drop shadow under the UIView. Also leave some space at the bottom, because we have to add our message Text Field and Send Button.

Figure 5.4.82 61.png

Add a UITextField and UIButton in the empty space at the bottom similar to the following screenshot.

Figure 5.4.83

62.png

Let's turn our attention back to our header view. Add a UIImageView and a UIButton inside the view we added at the top. Resize the image as needed, select our icon and set the Content Mode to aspect fit. Add the button on the right, change the color to white and the text to Log Out.

Figure 5.4.84

63.png

Click on your table view and then in the attributes inspector, make sure prototype cells is set to 1. If not, change it to 1. This adds a prototype cell on your view control to lay out your UI. In the prototype cell, add two labels, one for the username and the other for the message.

Figure 5.4.85 64.png

In the attributes inspector, I changed the label text as well as the font color and font style. For the font color, I used light gray for the username and dark gray for the message.

Next up, we need to add constraints. To get started, I am going to pin the header view to the left, right and top, as well as add a fixed height of 75.

Figure 5.4.86 65.png

I will pin our logo image to the left, the bottom of the header view and give it a fixed height and width. I will do the same for the Log Out button with the exception that it will be pinned to the right instead of the left. You get the idea here. I will leave it as an exercise for you to add in the other constraints. Just be sure you leave a little room between the table view and header view.

After laying all of the controls out and adding constraints, your completed MainVC should look something like the following:

Figure 5.4.87

66.png

We are almost there...let's move on, shall we?

Adding the code for the MainVC

To start, we need two @IBOutlets; one for our table view and one for our message text field.

Figure 5.4.88

import UIKit
import Firebase

class MainVC: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var messageTextField: UITextField!
}

I like to keep my code organized by using extensions for any protocols I need to conform to. We are going to be conforming to our DataServiceDelegate that we created earlier, so skip to the bottom of the file after the last curly brace and add our extension.

Figure 5.4.89

extension MainVC: DataServiceDelegate {
    func dataLoaded() {
        tableView.reloadData()
        if DataService.instance.messages.count > 0 {
            let indexPath = IndexPath(row: DataService.instance.messages.count - 1, section: 0)
            tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
        }
    }
}

In this extension, we implement our one required method dataLoaded(). In this method, we reload our table view data, and then check to see if the messages array in our Data Service actually contains any messages. If it does, we grab the index of the last item in our array and finally scroll the tableView to the bottom. This way, it will be similar to iMessage and other messaging apps in that the newest messages will be on bottom and the view automatically scrolls to the bottom.

Moving back up in our code, let's take a look at viewDidLoad().

Figure 5.4.90

    override func viewDidLoad() {
        super.viewDidLoad()

        DataService.instance.delegate = self

        tableView.delegate = self
        tableView.dataSource = self

        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil)

        let tap = UITapGestureRecognizer(target: self, action:
            #selector(dismissKeyboard))

        view.addGestureRecognizer(tap)
    }

Here, we set up our DataService and tableView delegates to be self (this object). We add a couple of NSNotificationCenter observers so we will be notified when the keyboard appears, as well as when it disappears. When we receive those messages, a new function that we will create shortly gets called to take some action. We also set up and add a tapGestureRecognizer to our view so that when our view is tapped, it will dismiss the keyboard, if present.

Our next bit of code is our first @IBAction in this vc.

Figure 5.4.91

    @IBAction func logOutButtonTapped(sender: UIButton) {
        do {
            try FIRAuth.auth()?.signOut()
            performSegue(withIdentifier: "showSignInVC", sender: nil)
        } catch {
            print("An error occurred signing out")
        }
    }

If our Log Out button is tapped, we simply log out of Firebase with the signOut() method and then immediately segue back to our SignInVC. This particular method can throw, so we set up the appropriate catch block to handle that if the need arises.

Our next and final @IBAcion is for our send button.

Figure 5.4.92

    @IBAction func sendMessageButtonTapped(sender: UIButton) {
        guard let messageText = messageTextField.text else {
            showAlert(title: "Error", message: "Please enter a message")
            return
        }
        guard messageText != "" else {
            showAlert(title: "Error", message: "No message to send")
            return
        }
        if let user = AuthService.instance.username {
            DataService.instance.saveMessage(user, message: messageText)
            messageTextField.text = ""
            dismissKeyboard()
            tableView.reloadData()
        }
    }

In this function, we guard against our text field being nil or empty once again. We check to make sure we have a good username, and if so, pass that as well as our message to our saveMessage method in our DataService singleton we set up earlier. Finally, we clear out our text field, dismiss the keyboard and reload our tableView data.

Next up, we have three functions that deal with the keyboard. The first two are functions that get called by our NSNotificationCenter observers we set up above. The third just simply dismisses the keyboard.

Figure 5.4.93

    func keyboardWillShow(notif: NSNotification) {
        if let keyboardSize = (notif.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
            if self.view.frame.origin.y == 0 {
                self.view.frame.origin.y -= keyboardSize.height
            }
        }
    }

    func keyboardWillHide(notif: NSNotification) {
        if let keyboardSize = (notif.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
            if self.view.frame.origin.y != 0 {
                self.view.frame.origin.y += keyboardSize.height
            }
        }
    }

    func dismissKeyboard() {
        view.endEditing(true)
    }

Our first observer function gets called when the keyboard is about to be shown. We get the size of the keyboard and then move the entire view up the size of the height of the keyboard if the view isn't already moved up.

The second function gets called when the keyboard is about to close. We simply reverse the process in this method. The dismissKeyboard function simply ends editing in the view and thus triggers the keyboard to hide.

The only remaining code before getting to our tableView methods is a showAlert function which is identical to the one we set up in SignInVC.

Figure 5.4.94

    func showAlert(title: String, message: String) {
        let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
        let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
        alertController.addAction(okAction)
        self.present(alertController, animated: true, completion: nil)
    }

Finally, we get to our tableView code. Here again, I like to put this in an extension to keep my code organized.

Figure 5.4.95

extension MainVC: UITableViewDelegate, UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return DataService.instance.messages.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let msg = DataService.instance.messages[(indexPath as NSIndexPath).row]
        if let cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell") as? MessageCell {
            if let user = msg.userId, let message = msg.message {
                cell.configureCell(user: user, message: message)
            }
            return cell
        } else {
            return MessageCell()
        }
    }
}

Here, we set our numberOfSections to 1. Next up, we set numberOfRowsInSection to the count of our messages array in our DataService. Finally I call cellForRow where I set up each individual cell. I am calling a custom UITableViewCell here, which we will create in just a second. We grab the message located at the index in question and then call dequeueReusableCell(withIdentifier:) to configure the cell. We are done with our MainVC, so let's create our custom UITableViewCell now.

Custom UITableViewCell

Right click on your View group in Project Navigator and add a new file. This will be a Cocoa Touch Class. Name the class MessageCell and make it a subclass of UITableViewCell. Open that file up in your editor and add the following code.

Figure 5.4.96

import UIKit

class MessageCell: UITableViewCell {

    @IBOutlet weak var userLabel: UILabel!
    @IBOutlet weak var messageLabel: UILabel!

    func configureCell(user: String, message: String) {
        userLabel.text = user
        messageLabel.text = message
    }
}

Here we have two outlets for our labels we added to our prototype cell. The configure cell method takes a user and a message. This is the method we called from dequeueReusableCell over in MainVC. It simply sets the labels text properties equal to the values passed in. Pretty simple, right?

There are just a couple of things left to do before we call this a wrap. First I want to create a custom Header View so that we can add a slight drop shadow on our header UIView. After that, it's just a matter of connecting all of our outlets and specifying our cell reuse identifier. Let's go ahead and create our custom header view now.

Custom UIView for our header

Right click on your View group once again and create a new file. This should be a Cocoa Touch Class class named HeaderView and a subclass of UIView. The code is very simple.

Figure 5.4.97

import UIKit

class HeaderView: UIView {

    override func awakeFromNib() {
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowOpacity = 0.7
        layer.shadowOffset = CGSize.zero
        layer.shadowRadius = 2
    }
}

In this file we simply adjust the layer shadow properties on our view. With this, we can set the custom class for our header and have a nice little shadow right under the header. This is a nice touch that makes it more appealing visually. To set the class on the header, click on the identity inspector and set the class to HeaderView.

Figure 5.4.98 67.png

Wiring things up

At this point, all that is really left to do is to make all of our IBOutlet and IBAction connections as well as specify our cell reuse identifier. For our cell reuse identifier, go back to the Storyboard and click on our table view cell in the Document Outline. In the Attributes Inspector, type in MessageCell in the box for the reuse identifier.

Figure 5.4.99 68.png

I normally don't like to make assumptions, however with this being an intermediate topic, I am going to make one here. I am going to assume that you are familiar with hooking up outlets and actions. Lets go ahead and walk through one and then I'll leave the rest for you to connect.

If I right-click on the file owner of my view controller in the document outline, I am presented with a dialog of all my actions and outlets. I can then click the circle next to the item and "drag" a connection to the control in the Storyboard.

If it is just an outlet, that is all you have to do. If it is an action, you are presented with one more dialog asking which action to connect it to. In the case of the button in my example here, that action would be touch up inside.

Figure 5.4.100 69.png

Take your time and make sure you have all of the outlets and actions connected. Be sure not to forget to connect the outlets in our custom TableViewCell. Once you are sure, go ahead and fire it up in the simulator or run it on your device. Put the app through its paces and see how it works. Log in with one session in your simulator and another session of your device with a different email address...talk to yourself.

Figure 5.4.101 iPhone Screenshots.png

Conclusion

Firebase is a really nice Backend as a Service (BaaS) for quickly and cost effectively setting up services for your apps to connect with. It would require a large amount of resources and time to set up your own realtime database, and yet Firebase offers that plus a lot more right out of the box.

Realtime Database, built in authentication, Cloud Messaging, Storage, Hosting, Notifications and other services are offered as a part of the package. As long as you are using one of the platforms which has a native SDK, it is hard to beat Firebase for setting up your backend services. The chat app we built in this chapter is a perfect example of the power and ease of use that Firebase offers.

Exercise

This is obviously a simplistic app, but it showcases the power of the Firebase Realtime Database and Authentication System.

As an Exercise, extend your version by adding/completing the following:

  • Currently, all chats are left justified in the tableView. Add a second prototype cell so that your messages can be justified to the right and others messages can be justified to the left.
  • Add the necessary code to your tableView to make the rows variable height. hint look into estimatedRowHeight and UITableViewAutomaticDimension.
  • Do some custom drawing to make chat "bubbles" similar to what a standard messages app might have.

Come up with some other cool ideas and extend it any way you see fit. All we ask is that if you do, send us an email or post something in our forums letting us know what you came up with...we would love to see it.