Why firebase?
Making a stand alone application is fun, we developers have access to a rich API provided from Apple to make beautiful design and intricate, elegant logic. However, for a modern app a way to persist data outside the actual device is needed.
Devices get lost or broken, apps can be uninstalled and so on. No developer wants users to lose their data. It may be a substantial collection of effort or memories. For this particular problem, Apple provides us with iCloud which is an elegant way of storing data outside the physical device.
Modern apps usually need to store data and be able to share it with friends so they can appreciate each other’s progress and accomplishments.. A network service used by an app used by more users is called a back end service. If we take a while to think about apps, it can be determine that there isn’t a back end service that fits all needs. Some apps are image oriented and need elaborate ways to store images, tag them, keep lists of persons portrayed in them, etc.. Other apps can be games, that need to store the player’s accomplishments, level of experience and other data that is particular to the specific game. Yet another segment of apps may be enterprise apps that log and help workers process actual commercial activity.
The usual way to create a back end service would be to acquire hosting on a physical computer that is always connected to the internet and write software that handles traffic. The preferred protocol for this task is HTTP and the most used programming language is PHP. This is a substantial endeavor, it can take up more time than writing the actual app. This approach is less suited for individual mobile developers than for teams that include dedicated back end developers.
In the past few years back end as a service options became available. These are great for reducing the time to market of mobile apps and have back end development time all but gone.
Firebase is a back end as a service provider. It’s also one of the best because of integrations bundled with it and ease of use. Firebase is free for development and affordable pricing plans are available once the app is used by more users. Other back end providers are available, but this one is ours for this tutorial.
A few words about JSON
Firebase uses JSON to store data. JSON stands for JavaScript Object Notation and is a data storage and transmission format. In a nutshell, a JSON object can be thought of as a dictionary in the way that it contains keys and values for those keys. Types that can be stored in JSON are:
- strings (eg.
"John Appleseed"
) - numbers (eg.
"3.14"
, but not “NaN”) - boolean (
true
,false
,True
,False
) null
the absence of value [represented bynil
in Swift]- arrays (a list of basic types, arrays, or dictionaries, eg.
["red", "blue", "yellow"]
) [represented asArray
in swift] - dictionaries (a key-value collection of basic types, arrays, or dictionaries)
An example JSON that can be used to represent a person could be:
{
"name": "Andrei",
"age": 20,
"favourite_colors": [
"red",
"blue"
],
"memories": {
"best": [
"Riding the bicycle",
"Building model boats"
],
"worst": null
},
"is_friendly": true
}
Another example with a list of todos:
[
{
"task" : "Buy milk",
"done" : false
},
{
"task" : "Eat a cookie",
"done" : true
}
]
To make a quick check that it’s a valid JSON, an online validator can be used such as JSON lint. Mismatching parentheses or missing quotes will yield malformed JSON errors. If you create your own keys, they must be UTF-8 encoded, can be a maximum of 768 bytes, and cannot contain ., $, #, [, ], /, or ASCII control characters 0-31 or 127.
Arrays or dictionaries contained in other arrays or dictionaries are called nested objects. Firebase limits the nesting level to 32. This is more of a safety limit than an obstacle, and shouldn’t be reached if we consider our objects reasonably.
In Swift, JSON objects are represented by Dictionary objects. To keep the Swift code clean, it’s good to define constants when dealing with any string type keys. It may be a bit of an overhead for small projects, but the amount of avoided spelling errors and clarity of the code is well worth it. Working with Firebase, parsing and validating JSON isn’t necessary, this discussion was to help grasp how grateful we should be.
App Overview
To demonstrate Firebase, we will present the making of “Task Burner”, a minimal todo list app. In the interest of time, the focus will be more on Firebase and less on UIView fundamentals, Auto Layout, Table Views and Object Oriented Programming techniques.
A minimum prerequisite is to have built an app that uses a UITableView
before. If you haven’t, go ahead and check out our awesome tutorials and return for some swift Firebase action. This is not to say that it’s absolutely mandatory, but it’s best not to get side tracked by details.
The best way to understand the works is to follow the steps described below. If you want to fast track through it or if you get stuck, feel free to try the sample project.
The best way to learn is by practice. If you get stuck or you just want to get you hand dirty with Firebase you can download the finished app:
Firebase iOS SDK Overview
The classes that we are going to work with are:
FIRApp
configures the Firebase SDK in the app. It reads the GoogleService-info.plist and starts up the required services. It can also take other values than the ones in the plist via direct calls.FIRAuth
handles the user’s signed in state. It holds a reference toFIRApp
. It can also sign out the current user.FIRDatabase
provides database access for the specific app. It produces database references, which are objects to be used when interacting with data. It also supports pausing and resuming operation (goOffline and goOnline). It also has a setting to back up operations on disk so that intermittent connections can be handled, thepersistenceEnabled
property.FIRDatabaseReference
provides access to a section of the database. It coordinates reads and writes within that particular section. It provides elegant observation of changes with callback blocks, it creates, updates and deletes values.FIRDataSnapshot
is a vehicle through witch Firebase provides JSON data to your app. Data is accessed in the same way like a normal dictionary. This complementsFIRDatabaseReference
, so the basic operations of create, read, update, delete are covered.
Step 1: Create a New Xcode Project
Firebase uses the app’s bundle identifier to associate your app with firebase.
- Create a new Xcode project as a single view application.
- Set a unique bundle identifier so your application won’t conflict with the sample application or other reader’s applications.
- Click Next to complete the process.You should have an empty, running application. To make sure that everything works out, Run it on your device to make sure everything is in order.
Step 2: Create a new Firebase Project
- Open the Firebase Console, sign in, and create a new project:
- Fill in the project’s name and region if applicable.
Once your Firebase project is set up, you will be able to add and remove one or more apps, as needed. All the apps that will be added to this particular project, will be able to access the data it holds.
Step 3: Add an iOS App to the Firebase Project
The Firebase project holds a database for one or more apps. This can be useful if a number of apps share the same data. For example the Android version of your app.
Another example would be a blogging platform composed of two apps: the Writer app is used to create and edit articles, it shares data with the Reader app which is used to rate them and comment on them.
- Set an unique bundle identifier for your app.
- Fill in the app’s bundle identifier that you selected when creating the project. You can obtain it from the Xcode project navigator.
The bundle id, is a unique identifier to an iOS application, so you should create a new one so as not to conflict with the one used as example.
- A “GoogleService-Info.plist” file download will be started, make sure to save it in a known place. This file contains all the settings Firebase needs to start and work. If you look in it, it contains keys for all the different services that Google provides bundled with Firebase.
- Click Continue to see the pod instructions. No need to save the Podfile contents, it will be provided shortly.And the same for the AppDelegate code sample.
- Check to make sure that the email sign in method is enabled (auth > sign in method > email)
Step 4: Integrate Firebase intro the iOS App
Firebase uses CocoaPods by default, which is a dependency manager. If you are not familiar with it, take a quick introto set it up.
- To make sure you have CocoaPods installed, open a terminal window, type pod –version and press Enter. You should get an output such as:
[email protected] ~ $ pod --version 1.0.0 [email protected] ~ $
- Create a new empty file and name it Podfile and save it next to your .pbxproj file.
- Paste the following in the Podfile:
inhibit_all_warnings! target "YourAppTargetHere" do pod 'Firebase/Core' pod 'Firebase/Database' pod 'Firebase/Auth' end
- Replace YourAppTargetHere with your actual app target.This Podfile is used by CocoaPods to determine which dependencies are needed for a project. It downloads code from trusted repositories and adds them to your project. This is to keep the actual app code short. Over a few months, when updates to installed pods will be released, developers can update pods to make sure they always use the latest and greatest versions.
- Close Xcode and open a terminal in the project’s folder. The easiest way is to drag and drop the project folder over the terminal icon in the dock.
- Run
“pod install”
in the project’s directory using a terminal. You should see the output similar to:[email protected] Task Burner$ pod install Analyzing dependencies Downloading dependencies Using Firebase (3.7.0) Using FirebaseAnalytics (3.4.3) Using FirebaseAuth (3.0.5) Using FirebaseCore (3.4.3) Using FirebaseDatabase (3.0.3) Using FirebaseInstanceID (1.0.8) Using GoogleInterchangeUtilities (1.2.2) Using GoogleNetworkingUtilities (1.2.2) Using GoogleSymbolUtilities (1.1.2) Using GoogleUtilities (1.3.2) Generating Pods project Integrating client project Sending stats Pod installation complete! There are 3 dependencies from the Podfile and 10 total pods installed. [email protected] Task Burner $
This will take a minute or two, and it will generate an .xcworkspace file. This is an Xcode Workspace and it can contain multiple .pbxproj files. You should use only the .xcworkspace in Xcode from now on.
- Xcode should be able to build the .xcworkspace project now.
- Once the project builds, drag and drop the GoogleService-info.plist you downloaded when creating the Firebase project in the Xcode Project.
- Add the following lines to AppDelegate.swift in the applicationDidFinishLaunching method:
FIRApp.configure()
- Add the keychain sharing entitlement to the build target in the Xcode project:
The keychain services API is a way for developers to use built in security features in Apple devices. To store secure information about users, applications call the keychain API to leverage specialized hardware and encrypt data. The keychain entitlement allows the iOS app (and Firebase auth) to store the authentication token and perform user registration and sign in.
Xcode should be able to build and run the project without errors. When running on the simulator, issues similar to nw_socket_set_common_sockopts setsockopt SO_NOAPNFALLBK failed: [42] Protocol not available, dumping backtrace:
can be ignored.
Step 5: Login Screen
Firebase provides authentication for the most common services: email, facebook, google, github and others. It provides efficient wrapping for the developers so we don’t need to bother with very low level details about the implementation.
- To create an authentication screen, create a new
UIViewController
subclass and name itMainViewController.swift
- In
Interface Builder
add a navigation controller and setMainViewController
as the root view controller of the navigation controller. - Set the Navigation Controller as the initial view controller from the Storyboard:
- Add two buttons and two text fields to create the interface:
- Add the
MainViewController
code:import UIKit import Firebase class MainViewController: UIViewController { static let kUserLoggedInSegueIdentifier = "userLoggedIn" /* @IBOutlets create references so strings can be read from the UITextFields */ @IBOutlet weak var emailField: UITextField! @IBOutlet weak var passwordField: UITextField! /* Have a reference to the last signed in user to compare changes */ weak var currentUser: FIRUser? @IBAction func login() { print("login") } @IBAction func register() { print("register") } @IBAction func signOut(segue: UIStoryboardSegue) { } }
- Going to the Connections inspector in the interface builder now shows the
@IBAction
members and@IBOutlet
properties and they can be linked to the elements. - Connect
emailField
andpasswordField
references to the respective interface builder elements. - Connect the
login()
andregister()
actions to the buttons. Make sure to select “Touch Up Inside”.Running the project now, it should build without errors and pressing the Login and Register buttons should print “login” and “register” in the debugger.
- A
UITableViewController
is the perfect component to show tasks in. The Table View Controller works by interrogating a data source to show cells and calls back a delegate to track the user’s actions. To add a table view controller in the interface builder, drag and drop one from theComponent Library
. - Create a push segue, by control-dragging from the source view controller to the new table view controller.
- Set the segue identifier to userLoggedIn.
Step 6: Handle User Registration
To create a user in Firebase, there’s a member to be called with the email and password parameters.
let email = “userEmail@email.com”
let password = “LoNgPaSsWoRd”
FIRAuth.auth()!.createUser(withEmail: email,password: password) {
user, error in
}
The call is asynchronous. This means that the callback closure will be called when it will be completed and the actual Firebase call won’t freeze the UI. An important note is to keep in mind that a login is performed after a user creation.
Step 7: Handle User Sign In
The sign in call syntax is similar:
FIRAuth.auth()!.signIn(withEmail: email, password: password)
As there isn’t any callback closure when the signIn call is completed, a state change listener should be in place when the call is performed.
if let auth = FIRAuth.auth() {
auth.addStateDidChangeListener() { auth, user in
if user != nil {
}
}
}
To get all the parts working in code, follow this example to fill in your MainViewController
:
import UIKit
import Firebase
class MainViewController: UIViewController {
static let kUserLoggedInSegueIdentifier = "userLoggedIn"
/* @IBOutlets create references so strings can be read from
the UITextFields
*/
@IBOutlet weak var emailField: UITextField!
@IBOutlet weak var passwordField: UITextField!
/* Have a reference to the last signed in user to compare
changes
*/
weak var currentUser: FIRUser?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let auth = FIRAuth.auth() {
/* Add a state change listener to firebase
to get a notification if the user signed in.
*/
auth.addStateDidChangeListener({ (auth, user) in
if user != nil && user != self.currentUser {
self.currentUser = user
self.performSegue(withIdentifier: MainViewController.kUserLoggedInSegueIdentifier,
sender: self)
}
})
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if let auth = FIRAuth.auth() {
/* Following the balance principle in iOS,
Stop listening to user state changes while not on screen.
*/
auth.removeStateDidChangeListener(self)
}
}
@IBAction func login() {
if let email = self.emailField.text,
let password = self.passwordField.text,
let auth = FIRAuth.auth() {
/* If both email and password fields are not empty,
call firebase signin
*/
auth.signIn(withEmail: email,
password: password)
}
}
@IBAction func register() {
if let email = self.emailField.text,
let password = self.passwordField.text,
let auth = FIRAuth.auth() {
/* Note: creating a user automatically signs in.
*/
auth.createUser(withEmail: email,
password: password) { user, error in
if error != nil {
self.present(UIAlertController.withError(error: error as! NSError),
animated: true,
completion: nil)
}
}
}
}
@IBAction func signOut(segue: UIStoryboardSegue) {
/* When Sign out is pressed, and the task list controller closes,
call Firebase sign out.
*/
if let auth = FIRAuth.auth() {
do {
try auth.signOut()
} catch {
self.present(UIAlertController.withError(error: error as NSError),
animated: true,
completion: nil)
}
}
}
}
Once you have all this, you can follow users creation and login on the firebase console.
Step 8: Tasks View Controller
- Click on the
Table View Cell
in interface builder and set it’s reuse identifier toTasksTableViewCell
- Create a new Cocoa Touch Class, subclassed from
UITableViewController
and name itTasksTableViewController
.
TasksTableViewController
code:
import UIKit
import Firebase
class TasksTableViewController: UITableViewController {
static let kTasksListPath = "tasks-list"
static let kTaskViewControllerSegueIdentifier = "TaskViewController"
let tasksReference = FIRDatabase.database().reference(withPath: kTasksListPath)
var tasks = [Task]()
var selectedTask: Task? = nil
weak var currentUser = FIRAuth.auth()?.currentUser
override func viewDidLoad() {
super.viewDidLoad()
/* Query tasks from firebase when this view controller is instantiated
*/
self.tasksReference.queryOrdered(byChild: Task.kTaskCompletedKey).observe(.value, with: { snapshot in
/* The callback block will be executed each and every time the value changes.
*/
var items: [Task] = []
for item in snapshot.children {
let task = Task(snapshot: item as! FIRDataSnapshot)
items.append(task)
}
self.tasks = items
self.tableView.reloadData()
})
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == TasksTableViewController.kTaskViewControllerSegueIdentifier {
/* Pass along the selected task to the particular task view controller.
*/
if let taskViewController = segue.destination as? TaskViewController {
taskViewController.taskTitle = self.selectedTask?.title
taskViewController.taskCompleted = self.selectedTask?.completed
}
}
}
@IBAction func prepareForUnwind(segue: UIStoryboardSegue) {
if let taskViewController = segue.source as? TaskViewController {
taskViewController.updateValues()
if let task = self.selectedTask {
/* This particular task was an existing one
it should be updated in Firebase.
*/
if let title = taskViewController.taskTitle,
let completed = taskViewController.taskCompleted,
let email = self.currentUser?.email {
let taskFirebasePath = task.firebaseReference
taskFirebasePath?.updateChildValues([
Task.kTaskTitleKey: title,
Task.kTaskUserKey: email,
Task.kTaskCompletedKey: completed
])
}
} else {
/* This task is new,
it should be created in Firebase
*/
if let title = taskViewController.taskTitle,
let completed = taskViewController.taskCompleted,
let email = self.currentUser?.email {
let task = Task(title: title, user: email, completed: completed)
let taskFirebasePath = self.tasksReference.ref.child(title.lowercased())
taskFirebasePath.setValue(task.toDictionary())
}
}
}
self.selectedTask = nil
}
@IBAction func addTask() {
self.performSegue(withIdentifier: TasksTableViewController.kTaskViewControllerSegueIdentifier, sender: self)
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.tasks.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TasksTableViewCell", for: indexPath)
if let c = cell as? TasksTableViewCell {
let task = self.tasks[indexPath.row]
c.titleLabel.text = task.title
c.completedSwitch.setOn(task.completed, animated: true)
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let task = self.tasks[indexPath.row]
/* Keep a reference to the task that is currently edited.
*/
self.selectedTask = task
self.performSegue(withIdentifier: TasksTableViewController.kTaskViewControllerSegueIdentifier, sender: self)
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let task = self.tasks[indexPath.row]
/* Delete this task.
No tableview updates should be done here because a callback notification
will be provided.
*/
task.firebaseReference?.removeValue()
}
}
}
Step 9: Add the Navigation Bar Items
To get all the code in place and the interface builder work completed, you need to create the remaining components. This will ensure all the navigation is complete and focus only on Firebase code after that.
A Navigation Bar is always a nice place to have buttons. It’s always visible while the table view scrolls and it’s dependent on the shown view controller. For the tasks app, a Sign Out button is needed and a button to add a new task. To get them set up, we already have the @IBAction links in code in place, so only Interface Builder work is needed.
- Add the
Sign Out
button. Drag and drop aBar Button Item
in the left slot of the Navigation Bar. - Control-drag from the
Sign Out
button to theExit
outlet represented by a red square at the top of theTasksTableViewController
. - Select
signOutWithSegue
.All this was to use so called Segues Unwinding. Notice that
@IBAction func signOut(segue: UIStoryboardSegue)
is in theMainViewController
. When the user will press the Sign Out button, the TasksListViewController will signal the MainViewController that it needs to disappear. This functionality may not be very intuitive, but it helps us to keep all the user authentication code in theMainViewController
. - Add an Add Task button similarly, just this time we need to call
@IBAction func addTask()
instead of Unwind Segue.
Step 10: Task Cell
Arguably, work can be done without creating a cell subclass, but it’s a good practice to have components separated. Once the project grows in size and more customization is needed, it’s much more manageable to have separate classes each with it’s own concern.
- Create a new Cocoa Touch Class, name it
TasksTableViewCell
and set it to be a subclass ofUITableViewCell
. - Code for
TasksTableViewCell
:
import UIKit
class TasksTableViewCell: UITableViewCell {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var completedSwitch: UISwitch!
}
Elements will be linked in Interface Builder
so that the rest of the code has easy access to them. In a more elaborate project, code would be added to handle cell states and operations. For this tutorial a conscious decision has been made to keep the Firebase code in the tasks list, so it’s easier to follow.
- Go to
Interface Builder
, and select theTable View Cell
in the tasks list created previously. Check that it already has the reuse identifier already set toTasksTableViewCell
(check by selecting the cell and going to theAttributes Inspector
tab). - In the Identity Inspector for the cell, fill in the
Custom Class
field to containTaskTableViewCell
. This associates the class you just made to the stock cell. - Add the
UILabel
andUISwitch
to the cell and comment the referencing outlets to the properties fromTaskTableViewCell
:
The app will handle the event of the user selecting a cell. This will create a problem for the Switch on it because it’s tap area will be small enough to miss. A solution for this would be to make the switch larger, so the user can set the task completed. Another solution, the one chosen for this tutorial, was to disable the user interaction with the UISwitch. So, the only option for the user to set the task completed is to go to the next screen and edit it.
Step 11: Task ViewController
To show the details of a Task
, a decision has been made to use a dedicated view controller. This helps in the case when more features will be needed in the future. For the example in our tutorial the Table View Cell with a switch would be enough, but completing chores is not as easy as we want it to be. Sometimes it’s actually a drag.
- Create a new Cocoa Touch Class, set it to be a subclass of UIViewController and name it TaskViewController.
- Fill in
TaskViewController
code:
import UIKit
class TaskViewController: UIViewController {
var taskTitle: String?
var taskCompleted: Bool?
@IBOutlet weak var titleField: UITextField!
@IBOutlet weak var completedSwitch: UISwitch!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.titleField.text = self.taskTitle ?? ""
self.completedSwitch.setOn(taskCompleted ?? false, animated:false)
}
func updateValues() {
self.taskTitle = self.titleField.text
self.taskCompleted = self.completedSwitch.isOn
}
}
The functionality we want from this component is to read and update the UI with values in viewDidAppear(_:)
. When it is dismissed, we want to read any changes the user has made in updateValues()
. There can be other ways to accomplish this, but this is one of the shortest. The most obvious way to improve it is to have a Task property later, and have the view controller read data straight from the task and update it in Firebase when dismissed.
To add it in interface builder: 3. Add a new ViewController 4. Control-drag from the TasksListViewController to it and select Present Modally. 5. Set the storyboard identifier to TaskViewController. 6. Set the view controller’s custom class to TaskViewController.
- Add two labels, an UITextField, an UISwitch and an UIButton. Set their titles as appropriate.
- Using the Connections inspector, connect the UITextField and UISwitch to their respective outlets.
- Control-drag from the button to the exit button at the top of the view controller and select
prepareForUnwindWithSegue
to enable Unwind Segue back to the TasksListViewController.
Right now the app should build without errors and have the navigation intact. You should be able to login, have a tasks list that shows one task. When you select that task or click the add button, the TaskViewController is shown. You should be able to exit screens all the way to the MainViewController.
Step 12: Task Model
Now that all the UI is out of the way, we can focus on more pertinent tasks. Firebase stores data in JSON format and it can be up to 32 levels deep. We should try to keep data structures as flat as possible. Keys for JSON values must be UTF-8 encoded, can be a maximum of 768 bytes, and cannot contain ., $, #, [, ], /, or ASCII control characters 0-31 or 127. If this sounds too pedantic, follow a rule of the thumb to use only letters, numbers, and underscores for keys. Eg. *my_precious_key_number_1″ instead of “my_◈♫#_1″. It’s more expressive and an established naming convention.
All applications have data structures that developers call models. A model is used in code as a source and storage for information. It’s possible to avoid having dedicated model classes, but it would lead to mayhem as developers would no longer be able to understand and keep track of code.
Because our application is a task manager, it makes sense to model our representation of a task following what we know from daily life. One could jot down a quick list of properties a task could have, similar to this:
Property Name | Swift Type | Explanation |
---|---|---|
id | String | An unique identifier. Other objects may have the same title or equal values, better to have a unique identifier at hand always. |
firebaseReference | FIRDatabaseReference | A firebase type used to reference our Swift object to the Firebase store. |
title | String | Makes sense to have a title. |
completed | Bool | Need to be able to complete tasks. |
user | String | A reference to the user that created it. |
priority | Double | How important is this task. eg. 0 — not important, 10 — very important |
urgency | Double | Some tasks may not be important, but very urgent. |
friends | [String] | Array of user identifiers who may participate in the same task. |
date | Number | The number of seconds since 1970. We can’t use Date to send to firebase, so conversion to a standard is needed. |
And many more properties could be added. For the purpose of our tutorial, we present the implementation for the first four. Arguably, the absolute minimal representation of a task: unique id, a firebase reference, a title and wether or not it’s completed.
- To create the Task model struct, create a new Swift File from Xcode and name it Task.swift.
- Fill in the
Task
code:
import Foundation
import Firebase
struct Task {
/* Have keys as constants to prevent spelling errors
* and avoid confusion. eg. "title" can be found in
* ViewControllers too, so places can exist where the
* particular string is used for something else.
*/
static let kTaskTitleKey = "title"
static let kTaskCompletedKey = "completed"
static let kTaskUserKey = "user"
let title: String
let user: String
let firebaseReference: FIRDatabaseReference?
var completed: Bool
/* Initializer for instantiating a new object in code.
*/
init(title: String, user: String, completed: Bool, id: String = "") {
self.title = title
self.user = user
self.completed = completed
self.firebaseReference = nil
}
/* Initializer for instantiating an object received from Firebase.
*/
init(snapshot: FIRDataSnapshot) {
let snapshotValue = snapshot.value as! [String: Any]
self.title = snapshotValue[Task.kTaskTitleKey] as! String
self.user = snapshotValue[Task.kTaskUserKey] as! String
self.completed = snapshotValue[Task.kTaskCompletedKey] as! Bool
self.firebaseReference = snapshot.ref
}
/* Method to help updating values of an existing object.
*/
func toDictionary() -> Any {
return [
Task.kTaskTitleKey: self.title,
Task.kTaskUserKey: self.user,
Task.kTaskCompletedKey: self.completed
]
}
}
We chose a struct instead of a class to deter too many classes making changes to the same object. The main difference is that a struct is passed by value, so when a change occurs, another object that has a copy won’t be affected.
A task has properties that hold actual data that are or may be useful for the app. There’s a initializer used to create a Task object from user input and another one from a firebase snapshot.
The FIRDataSnapshot
contains data from a Firebase Database location. Any time you read Firebase data, you receive the data as a FIRDataSnapshot
. A FIRDatabaseReference
can be thought of as an accessor to the firebase representation. It can be used for reading or writing data. The dictionary representation function is needed to communicate changes to firebase.
After creating this file, the app should work like before, but no noticeable change is present because the model isn’t integrated in code yet.
Step 13: Get the list of tasks from Firebase
As the “CRUD” principle goes, any useful data has the same lifecycle of creation, reading, updating and deletion.
- Creation: With Firebase, we define a database reference to the array of tasks, and then we create a task representation from a dictionary.
- Reading: We create a database reference for the list of tasks and register to observe changes.
- Updating: On the particular task’s
FIRDatabaseReference
, callupdateChildValues()
. - Deletion: Call
removeValue()
on the specific task. For creating, updating and deleting the task list observer will call back and update the app model.
- Add the following properties are needed in the
TasksTableViewController
:
var tasks = [Task]()
var selectedTask: Task? = nil
This creates an array of Task
objects which will be used to fill the Table View. The selectedTask
property will be used to keep track of which task the user selected.
2. To observe changes to the tasks list, query and observe it when the TasksTableViewController
is initialized.Place this code in a convenient place such as viewDidLoad()
self.tasksReference.queryOrdered(byChild: kTaskCompletedKey).observe(.value, with: {
snapshot in
var items: [Task] = []
for item in snapshot.children {
let task = Task(snapshot: item as! FIRDataSnapshot)
items.append(task)
}
self.tasks = items
self.tableView.reloadData()
})
The first line runs a query on the tasks list database reference. It then orders the results by child attribute, which returns a FIRDatabaseQuery
object. Then, the view controller registers as an observer to changes in this list’s values. Other FIRDataEventTypes
are available and can be used for more fine grained control. The closure is instantiating models from the data and feeding them to the UI.
Running right now, still wouldn't show created tasks in the table view because the tasks array is not integrated yet.
3. Replace the following code in the TasksListViewController:
class TasksTableViewController {
...
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.tasks.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TasksTableViewCell", for: indexPath)
if let c = cell as? TasksTableViewCell {
let task = self.tasks[indexPath.row]
c.titleLabel.text = task.title
c.completedSwitch.setOn(task.completed, animated: true)
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let task = self.tasks[indexPath.row]
/* Keep a reference to the task that is currently edited.
*/
self.selectedTask = task
self.performSegue(withIdentifier: kTaskViewControllerSegueIdentifier, sender: self)
}
...
}
The two members func tableView(_:numberOfRowsInSection:)
and tableView(_:cellForRowAt:)
are required by the UITableViewDataSource
protocol. They provide the number of rows and the cells for those rows.
The tableView(_:didSelectRowAt:)
member is required by the UITableViewDelegate
. It is called when the user tapped on a table view cell. The project is using it to present the TaskViewController.
Running the project now, would in theory be able to show tasks, but the code to create them is still missing.
Step 14: Add new Task
Thanks to the elegant model already created, the code snippet creating the task is 3 lines long.
let task = Task(title: title, user: email, completed: completed)
let taskFirebasePath = self.tasksReference.ref.child(title.lowercased())
taskFirebasePath.setValue(task.toDictionary())
First, a task struct instance is created with the user’s options. Then, the lowercase title is used as an identifier. It could be a UUID or some other value. The new reference is uploaded with all the task’s values.
Step 15: Edit Task
Updating done with updateChildValues()
on an existing reference. A partial dictionary can also be supplied.
task.firebaseReference?.updateChildValues([
kTaskTitleKey: title,
kTaskUserKey: email,
kTaskCompletedKey: completed
])
It’s always nice to have constants defined to take advantage of autocomplete and defeat little spelling errors.
Step 16: Integrating tasks creation and editing
To communicate between the TasksViewController and the single TaskViewController, the segues features can be used. prepare(for segue: UIStoryboardSegue, sender: Any?)
is called just before a segue is performed (read: before a View Controller is shown). It can be used to pass data to it. prepareForUnwind(segue: UIStoryboardSegue)
is the action you linked to the button previously. It’s called just before the segue is unwound and it can be used to read the user’s input.
- Replace the existing implementation with the code below in TasksListViewController:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == kTaskViewControllerSegueIdentifier {
/* Pass along the selected task to the particular task view controller.
*/
if let taskViewController = segue.destination as? TaskViewController {
taskViewController.taskTitle = self.selectedTask?.title
taskViewController.taskCompleted = self.selectedTask?.completed
}
}
}
@IBAction func prepareForUnwind(segue: UIStoryboardSegue) {
if let taskViewController = segue.source as? TaskViewController {
taskViewController.updateValues()
if let task = self.selectedTask {
/* This particular task was an existing one
it should be updated in Firebase.
*/
if let title = taskViewController.taskTitle,
let completed = taskViewController.taskCompleted,
let email = self.currentUser?.email {
let taskFirebasePath = task.firebaseReference
taskFirebasePath?.updateChildValues([
kTaskTitleKey: title,
kTaskUserKey: email,
kTaskCompletedKey: completed
])
}
} else {
/* This task is new,
it should be created in Firebase
*/
if let title = taskViewController.taskTitle,
let completed = taskViewController.taskCompleted,
let email = self.currentUser?.email {
let task = Task(title: title, user: email, completed: completed)
let taskFirebasePath = self.tasksReference.ref.child(title.lowercased())
taskFirebasePath.setValue(task.toDictionary())
}
}
}
self.selectedTask = nil
}
Running the code now should allow you to create tasks, set them completed or not, update titles. When swiping left on a cell and then tapping the delete button, nothing happens.
Step 17: Deleting Tasks
Call removeValue()
to end the object’s cloud ambitions. 1. Replace the tableView(_ tableView: UITableView, commit editingStyle:
implementation with the one below:
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let task = self.tasks[indexPath.row]
/* Delete this task.
No tableview updates should be done here because a callback notification
will be provided.
*/
task.firebaseReference?.removeValue()
}
}
A callback will be received in self.tasksReference.queryOrdered(byChild: kTaskCompletedKey).observe
closure and the table view will update.
The task deletion should work at this point.
Final TasksTableViewController
For reference or in case you missed a step, you can compare your work to the TasksTableViewController
full code:
import UIKit
import Firebase
let kTasksListPath = "tasks-list"
let kTaskViewControllerSegueIdentifier = "TaskViewController"
class TasksTableViewController: UITableViewController {
let tasksReference = FIRDatabase.database().reference(withPath: kTasksListPath)
var tasks = [Task]()
var selectedTask: Task? = nil
weak var currentUser = FIRAuth.auth()?.currentUser
override func viewDidLoad() {
super.viewDidLoad()
/* Query tasks from firebase when this view controller is instantiated
*/
self.tasksReference.queryOrdered(byChild: kTaskCompletedKey).observe(.value, with: { snapshot in
/* The callback block will be executed each and every time the value changes.
*/
var items: [Task] = []
for item in snapshot.children {
let task = Task(snapshot: item as! FIRDataSnapshot)
items.append(task)
}
self.tasks = items
self.tableView.reloadData()
})
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == kTaskViewControllerSegueIdentifier {
/* Pass along the selected task to the particular task view controller.
*/
if let taskViewController = segue.destination as? TaskViewController {
taskViewController.taskTitle = self.selectedTask?.title
taskViewController.taskCompleted = self.selectedTask?.completed
}
}
}
@IBAction func prepareForUnwind(segue: UIStoryboardSegue) {
if let taskViewController = segue.source as? TaskViewController {
taskViewController.updateValues()
if let task = self.selectedTask {
/* This particular task was an existing one
it should be updated in Firebase.
*/
if let title = taskViewController.taskTitle,
let completed = taskViewController.taskCompleted,
let email = self.currentUser?.email {
let taskFirebasePath = task.firebaseReference
taskFirebasePath?.updateChildValues([
kTaskTitleKey: title,
kTaskUserKey: email,
kTaskCompletedKey: completed
])
}
} else {
/* This task is new,
it should be created in Firebase
*/
if let title = taskViewController.taskTitle,
let completed = taskViewController.taskCompleted,
let email = self.currentUser?.email {
let task = Task(title: title, user: email, completed: completed)
let taskFirebasePath = self.tasksReference.ref.child(title.lowercased())
taskFirebasePath.setValue(task.toDictionary())
}
}
}
self.selectedTask = nil
}
@IBAction func addTask() {
self.performSegue(withIdentifier: kTaskViewControllerSegueIdentifier, sender: self)
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.tasks.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TasksTableViewCell", for: indexPath)
if let c = cell as? TasksTableViewCell {
let task = self.tasks[indexPath.row]
c.titleLabel.text = task.title
c.completedSwitch.setOn(task.completed, animated: true)
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let task = self.tasks[indexPath.row]
/* Keep a reference to the task that is currently edited.
*/
self.selectedTask = task
self.performSegue(withIdentifier: kTaskViewControllerSegueIdentifier, sender: self)
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let task = self.tasks[indexPath.row]
/* Delete this task.
No tableview updates should be done here because a callback notification
will be provided.
*/
task.firebaseReference?.removeValue()
}
}
}
Persistence
Firebase has an option for persistence, which is a major prize for us developers. It doesn’t save data between app restarts, but saves changes in memory when the connection to the servers is unstable. This is compassionate for us because we don’t need to spend vast amounts of time to think and implement good solutions to keep our app stable. However, for persistence across restarts when the connection is offline, custom solutions are required.
To turn on persistence, add the following line in AppDelegate
:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
...
FIRDatabase.database().persistenceEnabled = true
}
Take a look at our example project
- Download the finished project:
Task Burner
- Drag and drop your
GoogleService-Info.plist
generated from Firebase Console. (See Step 3) - Replace the bundle id so you can follow the changes in your own firebase console.
The largest parts of the project and their functions:
Task.swift
contains the model objectMainViewController
is responsible for signup and login.TasksTableViewController
shows the list of tasks and triggers actions with them.TaskViewController
is used to create a new task or edit an existing one. When you feel confident enough, have a stab at our exercises below.
Conclusion
This tutorial covered the basic authentication and database aspects of Firebase. These are the basic notions to use in any app that relies on Firebase. Firebase itself is a modern, out of the box service for application developers. It’s free for 100 simultaneous connections and it’s affordable as your apps become App Store hits.
Other Firebase features are:
- Firebase Cloud Messaging (FCM) is a cross-platform messaging solution that lets you reliably deliver messages at no cost.
- Firebase Analytics is a free app measurement solution that provides insight on app usage and user engagement.
- Firebase Storage is built for app developers who need to store and serve user-generated content, such as photos or videos.
- Firebase Hosting provides fast and secure static hosting for your web app.
- Firebase Test Lab: Test your app on devices hosted in a Google datacenter.
- Firebase Crash Reporting: Comprehensive and actionable information to help diagnose and fix problems in your app.
- Firebase Notifications is a free service that enables targeted user notifications for mobile app developers.
- Firebase Remote Config: Change the behaviour and appearance of your app without publishing an app update.
- Firebase App Indexing gets your app into Google Search.
- Firebase Dynamic Links are deep links that survive the install process, which can convert your mobile web users to native app users, and increase conversion for user-to-user sharing.
- Firebase Invites are an out-of-the-box solution for app referrals and sharing via email or SMS.
- Google AdWords: Reach potential customers with online ads.
- AdMob by Google is an easy way to monetize mobile apps with targeted, in-app advertising.
Read more on Firebase Docs.
Exercises
To further develop your new Firebase skills try implementing the following:
1. Add property for the task to keep track of importance and show it in the table cell and edit screen.
2. Notice in the TasksTableViewController
that the tasks ordered query doesn’t take into account the task’s user property. Filter them out to show only the current user’s tasks.
3. Updating tasks on the firebase console while the app is showing the TaskViewController
doesn’t reflect changes in the app. Update the TaskViewController
to respond to remote changes.
Thanks for reading!
Thank you for taking the time to read my first tutorial!
My name is Andrei Nagy and I’ve been a iOS developer for the past 4 years. In 2015 I founded Logic Pods a small iOS development company located in Cluj Napoca.
We focus on quality, responsiveness and continuous improvement in order to make beautiful apps for our customers.
I hope this tutorial will help you get started with Firebase. Feel free to ask any questions you might have or send feedback in the comments section below.
If you enjoyed this tutorial please take a few seconds and share it with your friends!
Great job Andrei!
Enumerations are also good for modeling constants. And an enum with a raw type of String will automatically assign the case name as the raw value if you don’t manually assign it.
Also, have you checked out RxSwift? I think of RxSwift as a natural extension of Firebase (and other socket-based services), used together to create completely reactive apps, from device, to cloud, and back.
Hi Scott,
Enums are indeed a sound way to manage constants in apps. The choice here was to keep it easy to follow for learners.
RxSwift can be the subject of it’s own tutorial
Thank you for your interest in my tutorial!
Excellent tutorial Andrei. I plan on using it for a iOS course I am teaching in the college.
Thank you!
Excellent tutorial Andrei. Congratulations
There is a typo where Let constant starts with capital L.
Nice catch, thanks!
Good explanation of the basics but does not address sharing private data
The Table has a variable for an array of friends but the app does not use the array.
All of the tutorials on firebase I have seen do not address the issue of how to restrict the data to an invited group of friends. All todo type apps need data to be restricted to invited friends. How this issue is addressed is very poorly documented in Firebase.
Hi John,
Thank you so much for your feedback. The post is more of a primer to get started and basic notions are explained.
About restricting data, you can check out the firebase security docs. A quick and straight forward implementation would be to have tasks per user and a user to have a “shared tasks” array where invited tasks are stored to. But this depends on the way you want your app to work, are tasks owned forever or do they get transferred at some point.
Probably one of the most thoughtful tutorial I have seen in terms of its organization, its easy step through
at a glance. This is well appreciated.
Hi, i’ve used your wonderful code and added a img: String to view an image under the title, everything works fine except when i try to view the image in a uiimageview in taskviewcontroller whit this code:
let url = NSURL(string:taskImg!)
let data = NSData(contentsOf:url! as URL)
if data != nil {
imagePicked.image = UIImage(data:data! as Data)
}
if i print url the url is perfect but picture doesn’t show up, the only way to let it work is to set firebase rules without authentication, is there any other way?
Hi Marco,
Sorry for the late reply. If it works without authentication rules, then you would either need to have the user authenticated or setup the rules so authentication is not required.
Thanks for reading!
This is exactly the type of tutorial I needed as my data record was not you boring simple Dictionary (key: pair) record.
I love your flow of thoughts and Ideas, it was do direct to implement.
Please provide me interesting cases as expansion, if you can with Firebase mobile user authentication and reminders on tasks as sms. Just as a practical expansion, what do you think?
Anyhow, simply awesome!