Building a Realtime iOS App Using Functional Table Data
Stale data sucks. With the help of Firebase’s data subscription model, all the data shown in your app can be up to date and consistent across. This article will go over how we built a realtime contacts app using Firebase and FunctionalTableData (FTD) — a functional approach to maintaining view states.
Realtime Functional Table Data is a basic Contacts app built with Shopify’s Functional Table Data and Firebase. The idea is simple; each screen subscribes to a model in Firebase and is notified with realtime snapshots every time it’s updated. Each snapshot is decoded client-side and passed to Functional Table Data which compares it to the previous state to insert, update, and remove sections and cells that have changed.
Check out the Github project here: https://github.com/Campbell-Scott/RealtimeFunctionalTableData
Getting started
Detailed instructions on how to get Realtime Functional Table Data up and running, along with configuring Firebase are available in the README.
With this, run the RealtimeFunctionalTableData
target on any device!
Project Structure
As you might notice, this project isn’t built using the traditional Model-View-Controller design pattern. Functional Table Data helps separate view-level logic and concerns from the view controller. FTD facilitates this React-like view building with its efficient state diff’ing algorithms and its functional approach to maintaining view states. Here are the four component’s responsibilities in each view:
- the state: responsible for maintaining the current source of truth of the view’s data.
- the builder: responsible for deciding which data from the state to display, in what style, and how to handle action events.
- the view controller: responsible for performing or delegating actions passed from the builder, then updating the state accordingly.
- the service: responsible for performing asynchronous tasks (such as network requests) and reporting the results.
There are three views in this demo contacts app; List, Add, and Details. As you’ll see nested under Sections
in the project, each of these have a respective View Controller and Builder. Since the state of these are so small, they currently live in the Builder files, but it wouldn’t hurt to extract them.
Let’s examine how the List view is built and organized! As previously explained, you’ll notice the View Controller (VC) doesn’t know anything about the User or the view elements. The VC keeps reference to a FunctionalTableData
object which will be managing our table view and diff’ing when needed. The state for this view is an enum with two cases; .loading
and .finished(result:)
. As the content shown on this list needs to be fetched via a network request, we initialize the state as loading
, then update the state to finished
when the up-to-date User object is received.
Whenever the state is updated, the render(state:)
method is called with it. This method invokes the List Builder for TableSection
objects, then uses FunctionalTableData’s renderAndDiff(sections:)
to update the view with the new, up-to-date, data.
When the builder receives a new state, it decides what the list view should display based on its enum value. If the state is set to .loading
, the builder will return a spinner cell. If the state is set to .finished(result:)
, the builder will return an array of ListCells
, each representing a contact. Every cell is represented as a CellConfigType
object, which holds its view, state and actions.
Cell actions (such as selection, or a textfield value changing) in FTD are protocol-based. Typically, a protocol with the Actionable
nomenclature will be defined with a function representing each action to be used in that builder. Respective VCs simply conform to these protocols and define the behaviour for each action. In the case of the list view, one action is defined for the selection of a contact cell which pushes the Details VC of that contact.
Services are responsible for performing asynchronous/non-deterministic tasks (such as network requests) and reporting the results to a concrete instance of the ServiceDelegate
. Services are vended to their consumers through the ServiceProvider
. Since consumers communicate with every Service
through a shared interface, each service can hide its own implementation details. This allows a developer to swap in any new Service
implementation. For example, the FirebaseService
in RealtimeFunctionalTableData
implements subscribeToUser()
by subscribing to a User
document in Firestore
and calling its delegate every time it receives an updated snapshot. Another Service
might implement this by fetching a User
from CoreData
or a Realm Database
, etc.
With all this, our app never needs to worry about stale data being shown! All in all, this app features:
- Realtime syncing and rendering — no pull-to-refresh!
- A functional approach to maintaining view states
- Programmatic view layout — no more interface builder!
- An agnostic network layer which accepts any service
Come help us build Functional Table Data!