Within Uber’s mobile ecosystem, we are always trying to engineer our architecture for reuse. We benefit greatly from properly modularized code because we are constantly running experiments to help us make product decision across our varied markets and types of people we serve. Over the past year, we have iteratively worked on Ohana, an iOS framework for retrieving and formatting contact information, which we have open sourced for others to build on.

Ohana, which means family in Hawaiian, provides tools for easily organizing and presenting contacts information from the iOS address book. It’s a data toolset, not a UI widget. Ohana is part of a broader effort to modularize both the logic and the UI around displaying contacts to the users of the Uber iOS driver app. Here’s more on what Ohana is about, and how it can be a useful part of iOS app architecture. We will present an overview of Ohana and what its components do, with some of the source code interface in case you want to use it for your application.

 

Architecture

Ohana’s high level architecture is about filtering data from multiple sources (abstracted to Data Providers), through an ordered set of post processors to transform the data for a specific user need, such as displaying it within the app:

Ohana uses UberSignals (an implementation of the observable pattern) as its primary data flow mechanic. Ohana is structured with a small set of implementable protocols (DataProvider, PostProcessor, SelectionFilter) which allow the consuming app to customize its behavior. We’ve ordered the explanation of its components, in order of precedence for importance for how you would use Ohana.

 

OHContact

This is the data model for a contact and is the output from the system: the end result. OHContact contains properties for fields like first and last name which are common on most contacts, as well as a list of OHContactFields for the arbitrarily named fields like ‘iPhone’ or ‘fax’:

@interface OHContact : NSObject <NSCopying>

@property (nonatomic, nullable) NSString *firstName;
@property (nonatomic, nullable) NSString *lastName;
@property (nonatomic, nullable) NSOrderedSet<OHContactField *> *contactFields;
// properties omitted

– (BOOL)isEqualToContact:(OHContact *)contact;

@end

 

OHContactsDataSource

The Data Source is where you retrieve the contacts from. It’s a Data Source in the Cocoa sense; it’s what provides the final contacts data to your application. The Data Source is the class which contains the core of Ohana’s internal implementation. It triggers contact loading via the Data Providers and uses the Post Processors to configure their data. Once the contacts have been loaded and processed, the onContactsDataSourceReadySignal is fired and the contacts list is populated. Consumers of the framework plug this custom Data Providers and Post Processors into this object to configure Ohana for their use case:

@interface OHContactsDataSource : NSObject

@property (nonatomic, readonly) OHContactsDataSourceReadySignal *onContactsDataSourceReadySignal;
@property (nonatomic, readonly, nullable) NSOrderedSet<OHContact *> *contacts;

– (instancetype)initWithDataProviders:(NSOrderedSet<id<OHContactsDataProviderProtocol>> *)dataProviders postProcessors:(NSOrderedSet<id<OHContactsPostProcessorProtocol>> *)postProcessors;

– (void)loadContacts;

// properties omitted

@end

 

OHContactsDataProviderProtocol

The data provider protocol describes an interface to retrieve contact information from a source of your choice. Ohana provides default implementations to use the iOS’s address book via the ABAddressBook or CNContacts API. You can use multiple data providers at once. You could, for example, implement your own data provider which retrieves contacts from Facebook and interleaves them with the system contacts:

@protocol OHContactsDataProviderProtocol <NSObject>

@property (nonatomic, readonly) OHContactsDataProviderFinishedLoadingSignal *onContactsDataProviderFinishedLoadingSignal;
@property (nonatomic, readonly, nullable) NSOrderedSet<OHContact *> *contacts;

– (void)loadContacts;

// properties omitted

@end

 

OHContactsPostProcessorProtocol

The Post Process protocol describes an interface for filtering the data to be exposed to the consumer. A Post Processor could filter for only contacts which have associated phone numbers, or reorder input contacts to be displayed in alphabetical order. You could then chain these Post Processors so that the data reflects both transformations. These and many other useful Post Processors are available by default in Ohana.

The real power of the system emerges when the consumer implements their own Post Processors for their custom needs. Do you want to filter for contacts with a certain area code? Dedupe contacts by shared address? Ensure you only display contacts that have all of the fields you happen to care about? All of these use cases fit nicely within the API. (Once you’ve built them, consider making a pull request!)

@protocol OHContactsPostProcessorProtocol <NSObject>

– (NSOrderedSet<OHContact *> *)processContacts:(NSOrderedSet<OHContact *> *)preProcessedContacts;

@end

Here’s an example of filtering if you had the following data set…

{ “first”: “Marge”, “last”: “Simpson”, “image”: <UIImage> }
{ “first”: “Mom”, “last”: nil, “image”: <UIImage> }
{ “first”: “Wednesday”, “last”: “Addams”, “image”: <UIImage> }
{ “first”: “Homer”, “last”: “Simpson”, “image”: <UIImage> }
{ “first”: “Maggie”, “last”: “Simpson”, “image”: nil }
{ “first”: “Morticia”, “last”: “Addams”, “image”: <UIImage> }
{ “first”: “Gomez”, “last”: “Addams”, “image”: <UIImage> }
{ “first”: “bae”, “last”: nil, “image”: nil }

…and wanted to filter for complete data, and change the data to be sent to a server in a different format such as the following:

{ “full_name”: “Morticia Addams”, “image”: <UIImage> }
{ “full_name”: “Gomez Addams”, “image”: <UIImage> }
{ “full_name”: “Wednesday Addams”, “image”: <UIImage> }
{ “full_name”: “Homer Simpson”, “image”: <UIImage> }
{ “full_name”: “Marge Simpson”, “image”: <UIImage> }

You could use three Post Processors to execute the following steps:

  1. Filter for contacts with data for all of the: ‘first’, ‘last’, and ‘image’ fields. (OHSplitOnFieldTypePostProcessor)
  2. Order the contacts by ‘last’, then ‘first’ (OHAlphabeticalSortPostProcessor)
  3. Merge the ‘first’ and ‘last’ fields into a ‘full_name’ field (your custom Post Processor implementation)

 

OHContactsSelectionFilterProtocol

Since selecting and deselecting contacts in a UI is an exceedingly common use case, Ohana provides first class support for statefully filtering selections and notifying when they happen. For example, a contact picker might only allow for selecting three contacts at any one time (OHMaximumSelectedCountSelectionFilter), and may want to show an clear error state and cancel selection when the user attempts to select a fourth contact.
We won’t further describe this API here, but in our open source documentation we have some example implementations to learn more about how contact selection in Ohana works.

 

Using Ohana as Part of Your Project

Setting up Ohana as a simple consumer of the iOS system address book is easy. This code snippet from a ViewController instantiates a single Data Provider for the system address book and a single Post Processor to order those contacts by their full names:

let alphabeticalSortProcessor = OHAlphabeticalSortPostProcessor(sortMode: .fullName)
var dataProvider = OHABAddressBookContactsDataProvider(delegate: self)
let dataSource = OHContactsDataSource(dataProviders: NSOrderedSet(objects: dataProvider), postProcessors: NSOrderedSet(object: alphabeticalSortProcessor))

Finally, we indicate which code to trigger when the contacts are returned, and we call to load them:

dataSource.onContactsDataSourceReadySignal.addObserver(self, callback: { [weak self] (observer) in
   self?.tableView?.reloadData()

   // The table view reads contacts from dataSource.contacts
})
dataSource.loadContacts()

For a basic contact picker view, that’s it. All that remains is to hook your OHContactsDataSource up as the data source for your table view.

Ohana allows you to spin up a basic address book UI in a very small number of steps.

Ohana includes an example app with a large set of examples in both Swift and Objective-C. The example app shows how to make more complex transformations, as well as how Ohana seamlessly handles asynchronous issues like auth challenges.

 

Using Ohana at Uber

The effort that produced Ohana started on the driver signups team, whose mission is to increase partner signups via the driver app. We found that componentization and the careful separation of concerns in code was essential in allowing us to maintain codebase quality while doing the rapid experimentation we were tasked with.

Once properly componentized, our contacts handing logic became extensible enough that other engineers wanted to use it. Teams responsible for contacts display UI in other parts of our apps (e.g. fare splitting, share my ETA, invites) quickly consolidated on using the same architecture. The contacts data model that we’d created became the de facto format for interacting with contact information across every layer of the app.

We’ve since open sourced Ohana. Uber has a flexible open source policy. The code we release must be of general interest to the open source community, and we must be able to license it to allow it to have utility to others. Importantly, there should also be a clear set of owners for the open source project who can commit to handling the project’s lifecycle as the open source community becomes involved, and as the technical environment around the project changes.

Since open sourcing Ohana, we’ve added support for managing dependencies with Carthage, added features, and fixed some bugs in Swift 3 support.

Making iOS contacts more accessible than the core APIs allow gives you more creative control how you display contacts from within the app. It will be useful for handling contacts in your project if your needs go beyond showing the default iOS contact picker. Take a look at Ohana, see if it matches your project’s needs. We welcome pull requests, and we at Uber Engineering would love to see the wider iOS community give the tool a try.

 

Adam Zethraeus is an iOS engineer on Uber Engineering’s Driver Team, who developed Ohana alongside Maxwell Elliot, Doug Togno, and Nick Entin. The Driver Team is hiring talented mobile engineers to help make driving with Uber a better experience.

0 Comments