Advanced Firebase For The Win

Introduction

With Firebase 101 we introduced the basic features of firebase and most of it’s database features.

With this tutorial, we’re continuing the presentation of Firebase with some more advanced topics:

  • Authentication: In most apps, some data needs to be restricted to certain users. A user may want to share information with his friends, but may not like to have that information public.
  • Advanced database models: Apps often work with more than one model object, and in between those objects ownership relations occur.
  • Storage: As an added bonus, we will show how to upload images to firebase too.
  • Security rules: Restrict data to a set of users.

Because of the complexity of the presented items, we’ll insist more on discussing the principles than the code walkthrough. However, pertinent snippets of code will be explained.

The prerequisites for this tutorial are some familiarity with Swift, making tableviews, the Interface Builder and Firebase 101. We’ll also make use of the UIImagePickerController, so you could look at that tutorial too.

You don’t need to be an expert on these, but it helps in enjoying the good part of this tutorial instead of being side tracked by the details.

The amount of information is large, the complexity of the app is higher than a regular tutorial and real world software development skills have been used in it’s development. To keep up with this tutorial, you should have a firm grasp of the basics. Knowing how to use a version control system such as Git would be an infinite advantage because of being able to save (commit) your progress and discard (reset) changes that don’t work.

As with any complex app, an iterative development model was used to reach this finished example. This means that we split the larger problem into a set of small ones, and then started solving them in turn. Each of these turns require modifications on previous ones, so it wouldn’t be fair to give a roadmap and leave the assumption that it was written from start to finish in one sitting without any fails in between.

Run the sample project

To run the project, you would need the code, create a firebase app and plug in the google services plist.

Download the project

  • Go to firebase console
  • Create a new project
  • Create a new app
  • Download the GoogleService-Info.plist and drag and drop it to your project.
  • Go to the Authentication > Sign in method and enable the Email/Password method.
  • Open Xcode
  • Click Run on the simulator or device.
  • Once the app starts, use any pair of credentials such as “[email protected]” and a random password and sign up.

You should see the MainTableViewController with the main menu. For best results, you could use two devices as the whole app is designed to help users get together.

Before we begin

Check out the sample JSON

To get a better grasp on the data structures used, download and Sample JSON. Open firebase, select database and then import JSON on the right hamburger menu.

General Firebase Guidelines

Firebase uses a JSON structure for storage. It is limited at 32 levels deep, so we need to keep it shallow. On sign up, the app will have access to the current user’s UID. Based on the UID, a child for the specific user will be created in the database. Associations between objects will be done via child keys. So a user’s friendship will be represented by the friend’s key present at a certain path in his properties.

  "users" : {
    "Kramer" : {
      "email" : "[email protected]",
      "friends" : {
        "Newman" : true,
        "Seinfeld" : true
      },
      "uid" : "Kramer"
    },
    
    "Seinfeld" : {
      "email" : "[email protected]",
      "friends" : {
        "Kramer" : true
      },
      "uid" : "Seinfeld"
    }
  }

In the above example, actual ids have been replaced for legibility. Firebase generates random unique ids so we won’t have to worry about unintended matches.

Security Part 1

Firebase allows us to set custom security rules for data. At a first glance, one may ask himself “Meh, why would I care? I just want to get my app working first, it’s just a toy project anyway.” The trouble is that it’s burdensome to fix bad design than to do it right the first time. It’s worth a few hours of your time to study firebase security and then proceed with a valuable tool in your arsenal.

Go to Database > Rules and consider the following set of rules:

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

Click on the simulator tab and, select a Read and run the command while simulating being authenticated or not. You should see that the read fails when the simulation is run without authentication.

 

This is all fine and dandy, it means that a user that isn’t authenticated can’t read or write to the database. This is not necessarily all about protecting the app from the HaxxxorM4||. There can be any number of situations where the security rules are the last line of defence against bugs, problematic states in the app and so on. Consider that most modern software products have a web component, an iOS version, an Android version and maybe other smartphones operating systems versions. It would be totally unpleasant to have users accidentally lose their data because some awkward state wasn’t handled properly on one of the clients. Security rules don’t readily protect data from broken code, but they are very useful nevertheless.

 

Security rules run atomically. This sounds very radioactive, but it means that a rule must be readily available for a certain path (and any child rules won’t be evaluated). Let’s simulate the following example:

{
  "rules": {
    "records": {
      "rec1": {
        ".read": true
      },
      "rec2": {
        ".read": false
      }
    }
  }
}
  • Simulate /records/ fails
  • Simulate /records/rec1 succeeds
  • Simulate /records/rec2 succeeds

Reading the /records/ fails because there is no explicit rule for it. Don’t be ashamed if you would have expected /records/rec1 to be available, I did too.

Another valuable information is that security rules are not filters. As such, we’ll need other ways to keep subsets of data available for users.

WildFun app overview

Let’s prepare an app to help friends get together at parties. Users would make accounts, add each other as friends. A user would schedule to host a party, add a description such as “Dungeons and Dragons open” and invite his friends. The friends would need to announce that they can participate.

The app’s doesn’t seem so complex. Searching the app store for something like this certainly yields quite a few results. This app isn’t revolutionary in any way, so why would implementing it and understanding it take anything over a full 10 minutes?

Let’s consider the app in more detail. To make the process easier we can list a few user stories:

  • A user logs in the app and sends a friend request.
  • The friend accepts or denies the friend request.
  • A user can schedule a party and all his friends are automatically invited.
  • Only the partie’s owner can edit party information.
  • Friends can attend or announce they can’t attend the party.
  • Friends attending a party can post pictures about the party.
  • Users that aren’t friends can’t see other user’s parties.

As always with apps, synthesizing the objects needed and the interactions between them is a delicate question. To ease the decision making let’s agree on a few assumptions:

  • There won’t be many users using the app, so complexity and speed isn’t critical.
  • The app is intended for code clarity, so we will prefer legibility over too much abstraction.

After thinking about the app, the way it should work, the mist clears over the models and classes the app needs to achieve it’s goal:

  • A Login View Controller.
  • A Navigation Controller with cells for Parties, Friends, Messages.
  • A User struct with it’s own path in Firebase.
  • A Party struct with it’s own path in Firebase.
  • A Message struct with it’s own path in Firebase.

Firebase Implementation details

Firebase is started up in the AppDelegate.swift with the FIRApp.configure() line. It reads the GoogleService-info.plist and allows Firebase to be started. Three components from Firebase will be used:

Firebase auth

Firebase auth which provides us with a way to manage users. The users panel is available in the Firebase console > Authentication > Users.

The main points of this (described more in length in Firebase 101) are:

  • Login and create user, `FIRAuth.auth().signIn(withEmail:password)
  • The state listener, FIRAuth.auth().addStateDidChangeListener()

Firebase database

Firebase database is the main model storage functionality. The data is available in the Firebase console > Database, Data and Rules tab.

The absolute basics of using the database are:

  • FIRDatabaseReference which points to a specific path in the database.
  • FIRDataSnapshot provides an interface that contains objects values, similar to a Swift Dictionary.
  • aCertainReference.queryOrderedByKey().observe(.value, with: { snapshot ... which is used to update the application with initial values and changes.
  • Creating database entries: firebaseReference = aCertainPathReference.child(), and then set values with firebaseReference.setValue([String: Any])
  • Deleting firebase entries: firebaseReference.removeValue()
  • Instantiating models is done via an optional initializer such as init?(snapshot: FIRDataSnapshot)

Firebase storage

Firebase storage will be used to store party photos. We’ll use it for party photos and works in about the same way as the database:

  • FIRStorageReference points to a specific path in the cloud.
  • FIRStorageMetadata is a data type to provide additional information about the data.
  • aCertainReference.put(data, metadata:...) is used to upload photos.
  • aCertainReference.data(withMaxSize:... is used to download the data.
  • A minor diference to the database is that storage references don’t have .childByAutoId(), so Foundation’s UUID().uuidString can be used to create unique keys.

This powerful Firebase feature can be used to upload and host a number of files on the cloud.

Swift project implementation details

Main UI

It’s implemented similar to Firebase 101. A storyboard with viewcontrollers, show and unwind segues. Tableviews have been chosen to show data because of the straight-forward implementation. Tableviews with static cells don’t require any delegate protocols to be implemented. Segues and custom UI with IBOutlets cand be used throughout the cells. The only limitation is that a TableViewController must be used.

Extensions

For ease of access, we’re using an AlertController-Extension.swift:

import Foundation
import UIKit

extension UIAlertController {
    
    static func withError(error: NSError) -> UIAlertController {
        let alert = UIAlertController(title: "Error",
                                      message: error.localizedDescription,
                                      preferredStyle: .alert)
        
        let cancelAction = UIAlertAction(title: "Ok",
                                         style: .default)
        alert.addAction(cancelAction)
        return alert
    }
    
    static func withMessage(message: String) -> UIAlertController {
        let alert = UIAlertController(title: nil,
                                      message: message,
                                      preferredStyle: .alert)
        
        let cancelAction = UIAlertAction(title: "Ok",
                                         style: .default)
        alert.addAction(cancelAction)
        return alert
    }
}

And an UIImage-Extension.swift:

import Foundation
import UIKit

extension UIImage {
    
    func resized(width: CGFloat) -> UIImage? {
        let image = self
        let height = CGFloat(ceil(width/image.size.width * image.size.height))
        let canvasSize = CGSize(width: width, height: height)
        UIGraphicsBeginImageContextWithOptions(canvasSize, false, image.scale)
        defer { UIGraphicsEndImageContext() }
        image.draw(in: CGRect(origin: .zero, size: canvasSize))
        return UIGraphicsGetImageFromCurrentImageContext()
    }
}

Model organization

The basic objects needed to build this app are Users, Messages and Parties. Firebase needs data to be as flat as possible, so keys would be used for each of these groups and nesting is to be avoided. The association between objects will be done by keys, so user1234 would have his friend’s uid, user2345, in his friends key. Whenever user2345 data would need to be displayed, the users/user2345 path would be read.

Any developer would be temped to instantiate all objects at once, all users, all the messages, all the parties from firebase in the app and then establish references between them. That would yield a rich object graph, everything would be accessible from anywhere and life would be much easier at a first look. At a second glance, it would violate the Single source of truth principle and keeping it in sync with the back end would be riddled with edge cases and trouble. All information presented in the app should originate from Firebase which is a few milliseconds old and not minutes or hours.

The communication between the users is done via message objects. When a friend request, a party invitation or an image is uploaded, a message will be sent to a recipient or party owner. The party owner will then update the party with the new data. This may be a bit on the paranoid side, but it’s partly done like this to demonstrate strict security practices.

For easier management of Firebase specific code, we created: FirebaseConstants.swift that contains string constants for paths and a number of helper functions.

FirebaseConstants.swift

import Foundation
import FirebaseDatabase
import FirebaseStorage

let FirebasePathSeparator = "/"
let FirebasePhotosPath = "photos"
let FirebaseEmptyValue = "null"

enum FirebasePaths: String {
    case messages = "messages"
    case users = "users"
    case parties = "parties"
}

enum FirebaseMessagesKeys: String {
    case from = "from"
    case to = "to"
    case party = "party"
    case photo = "photo"
    case messageType = "messageType"
    case messageContent = "messageContent"
}

enum FirebaseUserKeys: String {
    case uid = "uid"
    case email = "email"
    case friends = "friends"
    case parties = "parties"
    case photos = "photos"
    
    case ownedParties = "ownedParties"
    case attendingParties = "attendingParties"
}

enum FirebasePartiesKeys: String {
    case guests = "guests"
    case photos = "photos"
    case description = "description"
    case owner = "owner"
}

func createFirebaseReference(components: [Any]?) -> FIRDatabaseReference? {
    if let path = firebasePath(components: components) {
        return FIRDatabase.database().reference(withPath: path)
    }
    return nil
}

func createFirebaseStorageReference(components: [Any]?) -> FIRStorageReference? {
    if let path = firebasePath(components: components) {
        let storage = FIRStorage.storage()
        let reference = storage.reference(withPath: path)
        return reference
    }
    return nil
}

fileprivate func firebasePath(components: [Any]?) -> String? {
    guard let components = components else {
        return nil
    }
    
    var strings = [String]()
    for thing in components {
        if let string = thing as? String {
            strings.append(string)
        }
        
        if let path = thing as? FirebasePaths {
            strings.append(path.rawValue)
        }
    }
    
    if strings.count > 0 {
        return strings.joined(separator: FirebasePathSeparator)
    }
    
    return nil
}

/* Converts a dictionary of keys and bools to an array of strings with "true" keys
 */
func stringsArrayWithTrueKeys(snapshotValue: Any?) -> [String]? {
    var result: [String]? = nil
    
    if let dict = snapshotValue as? [String: Bool] {
        
        var keys = [String]()
        for (key, value) in dict {
            if value == true {
                keys.append(key)
            }
        }
        
        if keys.count > 0 {
            result = keys
        } else {
            result = nil
        }
    }
    return result
}

[collapse]

And a FirebaseModel.swift protocol which contains all the common features a model struct should have:

import Foundation
import FirebaseDatabase

protocol FirebaseModel {
    init?(snapshot: FIRDataSnapshot)
    func toDictionary() -> [String: Any]
    func removeFromFirebase()
    mutating func createInFirebase()
}

Security Part 2

In Firebase rules, there are a few keywords and values:

  • auth stands for the currently authenticated user that’s trying to access the data.
  • $someSpecifficKey stands for an element in a list. For example messages/$message stands for a each object in the messages list.
  • data is the actual value of an item. In the context of a message, data.child('form') equates to the sender’s UID field of a message.

Let’s consider the security rules needed for this project:

{
	"rules": {
		"messages": {
			"$receiver": {
				".read": "$receiver === auth.uid",
				"$message": {
					".write": "!data.exists() || data.child('from').val() === auth.uid || data.child('to').val() === auth.uid"
				}
			}
		},
		"parties": {
			"$party": {
				".write": "!data.exists() || data.child('owner').val() === auth.uid",
				".read": "data.child('owner').val() === auth.uid || data.child('guests/'+auth.uid).val() == true"
			}
		},
		"users": {
			".read": true,
			"$uid": {
				".write": "$uid === auth.uid"
			}
		}
	}
}

Messages:

  • As messages are organized by recipient, the recipient message key is readable by the sender and receiver.
  • Any user (!data.exists()) can create messages, but only the sender and recipient can update or delete them.
  • The message root key won’t be available for querying as there is no read rule for it.

Parties:

  • Any user can create and only the owner can write to a party.
  • Only owners and confirmed guests can read parties keys.
  • All existing parties can’t be queried.

Users:

  • Only the user with the current id can change his info.
  • Any user can query all existing users. We allowed all users to access the users path to be able to get an uid by an email address in the friend request.

Feel free to test these out in the Firebase rules simulator.

Other available security rules are:

  • .validate, defines rules for a correctly formatted value type. A validation rule for parties might be ".validate" : "newData.child('owner').isString()"
  • .indexOn, specifies a data child to index to support for querying. An indexOn rule for users might be ".indexOn": "email"

See more on Firebase rules in the official documentation linked in the references section.

Users

The current user

Firebase provides the FIRAuth.auth()?.currentUser?.uid accessor for the currently signed in user. Around this, we can write a struct with encapsulates the uid and provides additional help to update the current user’s keys.

CurrentUser.swift

import Foundation
import FirebaseAuth
import FirebaseDatabase
import FirebaseStorage
import UIKit

struct CurrentUser {
    
    let uid: String
    
    init?() {
        guard let uid = FIRAuth.auth()?.currentUser?.uid else {
            return nil
        }
        self.uid = uid
    }

    func friendsPath() -> String {
        return [FirebasePaths.users.rawValue,
                self.uid,
                FirebaseUserKeys.friends.rawValue].joined(separator: FirebasePathSeparator)
    }
    
    func addFriend(uid: String, value: Bool) {
        if let path = CurrentUser()?.friendPath(uid: uid) {
            let reference = FIRDatabase.database().reference(withPath: path)
            reference.setValue(value)
        }
    }
    
    private func friendPath(uid: String) -> String {
        return [FirebasePaths.users.rawValue,
                self.uid,
                FirebaseUserKeys.friends.rawValue,
                uid].joined(separator: FirebasePathSeparator)
    }
    
    func addPicture(image: UIImage, firebasePhoto: Photo) {
        if let reference = firebasePhoto.storageReference() {
            
            if let resizedImage = image.resized(width: 300),
                let data = UIImagePNGRepresentation(resizedImage) {
                
                reference.put(data, metadata: Photo.metadata())
                
                if let reference = createFirebaseReference(components: [self.path(), FirebaseUserKeys.photos.rawValue, firebasePhoto.key]) {
                    reference.setValue(true)
                }
            }
        }
    }
    
    func path() -> String {
        return User.pathFor(uid: self.uid)
    }
    
    func setOwner(partyKey: String) {
        let reference = createFirebaseReference(components: [self.path(), FirebaseUserKeys.ownedParties.rawValue, partyKey])
        reference?.setValue(true)
    }
    
    func addAttendingParty(partyKey: String) {
        let reference = createFirebaseReference(components: [self.path(), FirebaseUserKeys.attendingParties.rawValue, partyKey])
        reference?.setValue(true)
    }
    
    func removeAttendingParty(partyKey: String) {
        let reference = createFirebaseReference(components: [self.path(), FirebaseUserKeys.attendingParties.rawValue, partyKey])
        reference?.setValue(false)
    }
    
    func addGuestFrom(message: Message) {
        var value = false
        if message.messageContent == .accept {
            value = true
        }
        
        if let partyKey = message.party {
            let path = Party.pathForGuests(key: partyKey)
            let reference = FIRDatabase.database().reference(withPath: path)
            reference.setValue([message.from: value])
        }
        
        message.removeFromFirebase()
    }
    
    func addFriendFrom(message: Message) {
        var value = false
        if message.messageContent == .accept {
            value = true
        }
        
        self.addFriend(uid: message.from, value: value)
        
        message.removeFromFirebase()
    }
}

[collapse]

The current user struct is an accessor to it’s uid, we don’t consider it a user object.

The generic user

To help organize users in firebase, we will create keys for each new user that signs in.

User.swift

import Foundation
import FirebaseDatabase

struct User: FirebaseModel {
    let uid: String
    let email: String
    var firebaseReference: FIRDatabaseReference?
    let ownedPartiesKeys: [String: Any]?
    let attendingPartiesKeys: [String: Any]?
    
    init(uid: String, email: String) {
        self.uid = uid
        self.email = email
        self.ownedPartiesKeys = nil
        self.attendingPartiesKeys = nil
    }
    
    init?(snapshot: FIRDataSnapshot) {
        if let snapshotValue = snapshot.value as? [String: Any],
            let uid = snapshotValue[FirebaseUserKeys.uid.rawValue] as? String,
            let email = snapshotValue[FirebaseUserKeys.email.rawValue] as? String
        {
            self.uid = uid
            self.email = email
            
            self.ownedPartiesKeys = snapshotValue[FirebaseUserKeys.ownedParties.rawValue] as? [String: Any]
            self.attendingPartiesKeys = snapshotValue[FirebaseUserKeys.attendingParties.rawValue] as? [String: Any]
            
            self.firebaseReference = snapshot.ref
        } else {
            return nil
        }
    }
    
    func toDictionary() -> [String: Any] {
        return [
            FirebaseUserKeys.uid.rawValue: self.uid,
            FirebaseUserKeys.email.rawValue: self.email,
        ]
    }
    
    func removeFromFirebase() {
        self.firebaseReference?.removeValue()
    }
    
    mutating func createInFirebase() {
        let reference = FIRDatabase.database().reference(withPath: FirebasePaths.users.rawValue)
        self.firebaseReference = reference.child(self.uid)
        self.firebaseReference?.setValue(self.toDictionary())
    }
    
    static func pathFor(uid: String) -> String {
        return [FirebasePaths.users.rawValue, uid].joined(separator: FirebasePathSeparator)
    }
    
    func path() -> String {
        return User.pathFor(uid: self.uid)
    }
}

[collapse]

On a device with a user signed in, a CurrentUser object would have an analogous User object. This separation makes sense so one would have uniform objects for all users, local and friends alike.

The messaging system

Messaging system specification

In firebase, and in any complex client-server back end, users should own specific parts of data and share some of it.

Considering parties, for example, a user hosts it and other users attend it. It stands to reason that not all users should be able to modify any part of the party information.

Most of the time, applying the way things work in the real world to app models makes development a lot easier and saves plenty of problems. In this particular case, the concept of ownership suits the problem well.

Let’s consider that the host should own the party. Following this rule, for the purpose of this app, only the owner should be able to change party information and invite his friends.

This saves a lot of effort because we’re already familiar with the concept of hosting a party and that the host is usually organizing it.

To put things into perspective, one may notice that friends of the user may find it useful to be able to invite other friends as well. This is a valid feature request, for this tutorial, we’ll do without it.

So how can a friend attend a party? Let’s consider what would happen in a real party planning situation: The host decides to have a party. It has a theme, a date, and so on. He then proceeds to invite his friends to the party. The friends consider their schedules and accept or refuse the invitation. The original host may then see how many friends accepted the invite and could decide to invite more friends to reach a certain number of guests.

It seems obvious now that all the changes to the party should be done by the host’s user. Having it any other way would imply an exponential increase in complexity in the app’s code.

All this could be accomplished by having a messaging system. Let’s consider each invitation to be a message. Friends could then answer the invitation by accepting or declining it.

For the messaging system implemented in the app, the way regular email works was chosen as the reference. Analyzing the email system we can distinguish the following traits

  • Messages are sent to any user’s inbox (not only to friends)
  • The receiver of the message owns the message
  • Messages have a subject
  • Messages have a content

Other messaging models such as Facebook, skype, may use slight variations of this. Messages could be deleted after being sent. They only have a string content.

Modeling the WildFun messaging system the same way email works allows us to reason easily about it and answer any dillemas by examining plain old email behaviour.

To further simplify things, we won’t allow users to write their own content, but use a set of predefined types an keys that can be easily parsed out from firebase.

When the host creates a sends a message to a friend, it should be added to the messages/receiverUserId/newMessageKey. The receiver will check his messages in the messages screen and then answer them. The receiver would act on the message at the time of the response (eg. add the party to his own attending parties key if he accepts) The answer would be a message posted to the messages/senderUserId/anotherMessageKey. The original sender would observe responses and apply them to his parties.

Here’s an example of such a message in firebase:

receiverUserId {
	 messageKey {
		 from: senderUserId
		 messageContent: party_invite
		 messageType: invite
		 party: partyKey
		 to: receiverUserId
	 }
 }

This approach separates concerns and makes the code less complex. Situations and states are all predictable and contain information. One could say that there are limitations to this model. One of these may be that it’s possible for the host not to check the app, in which case responses won’t get applied. This is a valid concern, but due to time and complexity limitations we chose not to handle all cases but highlight principles of design and implementation.

Further details on the messages are:

  • Message Content, similar to the email’s subject field, I could be a party_invite, friend_request.
  • Message Type, analogue to an email’s content. It could be an invite, accept, reject.

Using these two fields a number of combinations could be possible. For example, if the app may need to be extended to other kinds of events, not only parties in the future.

Let’s imagine a scenario for two users using the app:

  1. The host signs in.
  2. A friend signs in.
  3. The friend sends a friend_request to the host.
  4. The host visits the messages screen and accepts the friend request. At the same time, the app ads the friend to the host’s list of friends and sends back a reply message.
  5. The user deletes the friend request message as it doesn’t need any further action.
  6. The host receives the friend_request reply (accept or reject) via a callback and adds the friend to his list of friends. The message can then be deleted.
  7. The host creates a party and sends an party_invite message to his friend.
  8. The friend sees the invite message in his messages screen. He answers it, his parties keys get updated with the party and a party_invite reply message is sent to the host.
  9. The host gets notified that a party_invite reply has been received, applies the information to his party and deletes the reply.

Messaging system implementation

We can start to discerne the implementation for this system:

  • Messages are sent on user actions, on pressed buttons.
  • The user’s app automatically applies them based on content.
  • The user’s app removes messages that were already acted on.
  • The incoming message key should be observed, when a reply is received, the sender’s data is updated with the response. A reply message is any message that has a type accept or reject.
Message.swift

import Foundation
import FirebaseDatabase

enum MessageType: String {
    case invalid = "invalid"
    case friendRequest = "friend_request"
    case partyInvite = "party_invite"
    case imageUploaded = "image_uploaded"
}

enum MessageContent: String {
    case invalid = "invalid"
    case invite = "invite"
    case accept = "accept"
    case refuse = "refuse"
}

struct Message: FirebaseModel {
    let from: String
    let to: String
    let party: String?
    let photo: String?
    let messageType: MessageType
    let messageContent: MessageContent
    var firebaseReference: FIRDatabaseReference?
    
    init(from: String, to: String, party: String?, photo: String?, messageType: MessageType, messageContent: MessageContent) {
        self.from = from
        self.to = to
        self.party = party
        self.photo = photo
        self.messageType = messageType
        self.messageContent = messageContent
        self.firebaseReference = nil
    }
    
    init?(snapshot: FIRDataSnapshot) {
        if let snapshotValue = snapshot.value as? [String: Any],
            let from = snapshotValue[FirebaseMessagesKeys.from.rawValue] as? String,
            let to = snapshotValue[FirebaseMessagesKeys.to.rawValue] as? String,
            let type = snapshotValue[FirebaseMessagesKeys.messageType.rawValue] as? String,
            let content = snapshotValue[FirebaseMessagesKeys.messageContent.rawValue] as? String
        {
            self.from = from
            self.to = to
            let partyKey = snapshotValue[FirebaseMessagesKeys.party.rawValue] as? String
            self.party = partyKey == FirebaseEmptyValue ? nil : partyKey
            let photoKey = snapshotValue[FirebaseMessagesKeys.photo.rawValue] as? String
            self.photo = photoKey == FirebaseEmptyValue ? nil : photoKey
            self.messageType = MessageType(rawValue: type) ?? .invalid
            self.messageContent = MessageContent(rawValue: content) ?? .invalid
            self.firebaseReference = snapshot.ref
        } else {
            return nil
        }
    }
    
    func toDictionary() -> [String: Any] {
        return [
            FirebaseMessagesKeys.from.rawValue: self.from,
            FirebaseMessagesKeys.to.rawValue: self.to,
            FirebaseMessagesKeys.party.rawValue: self.party ?? FirebaseEmptyValue,
            FirebaseMessagesKeys.photo.rawValue: self.photo ?? FirebaseEmptyValue,
            FirebaseMessagesKeys.messageType.rawValue: self.messageType.rawValue,
            FirebaseMessagesKeys.messageContent.rawValue: self.messageContent.rawValue,
        ]
    }
    
    func isReply() -> Bool {
        return self.messageContent == .accept || self.messageContent == .refuse
    }
    
    func removeFromFirebase() {
        self.firebaseReference?.removeValue()
    }
    
    mutating func createInFirebase() {
        guard let reference = createFirebaseReference(components: [FirebasePaths.messages, self.to]) else {
            fatalError("Cant' create message path")
        }
        self.firebaseReference = reference.childByAutoId()
        self.firebaseReference?.setValue(self.toDictionary())
    }
}

[collapse]

The message struct has a firebase snapshot initializer and a regular initializer for creating new messages. isReply()is used to determine if it’s a new message or a reply to a previously sent message.

A new message would be created in a way similar to:

var newMessage = Message(from: currentUser.uid,
	to: user.uid,
	party: nil,
	messageType: .friendRequest,
	messageContent: .invite)
newMessage.createInFirebase()

Replies are applied in MainTableViewController.swift:

MainTableViewController.swift observer code

override func viewDidLoad() {
        super.viewDidLoad()
        
        if let currentUser = CurrentUser(),
            let reference = createFirebaseReference(components: [FirebasePaths.messages, currentUser.uid]) {
            let path = currentUser.path()
            
            self.messagesReference = reference

            self.currentUserReference = FIRDatabase.database().reference(withPath: path)
            self.currentUserReference?.queryOrderedByKey().observe(.value, with: { snapshot in
                
                if let currentUser = User(snapshot: snapshot) {
                    
                    self.messagesReference?.queryOrderedByKey().observe(.value, with: { snapshot in
                        
                        var messages = [Message]()
                        for item in snapshot.children {
                            if let message = Message(snapshot: item as! FIRDataSnapshot) {
                                if message.to == currentUser.uid && message.isReply() {
                                    messages.append(message)
                                }
                            }
                        }
                        
                        self.applyMessages(messages: messages)
                    })
                }
            })
        }
    }

[collapse]

We can notice here that the implementation takes up way more characters than the actual specifications establishing process. Using a wide spread model as a starting point reduced the complexity of the system and increased it’s predictability. Extending it is reduced to adding new keys to the enums, integrating the new message types to corresponding actions and adding members to apply their replies.

Inviting Friends

Having a working messaging system makes it manageable to expand the app with new features. The obvious place to start is adding friends. The flow of adding friends is implemented using the following steps:

  • A user sends a friend request using the friend’s user email (AddFriendTableViewController).
  • The invited user receives a message in the Messages view controller.
  • The invited user accepts or declines the friend request.
  • When the new friend accepts the invitation, the sender is added to it’s friends path and a confirmation message is sent to the original sender. (MessagesViewController)
  • The original sender receives the reply and adds the new friend to his friends path (MainTableViewController).

The data structures for this functionality are messages and users. The pertinent view controllers are the FriendsViewController, AddFriendTableViewController, MessageViewController and MainTableViewController.

Parties

The invited and owned parties list is shown in the PartiesViewController.swift. A user creates, edits or views a party in the PartyDescriptionTableViewController.swift. Moreover, guests can be invited from the PartyInvitesTableViewController.swift and PartyImagesTableViewController.swift. A party object is instantiated either from Firebase or the user’s input. When the user updates the party, a new object needs to be created (because structs are value types) and the original Firebase reference is updated with the new values.

Party.swift observer code

import Foundation
import FirebaseDatabase
import UIKit

struct Party: FirebaseModel {
    let partyKey: String?
    let owner: String
    let description: String
    let guests: [String]?
    let photos: [String]?
    var firebaseReference: FIRDatabaseReference?
    
    init(owner: String, description: String, partyKey: String?) {
        self.owner = owner
        self.description = description
        self.guests = nil
        self.partyKey = partyKey
        self.photos = nil
    }
    
    init?(snapshot: FIRDataSnapshot) {
        if let snapshotValue = snapshot.value as? [String: Any],
            let owner = snapshotValue[FirebasePartiesKeys.owner.rawValue] as? String,
            let description = snapshotValue[FirebasePartiesKeys.description.rawValue] as? String
        {
            self.owner = owner
            self.description = description
            self.partyKey = snapshot.key
            
            self.guests = stringsArrayWithTrueKeys(snapshotValue: snapshotValue[FirebasePartiesKeys.guests.rawValue])
            self.photos = stringsArrayWithTrueKeys(snapshotValue: snapshotValue[FirebasePartiesKeys.photos.rawValue])
            
            self.firebaseReference = snapshot.ref
        } else {
            return nil
        }
    }
    
    func toDictionary() -> [String: Any] {
        return [
            FirebasePartiesKeys.owner.rawValue: self.owner,
            FirebasePartiesKeys.description.rawValue: self.description
        ]
    }
    
    func removeFromFirebase() {
        self.firebaseReference?.removeValue()
    }
    
    mutating func createInFirebase() {
        let reference = FIRDatabase.database().reference(withPath: FirebasePaths.parties.rawValue)
        if let key = self.partyKey {
            self.firebaseReference = reference.child(key)
        } else {
            self.firebaseReference = reference.childByAutoId()
        }
        self.firebaseReference?.setValue(self.toDictionary())
    }
    
    func addAttendee(uid: String) {
        let reference = createFirebaseReference(components: [FirebasePaths.parties.rawValue, FirebasePartiesKeys.guests.rawValue])
        reference?.setValue([uid: true])
    }
    
    func removeAttendee(uid: String) {
        let reference = createFirebaseReference(components: [FirebasePaths.parties.rawValue, FirebasePartiesKeys.guests.rawValue])
        reference?.setValue([uid: false])
    }
    
    func addPicture(key: String) {
        guard let partyKey = self.partyKey else {
            return
        }
        Party.addPicture(imageKey: key, partyKey: partyKey)
    }
    
    static func addPicture(imageKey: String, partyKey: String) {
        if let reference = createFirebaseReference(components: [Party.pathFor(key: partyKey), FirebasePartiesKeys.photos.rawValue, imageKey]) {
            reference.setValue(true)
        }
    }
    
    static func pathFor(key: String) -> String {
        return [FirebasePaths.parties.rawValue, key].joined(separator: FirebasePathSeparator)
    }
    
    static func pathForGuests(key: String) -> String {
        return [FirebasePaths.parties.rawValue,
                key,
                FirebasePartiesKeys.guests.rawValue].joined(separator: FirebasePathSeparator)
    }
}

[collapse]

The flow for establishing a party is as follows:

  • A person decides to host a party. He creates a party object in the PartyDescriptionViewController and the party is added the root parties key and to his user/ownedParties key.
  • He invites friends from the same screen and messages are sent to them.
  • The invited friends see the invitations in the MessagesViewController.
  • The friends accept or refuse the invitations. When an accept is issued, the party is added to his attendingParties key and a reply message is issued.
  • The host receives the replies the next time he visits the the app (MainTableViewController).

All users have a list of owned parties and attending parties keys in their user path. All parties information exists in the root party path identified by key.

Photos

The Swift photo object is a Firebase access adapter. Photo.swift implementation code:

import Foundation
import FirebaseStorage

struct Photo {
    let key: String
    
    static func metadata() -> FIRStorageMetadata {
        let metadata = FIRStorageMetadata()
        metadata.contentType = "image/png"
        return metadata
    }
    
    static func storageReference() -> FIRStorageReference? {
        let imageId = UUID().uuidString
        return createFirebaseStorageReference(components: [FirebasePhotosPath, imageId])
    }
    
    func storageReference() -> FIRStorageReference? {
        return createFirebaseStorageReference(components: [FirebasePhotosPath, self.key])
    }
}

The code for uploading photos is in CurrentUser.addPicture(image:...):

        if let reference = firebasePhoto.storageReference() {
            
            if let resizedImage = image.resized(width: 300),
                let data = UIImagePNGRepresentation(resizedImage) {
                
                reference.put(data, metadata: Photo.metadata())
                
                if let reference = createFirebaseReference(components: [self.path(), FirebaseUserKeys.photos.rawValue, firebasePhoto.key]) {
                    reference.setValue(true)
                }
            }
        }

The code for downloading photos is in the PartyImagesTableViewController.swift:

            if let key = self.selectedParty?.photos?[indexPath.row] {
                let firebaseImage = Photo(key: key)
                let reference = firebaseImage.storageReference()
                
                reference?.data(withMaxSize: 10 * 1024 * 1024) { data, error in
                    if (error != nil) {
                        print(error?.localizedDescription ?? "unknown error")
                    } else {
                        if let d = data {
                            cell.uiimageView.image = UIImage(data: d)
                        }
                    }
                }
            }

The process of adding photos is not as strict as adding friends and owning parties. Guests should be able to add photos taken during the party. Of course, the owner can add photos without messaging himself to accept them. The guest’s flow for adding photos is the following:

  • Go to the party details, selects the photo and uploads it to firebase storage with an unique ID.
  • The new image is also added to his user’s photos key.
  • A message is sent to the party host asking for the photo to be accepted.
  • The host finds the message listed in the MessagesViewController.
  • The host accepts the picture and then it is added to the party key.

Although deleting the images is and exercise, the original uploader should be considered the owner of the photo. The host of the party should be allowed to remove the photo at a later time, but not delete it from firebase.

Conclusions

We have provided a sample application to help users organize parties and invite their friends. Explaiing pertinent parts of the code was prefered instead of a walkthrough because of the lenghty process involved in writing and then integrating already written code. This way, the golden ratio of explained information and length of text was achieved. To better understand the topic, you could start from scratch and try to replicate this app, or use it as a starting point for your own experiments.

Exercises

Apprentice skill (Easy):

  • Add .validate rules for each database entry.
  • In the Messages view controller, display the actual friend’s email instead of UID. Follow the implementation in the FriendsViewController.
  • Add a date field to parties.

Spellcaster (Intermediate):

  • Implement a proper cancel party functionality. Right now, only the party is deleted. The party’s key should be removed from the user’s path and messages to confirmed guests should be sent so they can remove their party also.
  • Allow users to unfriend. When deleting a friend, set it’s key in the friends path to false also send a message to inform the unfriended friend so he can set his own key to false.

Warlock (Hard):

  • Allow images deletion from parties by original uploader and host. The photo owner should be able to physically delete the data from firebase. If the host is not the photo owner, it should remove the photo key from the party.
  • Allow guests to invite other friends to the party. These friends should send a message to the host suggesting an invite. The owner should be able to accept or reject the invite. When accepting, the owner should then send a friend request if necessary and a party invite.

References

Thanks for reading!

Andrei Nagy

Thank you for taking the time to read my 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 JSON. 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! :)

1 Star2 Stars3 Stars4 Stars5 Stars (9 votes, average: 4.56 out of 5)
Loading...

  2 comments for “Advanced Firebase For The Win

  1. Ciaran
    June 8, 2017 at 10:16 am

    Hi,
    Is this project in Swift 3? if not is there a swift 3 version available?

    Thanks

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Subscribe
We send about one email per week with our latest tutorials and updates
Never display this again :)