Building iFood's Amazing iOS Cart Experience

Explaining our mission to provide a scalable, and efficient checkout experience for our costumers

Leo
iFood Engineering
8 min readAug 31, 2022

--

A large and diverse number of people use iFood every day and working on a project of this size means our impact is huge. Have you ever heard the phrase "With great power comes great responsibility"? That's it.

The Checkout is one of the core businesses here at iFood. It's where our customers end their buying journey, the final big step before the purchase. Our mission here is to bring more value and transform the way we buy food.

Screenshot of one of the Checkout steps

In this article, you'll see how Checkout works behind the scenes, and how everything is composed together to create a clean, readable, maintainable code for developers and the best experience for our users.

The Checkout Module

As an engineering team, we need to think of Checkout as a platform where other teams can introduce their features that help our customers. But to handle a lot of features and a lot of complexity, we need a structure that supports this code's scalability. That’s why we introduced a Plugin based Checkout.

The Checkout module contains two main pieces that visually represent the cart state: the core and the plugins.

First, let's talk about how our plugins behave, how they interact with the server and with themselves, and how we compose them together.

Plugins

Checkout plugins contain business logic and layouts built using Swift and UIKit's UITableViewCell. They are made so that different contexts and layouts are placed separately, giving us an uncoupled and testable code.

At the moment of this article, we have more than 25 plugins available and documented. Take a look at some examples:

Creating a new Plugin

Inside each plugin, we have a different story to be told, with different goals and styles.

Creating a new plugin means that you need at least 3 things, the plugin identifier, the configurator, and the PluginProtocol conformance. Let's talk about each one of these.

Plugin Identifier

Every Plugin needs an identifier, and it has to be unique.

The identifiers are in an enum called PluginIdentifier and every new plugin has to be added with a new case in there. This identifier also defines the key to fetch the Plugin from the remote. What does it mean? The checkout is built based on a Remote Config which brings which plugin should compose the checkout, and at which order. We'll see more later.

Plugin Configurator

The configurator is the class responsible for instancing your plugin. It should not be part of the plugin itself, just something that knows how to create it.

This method will be called by the Core to create your plugin whenever the checkout is started and your plugin is on the list to be displayed.

The configurator also receives every State Provider and every Bridge injected into the checkout, so it can retrieve them if necessary.

If you take a better look into the method you’ll see an throws on the signature. It means that the Plugin instantiation can fail, and in this case, you throw an Error to be handled by the Checkout Core.

Plugin Protocol

Every Plugin has a group of methods that will be called upon updates or actions on the checkout, these methods are centralized within PluginProtocol. The first method called is the pluginDidLoad and it should be used to start the configurations of your plugin.

The Plugin can have any architecture as suited, but commonly we use VIP to keep it cohesive with our main project architecture, make its lifecycle testable, and solve class dependency issues like the Massive View Controllers.

Diagram showing some plugin VIP implementation

Components

Components are simple structures of domain data. Each Plugin specifies which is its Component and receives it.

The main objective of these structures is to adapt the external data into a language that Checkout understands to build the plugins, making it decoupled from the external world. Every data displayed in our plugins needs to be adapted first.

Server Data

When we talk about the external data we can say that it is the Cart object itself, which contains all the information related to the current Cart state. This bigger object contains all the information we need to populate a plugin and, together with other plugins, compose the entire Checkout flow.

Some plugins don't need to communicate to the server. These plugins will only manipulate the Cart entity locally and that's it. But for those who need to talk to the server, they should be capable to call Cart services asynchronously and display data when the response is returned.

In case of error responses, we have all errors mapped for each context inside Checkout. For example, increasing the number of items can result in an error about item updating; adding vouchers can result in voucher updating errors and etc.

Communication

Sometimes, our plugins need to notify other plugins that some change occurred. Here is an example of the Donation Plugin that should enable or disable donation selection based on the user's chosen payment method on the Payments Plugin:

Images of donation plugin disabled or enabled due to user's chosen payment method

To make it happen we’ve implemented a communication mechanism based on the Observer Pattern using NotificationCenter. The logic to update works like this:

  1. The publisher ComponentsUpdateEmitternotifies the subscriber ComponentsUpdateReceiver with the updated component
  2. CheckoutInteractor conforms ComponentsUpdateReceiver protocol, which means that is subscribed to components emission and receives plugin updates
  3. After receiving some update, it will notify all other plugins through updateComponentmethod from PluginProtocol that some change happened

Using core as the observer to manage plugin updates make it easy to maintain and, of course, avoid duplicating code around all plugins.

No, we don't use RXSwift or Combine yet. 👀

Visibility

Since each plugin controls its own business logic, it should know when to hide or show. These changes are notified to the core by some plugins through reloadPluginViewsmethod available on PluginProtocol.Since checkout core has a UITableView built-in, we simply reload the table to hide that cell.

Here's an example: note that based on the delivery mode selected we should show or hide the "Where to drop your order?" plugin.

Core

We've talked about plugins — a small component that with others composes the entire Checkout feature. But who orchestrates and puts it all together? Let's meet the Core.

The Core is responsible for handling and controlling the plugins, starting their lifecycles, and receiving/sending the components updated.

Preparation Step

The CheckoutPreparationStep is the first thing triggered when opening a new Checkout and it is responsible for some validations like seeing if the user is logged in and obtaining the remote configuration responsible for building the Checkout's Core VIP lifecycle.

Core Lifecycle

Its lifecycle is very similar to plugins. We’ve opted to use the VIP pattern to compose our entire Checkout scene, and it works just like this, the CheckoutViewControllerwill trigger lifecycles and inputs, which will be logically processed by CheckoutInteactorand later sent to the CheckoutPresenter to be formatted and displayed again by the CheckoutViewController.

Remote Config

Remote configurations can be used to get dynamic information, making it possible to change everything on the fly, without the need to release a new version of the app. The Checkout remote configuration is obtained during the CheckoutPreparationStepand is a JSON response that dictates which plugins will be displayed at each step and the number of steps.

Basically, we have an array of pages checkoutPageConfigsthat contain all the steps and their information like the navigation title, the next button's title, and all plugins checkoutPluginConfigs that will be displayed for this step.

Cart Context

With the help of the remote configuration above, we can introduce different contexts on Checkout like Restaurants, Groceries, and Express. Each one of them contains a number of pages and a set of plugins that makes sense for the user's experience in that context.

Fallback

Okay, but what to do in case a remote config could not be parsed or our services go down / are intermittent? Of course, we should think about a fallback solution for these scenarios, guaranteeing that users can still buy using our platforms. Each context (restaurant, groceries, etc) has a hardcoded structure that contains the basic plugins necessary for a user to finish their purchase journey.

Composing all together

Finally, we can compose it all together. Adding the first item or interacting with the big red bag icon at the bottom of the screen will create a new Checkout, which implies that we’ll create some instances of the CheckoutViewController, present it, instance every plugin as necessary, make each individual component, and attach everything to the Core.

Diagram showing how each entry point uses the Cart entity to create the entire Checkout feature

Documentation

Documentations are mandatory when we talk about Checkout.

Dealing with a lot of business logic, new features, changes from other teams into their plugins, and new a/b test experiments makes us keep notes and document every feature.

When writing technical documentation, we make it easier for everyone that needs to change some unknown code in checkout like create new plugins or refactor code, and need to know the business logic behind it.

For non-technical documentation, it makes it easier for engineers, managers, designers, and QA to understand. We can explain the product itself, the flow of things, and a/b testing experiment results.

RFCs

The Request for Comments is a technical document created to describe methods, behaviors, and how things should work on the internet. Here, we have our Checkout Plugins Application RFC.

The main idea is to explain what are our strategy and everyone’s obligations with regard to plugin development inside our Checkout Consumer mobile app. This RFC allows us to work in a more efficient way with all the squads that have a hypothesis, necessity, or opportunity that involves the checkout flow.

Wrapping up

Whew, is it over? You saw the basics of how we create and compose our plugins together into the core to create a building-blocks experience that is easy to change. But this is just the tip of the iceberg.

There are a lot more topics interesting that were not mentioned, like the automation of accessibility inside plugins, reusable analytics code to improve the Checkout experience, our design patterns studies to make everything clean, and, the thing I like the most, animations and micro-interactions, but that could be a topic for the next article, right?

See ya!

--

--