Perhaps everyone will agree that chatting became an indivisible part of modern life. It makes it so easy to communicate with your relatives, friends, coworkers, partners any time, almost anywhere.
Besides general-purpose chats like Telegram, Viber, WhatsApp and Messenger, a lot of businesses implement their own solutions for specific purposes, like contacting your cab driver, courier or landlord.
From developer’s point of view implementing chat may become a pain, for the first time — for sure. The reason of this is a synchronization between server and client, which is done with properly mixed powers of local database, http requests and socket connection. This is where Firestore can help us as it solves a problem of synchronization almost completely.
Google’s Cloud Firestore is a NoSQL document database that allows users to create serverless applications.
In this article series we will examine how to implement a chat app for iOS using Firestore. Entire process will be split into 3 parts:
- Requirements and model;
- Firebase interactions;
Information is targeted for beginners in mobile app programming. Only the last article will be strictly bound to iOS SDK, so you can apply the lessons to Android, React Native and Web as well.
In this article we will review requirements and implement models following plan below:
- Defining Domain Model
Let’s Start The Party
Any complex feature can be implemented easily with proper model design. In this article we will learn how to define and implement model both abstract and specific for programming language (Swift in our case).
At first let’s define functionality scope to see what we should cover.
The app will consist of following screens:
- Authentication. User can enter his email and password. App creates an account for new user or lets old user sign in. New users must also specify their name;
- List of available chats. Contains list of chats that current user is member of. Each chat has a name and last sent message. User can view particular chat, create a new one or sign out. When there are no chats available user sees a message suggesting to start using the app;
- Chat. Shows a list of users’ and system messages. Messages are being grouped by day of sending and common sender. Each message has time when it was sent and read status. Screen also shows a sheet with following options: invite user to chat, rename chat and leave chat.
For authentication we will use FirebaseAuth. It has nice ready-to-use UI and support for third-party social networks auth. In our case, we will only use email and password sign in to keep things simple, but you can add social login easily.
Defining Domain Model
Keeping in mind app functionality, let’s define three main entities:
- Chat Represents a chat that has a name and creation date. Has one to many relationship to events and to users;
- Event The thing we show in list on Chat screen. Can be a message from user or system info. If Event is a message, it has a text body, sender and read status. If Event is a system info, it can be one of this types: createdChat, addedUser, userLeft, renamedChat;
- User Name and email of user. These will be taken from FirebaseAuth and posted to Firestore.
These are high-level entities, each entity will be represented with one or more Swift structs for different layers of abstraction.
User entity is simple, there is no need to create multiple structs for it.
Same as User, Chat entity has only one struct to deal with in whole app. The only thing that differs is that we cannot store Date type as it is in Firestore, so it will be converted to timestamp. Also this struct stores latest message, which we will cover in next section.
Event is a bit more complex. It has nested enums with associated values that cannot be serialized into JSON (Firestore is JSON database). This is why Event requires different, flat representation for storing/transportation.
Now we have a flat FirebaseEvent model that can be converted into JSON, but can be in invalid state that doesn’t make sense. To understand what invalid state means let’s take a look at initialization of Api models.
Here every initialization fail represents invalid state of model. For example FirebaseEvent can have type message but contain info instead. We don’t want to deal with this validations later. That is why we separate complex entities to different structs. In view controllers we will only operate with high-level representation.
To keep things consistent we rename models that have only one struct and create aliases for high-level versions of them. So ApiUser becomes FirebaseUser, ApiChat becomes FirebaseChat and ApiMessage becomes FirebaseMessage. ApiUser, ApiChat, ApiMessage are now type aliases for corresponding Firebase models.
To sum up, structs with Firebase prefix will be used only for transportation and will be abstracted from domain logic code.
Firestore models are being represented by DocumentSnapshot class in it’s API. We need a way to convert it to structs we implemented recently. For this purpose I created a protocol FirebaseDecodable and methods for initializing models, that conform it, from Firestore snapshots.
Now let’s conform our Firebase models to FirebaseDecodable protocol. I will show an example of one model, all others can be done by analogue.
Here we convert iOS Foundation Date object to Firestore’s Timestamp to save model as dictionary to Firestore later.
Now we are ready to deserialize database objects into Swift structs. Next step is implementing serialization.
We could create similar protocol FirebaseEncodable as opposite to FirebaseDecodable, but saving info to Firestore isn’t same straightforward as fetching.
At first, we assumed that our structs can only represent entities that are already in database, because each of it contains an identifier. Beside that, we want to use server-side values for some properties like createdAt timestamps, because taking timestamp value from user’s phone is error prone. For example, user 1 with valid time 7:00 PM sends a message to chat. User 2 with wrong time 6:00 PM sends a message after user 1. If database query depends on createdAt property, message from user 2 will appear above message from user 1, which is incorrect. Firestore provides some API to handle such cases, we will take a look at it in details in next part.
Let’s create auxiliary high-level structs for saving info to database later.
We will call service object method passing this model to create event in the database.
Similar to events, this will be used for creating new chats.
We have implemented all models that we need for describing domain logic. You can find project code here.
In next part we will implement service object class, that will encapsulate Firestore interactions to abstract them from other parts of the app.