iOS Swift Firestore (Firebase) Photo Album

Swift Firestore Photo Album Preview
Firestore/Firebase is a great backend platform for small teams and independent developers but it does require a good understanding of the basics to get started and be productive. This is a ‘hands on’ tutorial where we’re going to build a simple photo albums application that allows a user to manage their albums and photos collection. In the tutorial we’re going to use the two arguably most important Firestore/Firebase services: Firestore real-time database and Firebase Storage. We’re also going to briefly touch on Firestore Functions in one of the challenges. Let’s get started by cloning the starter project, you can always switch to the master branch later if you would like to see the final finished version.

Setup
Let’s get started, fire up your terminal, navigate to the directory of your choice and type/paste the bellow commands (execute each line separately):

If you received an error running the above commands, chances are you’re missing either the git or cocoapods command line tools. For instruction on how to get those installed, please visit the git install guide and the cocoapods install guide. Once you’re in XCode, build the project (CMD+B) and then run it (CMD+R).

If everything went well you should see the starter app running. The app contains an initial ‘Sample Album’ album with four pictures of cool cats. There are controls to add/delete albums and photos but you will discover that those don’t actually work. No need to panic however – we will be fixing this soon!

Create Firestore Project
Before we begin fixing the starter app, we need to first register a new app at the Firestore end. Navigate to https://console.firebase.google.com and assuming you’re already logged in with your google credentials you should see the bellow page:

Click ‘Add Project’, fill the project name and click ‘Continue’ & ‘Create new project’, feel free to leave all checkboxes unchecked in the process. If everything went well, you should be notified that the project was created successfully and taken to the project Get Started page. You will now need to create an app configuration which you will later download to your XCode project. Click the iOS icon shown in the screenshot to continue.

Enter ‘test.SwiftFirestorePhotoAlbum’ for iOS bundle ID and click ‘Register app’. On the next screen simply click ‘Download GoogleServices-Info.plist’ file

Once the file is downloaded, open app the Finder next to XCode and drag this file into the Support Files folder.

From the Firestore console, click on the Database option from the Develop left-hand side menu option.

On the next screen you should see the Create database option, once it you click will be presented with the dialog, asking you if you would like to either start in locked mode or in the test mode. Please chose the test mode option to keep things simple for this tutorial and click the Enable button.

Now we need to enable Firebase Storage for our application. Simply click on Storage link in the Develop section of the left-hand menu in the Firebase console. On the Firestore welcome screen, click the Get Started button.

Next you will a Security rules dialog, which tells you that only authenticated user’s will be able to read & write to your storage.

As we’re not implementing authentication in this tutorial we’re going to change this to allow any user to read and write to our storage. It goes without saying that this is only acceptable for testing out things in a non production application and you should not take security lightly for production level apps that may contain sensitive data.

Navigate on the Rules tab from the Storage main page and you should see the default rules that were added by the Firebase for you:

On line 4 is where the check is performed to see if the user is authenticated by checking the request.auth object for null, change this to:

We have now modified the line 4 to always return true on every read and write request, regardless of user’s authentication status. To save the new rule press the Publish button. As mentioned earlier, this approach is fine for testing applications that don’t contain any private or sensitive user data. Please don’t do this for production level apps. For more information on implementing security rules, please have look into Firebase documentation.

Final step for this section is to go to AppDelegate.swift file and locate the didFinishLaunchingWithOptions method:

Uncomment the ‘FirebaseApp.configure()’ and re-build and re-run the project. If everything went well, you should not see any errors and the same sample app that we started with will appear.

Adding/Deleting new albums
Now that all the setup is out of the way, let’s write some code to add/remove the albums from the Firestore.

Navigate to Services folder and open up the ImageService.swift file. You will see the addAlbumWithName, deleteAlbumWithAlbumId and getAllAlbums methods. We will begin with implementing the getAllAlbums method. We will start by creating a reference for the collection we’re about to access:

The Firestore.firestore() is used to get the reference to the database object which we can later use to access collections and documents. The .collection(“albums”) part is used to specify that we would like to access the albums collection which is located in the root of the main database.

As seen on line 4, we have added a snapshot listener closure on the albums the collection reference. This means that every times the collections is modified (items added/removed/edited) we will receive a callback with all the latest documents in the collection. On lines 5-10 we do a simple error checking in case something went wrong and on line 12 we convert all the Firestore documents into AlbumEntity classes, which is our local representation of the Firestore album document.

Finally, on line 15, we switch to the main thread (queue) and call the albums() completion handler, which will notify the caller (AlbumListViewController) that the service has received new data. Run the application and see if you notice anything different. That’s right, the initial ‘Sample Album’ has gone and we see an empty screen. At this point our query does work, but as we have not added any albums yet, it correctly shows the empty screen. Next we will implement the addAlbumWithName method which will allow us to add new albums.

Optional Step: Managing Albums via Firestore console
This is an optional step so feel free to skip it, however, I think it’s a useful skill to learn to be able to modify the data right from the Firestore Database and be able to test your app and verify that it behaves as you intended even before you add all the functionality to manage the data from within your app.

Open up the Firestore console by navigating to https://console.firebase.google.com and click on your project name from the Recent projects list. Chose Database from under the develop section of the left hand menu. You should see an empty database page.

To begin adding a new collection, click Add collection link and enter albums as Collection ID before clicking Next. On the next screen we will be adding our first document, which is going to be our first Photo Album!

The Document ID needs to be something unique that can identify your album. To quickly generate a unique ID you can click on the Auto-ID button which is just to the right of the field. Fill out the first empty field by specifying it’s Field as name (i.e. the album name), leaving the type as string and anything you want for the value (in my example I filled ‘Sample Album’). Next, click on the little plus sign bellow the field you just filled and that should create another empty field. Give it dateCreated as a field name but this time chose a timestamp as a type. As soon as you select the timestamp as a type option, you’re given the date and the time controls to set the date. Set those as you like, the Date control is required and the Time is optional.

Click the little plus icon one more time and fill the newly created field with numberOfPhotos string for the Field, number for a type and insert any number of your choice to the value column. Finally hit the Save button. You will now see the newly created albums collection with a single album document that has the values you just provided.

Run the App (if it wasn’t running already from the previous step) and you should be able to see the first album that you just added in the list. You can play in the console by adding/removing and modifying the existing albums and the App should reflect the latest changes.

Adding a new photo album from the Album List screen
To add a new album when the plus (+) button is tapped and the album name is entered modify the addAlbumWithName method like so:

On line 2 we create a reference to the albums collection, next, on line 3 we create the data dictionary which will contain just two items: name and dateCreated. Finally we call the addDocument() method on the collection reference and that’s it.

Deleting a photo album
Deleting a photo album is even simpler than adding one:

We first create a reference to our album document, and than we call a delete() method on it.

Adding images to the album
While we are now able to add and delete albums, you have probably noticed that if you tap on any album you will still see the same pictures of the four cats again and again. Some could argue that the cats look way too cool and having them in every album is a feature rather than a bug but in this section we’re going to fix this unique ‘feature’ in three steps. The first step is going to be uploading images from our device’s photos to the Firestore real-time database and Firestore storage. The second step will be adding the ability to download and display those newly added photos and the final steps is to be able to delete them. Let’s get started with step one.

Upload new images
Before we jump to the code let’s think about how we’re going to implement the image uploading part. One way of doing this is to create an images collection just as we created the albums in one of the previous step and add a new image document for each new photo that the user adds.

There are two ways creating a document. One way is to call an addDocument() method on the collection the same way as we have done in one of the previous steps. This method is great if you just want to create a single document, however, if you need to create multiple documents at once, it is better to use Firestore batch for the job. Create a batch at the start of the method and keep adding documents to it until we’ve added them all, then simply call a commit() method on it which will send all the documents to Firestore and notify us when they’re created.

  • On line 2 we have created a reference to the images collection, just as we have done with the albums in the previous steps
  • On line 3 we are creating a new batch that we’re going to use later to append the image documents to
  • On line 5 we initiate a foreach loop to walk over every photo that was selected
  • On line 6 we create a reference to a new image document that we’re about to add to Firestore
  • On line 7 we create the data for the actual image document, which consists of the albumId, current time, and the status (more on the status later)
  • On line 8 we are telling the batch to create a new document with the data that we have provided
  • On line 11 we are telling the commit() method of the batch which means that all the operations that we have set on the batch will be executed now. It is important to understand that until we had called the commit() method, non of the data was actually uploaded to the server
  • Finally on line 12 we’re notifying the called that all the operations have been completed

Try uploading a couple of new images and see in your Firestore console if they appear as expected (Please note: if this is the first time you’re creating the images collection, than you may need to refresh the page to see any change, but once the collection is created, you should see any changes without the need of refreshing the page).

If you looked at the previous code carefully you may have noticed that we don’t actually do anything with the imageData within the foreach loop. In other words, we’re creating all the image documents but we’re not uploading the actual images. This is because we have only done the first part of the method which creates a record for the images in the database, and we are going to enhance it with the actual uploading in the next step.

The idea behind the uploadImages method is to first create the image documents and set their status to pending and tell to the calling method that it’s now ready to display. The calling method will now be able to fetch the newly created image documents from Firestore database and display them in the list. Because no actual image data has yet been uploaded to Firebase storage, instead of seeing the actual selected images in the app, the user will see just the placeholders with spinners. At this point the spinners will spin forever as we haven’t completed the uploadImages method just yet, but once we complete it, the spinners will be replaced by image thumbnails one by one as each image is uploaded to the server.

Alternatively, we could of uploaded all the images first before telling the caller to display them, but image uploading is a slow process (especially if you’re trying to upload gigantic pictures on a slow 3G connection) and this would of made the UI very unresponsive. By displaying image placeholders with spinners, we’re telling the user that we’re working on uploading the images and that they will appear shortly. Okay, let’s finish up the upload method:

There are a couple of small change on lines 6-7 and 9. Instead of enumerating the images array and creating a document reference on each cycle loop, we create imagesWithDocRefs, an array of key value pairs where the key is the new document reference and the value is the image bytes.

In the second step we’re going to work with Firebase storage to upload our images to the cloud:

  • On line 19 we’re creating a storage reference to the images folder that we’re going to use to upload all of our images
  • On line 21 we enumerate through imagesWithDocRefs
  • On line 22 we create a new image storage reference. We’re using the document ID from the images collection to name our uploaded image (saves us having to create and store an extra ID to tie the two together)
  • On line 24 we’re using the storage image reference we just created and we call the putData() method, passing the actual image bytes to it
  • On lines 25-28 we do a standard error handling

When we reach the line 31 this means that we have successfully uploaded the image to Firebase Storage. All that is left to do now is to get the newly uploaded image url, and update our image document that we created in Step 1 with it and change the status from pending to ready.

  • On line 31 we call downloadURL() method on the storage image reference which will give us the actual image url
  • On lines 32-35 we’re doing a standard error handling
  • On line 37 we create a dictionary with the image url and a new status
  • On line 38 we use a dictionary created in the previous line to update the image document

Displaying the uploaded images
Despite us implementing the image uploading logic in the previous step, we still continue to see the super cool cats and not the images we have uploaded. There is just one last step left before we can see our images, and this step is to implement the getAllImagesForAlbumId() method:

  • On line 2 we’re creating a reference to images collection as we have done many times before

As you can imagine, a user can have multiple albums with multiple photos in each. If you recall from previous steps, all the image documents are written to the images collection, regardless of which album they belong to. We need some way to differentiate between images from different albums. Likely, in uploadImages method where we’re creating the image documents, we had embedded the albumId into the image document. We can now use this field to filter out any images that do not belong to the album. On line 4 we’re doing just that. The whereField clause that is applied to the images collection reference will filter out any images not related to the album we’re in.

  • On line 3 we have appended a whereField clause that will ensure we only get images that belong to the album we’re viewing
  • On line 5 we’re adding a Snapshot Listener that will give us the initial images collection and any subsequent changes (i.e if we decide to add new images or delete an existing one)
  • On lines 6-11 we’re doing a standard error handling
  • On lines 13-14 we’re converting the Firestore documents we’ve received into local image entities
  • Finally on lines 16-18 we’re sending the new images to the AlbumDetailsViewController so that they can be displayed

Deleting images
Deleting an image is simple, we just need to remove it from two locations: the image document from images collection and the image file from Firebase Storage. Here is the implemented deleteImageId method:

  • On line 2 we’re creating a reference to the image document
  • On line 4 we’re calling the delete method and adding a closure to get notified when the image document is deleted
  • On lines 5-8 we’re doing the standard error handling
  • On line 10-12 we’re telling to the caller (PhotoDetailsViewController) that the image document has been deleted so that the collection view can be refreshed
  • On lines 15-16 we’re creating a reference to the image in the Firebase Storage and calling a delete on it, which will delete the actual image file

Homework challenges
If you have completed all the previous steps, stop what you’re doing and take a moment to give yourself a firm pat on the shoulder. You have implemented so far:

  • Creating and deleting albums
  • Uploading new Photos to Firestore real-time database and Firebase Storage
  • Deleting photos

There are couple of things that we have not finished however:

Photos Counter in Albums List
You may have noticed that regardless of how many photos we add to our album, the counter stays at zero. One of the ways of implementing this is to use Firestore Functions with triggers. We can have two triggers that are triggered every time a new image is added or deleted from the images collection, then calculates the total amount of images that belong to the album and finally updated the numberOfPhotos property on the albums collection. I’m in process of writing a tutorial on this topic so stay tuned if this is something you’re interested in learning. Before I complete the tutorial on Firestore Functions, if you feel like you’re up for a challenge than try implementing this yourself and let me know in the comments if you managed to get it working.

Deleting Firebase Storage images when album is deleted
We have implemented the deletion of individual images but not when the whole album is deleted. There is a method called deleteAllImagesForAlbumId() in ImageService that can be called from deleteAlbumWithAlbumId() of AlbumService. The implementation of deleteAllImagesForAlbumId is left for you as a challenge (Hint: use Firestore batch, just like we used when adding multiple images!).

Not cleaning up listeners
In both the AlbumListViewController and AlbumDetailsViewController we add snapshot listeners to listen for the initial data and any updates but we’re not removing them as we switch the screens. Imagine you’re on the Album List screen and open up a first album, now you have two snapshot listeners (one from the album list and one from album details). Now if you navigate to another album you will have three listeners. If you navigate yet to another album you will now have four, and so on (actually if you navigate even to the same album multiple times, you will be creating multiple listeners). The morale of the story is that once you navigate away from the screen, you probably need to clean up your listeners (especially if you pop that screen from the navigation stack). I will leave this as an exercise for you with a hint: the addSnapshotListener method returns a ListenerRegistration object which you can keep hold of in the caller controller until that controller is about to be popped off the stack, than call a remove() method on it. Please check the final version of the project if you get stuck.

Weekly typed references to collections and documents
In many of the examples in this tutorial, when we’re creating a reference to a collection or document, we are using the primitive methods like so:

There are couple of issues with these declarations:

  • They are long (i.e. a lot of typing with no autocompletion help when typing strings)
  • They are not always self descriptive. I.e. we can see that we’re trying to access some document, but what does this document means to us? (Ok, in small app like ours we can easily guess that we’re referencing an image or album but in larger apps this can become more tricky)
  • They are error prone. Imagine if you misspell the images string when creating a collection (i.e. ‘imagse’) in one of your declarations? The app will compile and run, but will not give you the data you expect, something that can be hard to spot and fix in a large application
  • If you rename one of the collections or documents, i.e. instead of images you decide to use photos as a collection name. Now you have to find all the “images” string and changes them to “photos”.

See if you can improve those declarations, you can create an extension file with a helper method for each collection and document you need to access, so the above example end up looking something like:

Order albums and photos by the date created
We have added dateAdded field to images collection but we have not done anything with it. See if you can modify getAllImagesForAlbumId of ImageService to always return images in the order they were added. Hint: if at some point you see an error in console inviting you to create an index in Firestore console, you’re on the right path.

The end
This is the end, hope you enjoyed this tutorial and learned few bits along the way. Please let me know how you did get on, and if you had any issues along the way.

Leave a Reply

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