Getting started with Firebase on iOS: Part 2

Niamh Power
9 min readJun 21, 2018

--

This is the second part of the ‘Getting started with Firebase on iOS’ series. We are currently part way through creating an iOS app that mimics the functionality of the Slack app, LunchTrain. You can find the first part here.

In the first part, we covered authentication and basic Firestore setup. In this part, we will first start with some refactoring and testing of the existing codebase. Then we will implement the Train Details screen, with the ability to see passengers, and also join a train yourself!

Refactoring

Currently, we have used a fairly basic MVVM architecture. This works well for this small scale project, though we have some points to improve.

Firstly, the table view in the train list access the view data, within the view model. This could be greatly improved by separating off the table view delegate methods into their own adapter class, allowing for as much business logic as possible to be outside of the View Controller.

Furthermore, given we are likely to use table views in other areas of the app, such as the passenger list, it is worth us using generics to design a adapter that can be reused.

This is the one we will use in the lunch train app. You can build on this to provide more of the delegate methods, depending on your requirements, but for now this implementation is enough for us.

In order to use this in the app, we need to adjust the view controller as below:

Again, the block within the didSelectItem section doesn’t need to be implemented yet. We will be covering that in the next section. I will do other small refactors as we go 😊

Create a Train

Now we’re going to move onto adding new Trains from the app. For this, we will need to use some different methods for the Firestore, and implement a new UI.

We will start with the UI and structure first.

Create a NewTrain folder, and add theNewTrainViewController.swift and NewTrainViewModel.swift files. We don’t need view data here as we can just reuse the Train model.

You will note that I have added an option to handle an error when creating a new train. We will be looking at error cases and adding UI for these in a later post! For now, to the user, it will not crash but simply dismiss the screen:

Next, we want to hook this up to the button we have in the list view. This is fairly simple:

@objc func addPressed(sender:UITapGestureRecognizer) {    performSegue(withIdentifier: "addTrain", sender: self)}override func prepare(for segue: UIStoryboardSegue, sender: Any?) {    super.prepare(for: segue, sender: sender)    guard let viewController = segue.destination as?   NewTrainViewController else { return }    viewController.viewModel = NewTrainViewModel()}

As you can see, we now have a dedicated screen for entering the train details, which automatically gets the user’s name and creates a new Train. If you open the Firebase console while creating this, you should see the collection automatically update, and the list will update with the new Train when you return to it.

Viewing Trains

Now we want the user to be able to select a train, and see the relevant details. For this again, create a new group, named TrainDetails and add the files TrainDetailsViewController.swift TrainDetailsViewModel.swift and TrainDetailsViewData.swift

In the view data, we will have a reference to the Train and also the DocumentReference for the chosen train, in order to perform operations later.

Then in the view model for now, it will only contain the didChangeData block, as we don’t need to do any network calls or data manipulation for now.

Again, feel free to customise your UI as you see fit. I have included a table view and a button ready for the passenger list and the ability to join a train.

So at this point, we have the ability to see the trains, and click into them to view more details. The join train button doesn’t currently work, so the passengers list isn’t functional at this point.

In order to implement this list, and the button, we want to add similar functionality to how we handled the list of trains, but just listen to a different collection of documents.

First off, we want to create a new data class to represent a user. This is really similar to the Train model we made earlier.

For now, these users are going to be made per train. Later on we can look at improving this, so a user can see all the trains they have joined, and manage their trains more effectively.

Add the methods to the TrainDetailViewModel as shown below, to observe the passengers collection within the Train model.

Next, just add the viewModel calls into the view lifecycle methods, to ensure no observers are left over when you leave the screen.

override func viewWillAppear(_ animated: Bool) {  super.viewWillAppear(animated)  viewModel.observeQuery()}override func viewWillDisappear(_ animated: Bool) {  super.viewWillDisappear(animated)  viewModel.stopObserving()}deinit {  viewModel.stopObserving()}

Now you should be able to run the app, but nothing will happen! This is because there’s not actually any data in your passengers collection yet, and there probably won’t be that value in the database either! So first we need to initialise each train with the list of passengers with just the owner inside.

Head over to your Firebase console, and delete all the trains you have in their, or add a passengers field to each, otherwise we’re going to have issues later! Our guard statements keep us pretty safe, but in order to keep our database clean it’s a good idea to reset here, while we’re in active development.

If you choose to add to existing Trains, make sure you click Add Collection rather than the Add Field, as this then creates a new collection inside the train document. Your console should then look similar to below:

Last, but definitely not least, we want to give our table view the data we get! Again, we’ll do this in the same way as in the list, with a table view adapter to handle the data. Handily, we made a generic adapter in our first part, so we can simply reuse this! Just make the below changes to the TrainDetailViewController :

You’ll note that we have made a cell view this time, for now this is fine, as we only have the name field to show. We will customise this more in future parts 🎉

Now when you load up your app, if you’ve added a train with passengers, you should be able to click on this and see the list in the detail view.

If you haven’t yet, try adding some data into your trains, and see what happens. Does the app crash if the data isn’t the right type?

Joining a Train

The last main functionality we’re going to add in this part is the ability for a user to join a train. For this, we want to add a gesture recogniser to our button, and handle the tap. When this happens, we want to get the user’s name, and simply create a User, which can then be added to the passengers collection. This should then automatically update the table view in the details.

In terms of UX, we probably want to add an activity indicator to replace the button whilst this is handled. And also the option to grey out the button if the user is already in the train. We’ll be adding the ability to leave a train later (again 🙃)!

First, add this code to our setup method:

let gesture = UITapGestureRecognizer(target: self, action: #selector(self.joinPressed))joinTrainButtonView.addGestureRecognizer(gesture)

Then we want to create the joinPressed method:

@objc func joinPressed(sender: UITapGestureRecognizer) {  //call a method on the view model!}

Now this is all tied up, we can implement the method on the viewModel. Add a public method joinTrain as below:

fileprivate func baseQuery() -> CollectionReference? {  return trainReference?.collection("passengers")}func joinTrain() {  guard let query = query else { return }  let user = Auth.auth().currentUser  let name = user?.displayName ?? ""  let passenger = User(name: name)  let newPassengerReference = query.document()  Firestore.firestore().runTransaction( { (transaction, errorPointer) -> Any in    transaction.setData(passenger.dictionary, forDocument: newPassengerReference)}) { [weak self] (object, error) in  guard let strongSelf = self else { return }  if let error = error {    print(error)    strongSelf.joinTrainRequestCompleted?(false)    return  }    strongSelf.joinTrainRequestCompleted?(true)
}
}

You’ll note I’ve change the baseQuery method to return a CollectionReference to allow use to more easily create new documents. You’ll need to change the baseQuery type to CollectionReference too.

You’ll also notice that I’ve added a new callback, joinTrainRequestCompleted . We can use this to handle stopping the activity indicator, and display an error if needed.

In your bindViewModel method, add this:

Once you implement the new code for the detail view controller, you should get a similar result as the GIF on the left. Feel free to customise this as you wish.

You’ll notice that once we’ve joined a train, leave the view, and then return, the join train button is available again. In order to handle this, we need to cross check the list of passengers against the current user. Again, this will be simpler once we know which lunches a person is involved with, but for now this will work.

Add the following code to your view model. Here we are getting the current user’s name, and doing a simple check within the passengers list.

private func checkIfUserIsAPassenger() {  viewData.isUserAPassenger =   viewData.passengers.contains(User(name: getCurrentUserName()))}private func getCurrentUserName() -> String {  let user = Auth.auth().currentUser  return user?.displayName ?? "" }

Add a isUserAPassenger boolean field defaulting to false on the TripDetailViewData struct. This allows us to simply check this value in the view controller, rather than having more complex callbacks or logic to deal with.

You’ll notice at this point that we will have build errors. This is because the compiler doesn’t know how to compare different User objects. For this, we need to make User conform to the Equatable protocol.

Add this extension to your struct:

extension User: Equatable {  static func ==(lhs: User, rhs: User) -> Bool {    return lhs.name == rhs.name  }}

Finally, we want to trigger this check. It makes the most sense to check this any time the data is updated, so within the didSet of the viewData.

var viewData: TrainDetailViewData {didSet {
checkIfUserIsAPassenger()
}}

Because we want to update the view data after this check, we can move the didChangeData?(viewData) call to within the check method. Simply add the call to the checkUserIsPassenger() function.

Now we want to handle this. Within the didChangeData block in the TrainDetailViewController , add the code to grey out the button if the value is true.

if data.isUserAPassenger {  strongSelf.disableButton()}

The disableButton method contains the code we wrote earlier:

private func disableButton() {  joinTrainButtonView.backgroundColor = UIColor.gray  joinTrainButtonView.isUserInteractionEnabled = false}

Now we just need to call the check in the viewModel! Just add the function call within the response from the firestore, as shown:

self.viewData.passengers = modelsself.checkIfUserIsAPassenger()self.documents = snapshot.documents

This means anytime the data is updated, we will be checking the value. There are likely more efficient ways of doing this. Can you think of any? Perhaps within the mapping of the response from Firebase?

Now we have the main functionality of the app in place, we are in a good position to start implementing more than the MVP. In the next part, we will go through how to implement notifications, and for a user to be able to manage the trains they’ve joined. We ill also look into some database rules, to ensure that a user can’t join a train twice, for example.

Let me know any questions you may have in the comments, and as always feel free to ping me on Twitter!

--

--

Niamh Power
Niamh Power

Written by Niamh Power

Senior iOS Engineer @ Yoto. All views are my own. Firebase GDE