Making reusable, configurable, and clean UI

Chen Hao
iHeartRadio Tech

--

Have you ever heard “Ohh nooo” in the office when developers or designers need to make a new screen for a native iOS app? If not, they might have just gotten used to it.

@mainasu otoko

Making a new screen or UI (user interface) element takes a lot of time, resources, and space, especially when it closely mirrors other current designs. A developer has to write redundant code for each new screen, which makes the code base bloated. Copy/pasting not only produces duplicated code that takes disk space, but also makes it more complicated to manage or update. For designers, it often happens that multiple people create different screens, and without strict standards, all those screens end up having different styles.

So, why don’t we create a standard library which saves both developers and designers from this pain? What about making a highly reusable and configurable UI library that takes care of building UI and keeping unification all by itself?

By the way, if you’re interested in working on challenges, problems and solutions like this one, check out our iHeartRadio Jobs page.

How do we make it?

First, we want to figure out where it will be used and what we need from it. “UI” is a very general word. It can be as simple as a color or a font. It could be a button, a list view, a cell or an image. It could be a whole screen which contains other elements. In other words, everywhere! To keep things standard and to reduce redundancy, we will need a tool which gives enough options for developers to generate standardized UI without writing UI code. We need it to be configurable enough to take any combination of data model, style and theme, then build the UI for us. Also, we would like it to listen to changes and updates we give. Impossible, right?

With the use cases and our goal in hand, we can now define what the constants and variables are. To make it reusable, we are going to need static APIs with model, style and theme options as input types. We need some protocols for integration. We want to hide internal code to make the integration as easy as possible. We want to support as many screens, views, elements, and colors as possible. It should be able to combine multiple inputs of different types of configurations to fulfill the complex requirements.

Here is a visual diagram of our chosen solution to this problem, which we call “iOSCompanion”:

CompanionColor

CompanionColor is the first element we created because almost all UI elements need to have colors. As a standard color library, it provides colors to UIs based on their type. For example:

someView.backgroundColor = UIColor.companion.viewBackgroundColor
anotherView.backgroundColor = UIColor.companion.viewBackgroundColor
someButton.textColor = UIColor.companion.buttonTextColor

Based on the element type, CompanionColorFactory finds the color for it. So in this case, if you change the UIColor.companion.viewBackgroundColor, the backgroundColor of both someView and anotherView are updated automatically.

In ColorFactory, we create CompanionColorTheme, so that we can assign different color combinations to all different types of elements in the app. We are able to apply different themes without making any changes in the UI code. We keep all color assignments (color mapping) within the ColorFactory.

RemoteTheme gives us the ability to load themes from a remote server. For example:

{brandColor:{hue:36, saturation:0.8, brightness:0.8},...}

Now, we have color support for all other elements.

CompanionFont

CompanionFontFactory contains pre-defined fonts, and is a basic element that other interfaces can share. Here is how we make FontFactory work for other UIs:

createText(with: String, font: CompanionFont) -> NSAttributedString

The FontFactory also supports different formats of inputs:

createText(with: [Any], font: CompanionFont) -> NSAttributedString

You can just pass an array of images and strings, plus the font into it:

let titleText = CompanionFontFactory.createText(with: "Cell Title 5", font: .tileTitle1)let descriptionText = CompanionFontFactory.createText(with: [UIImage.sunset, “ Description with image”], font: .caption)

Output:

CompanionCollectionView

Now that we have ColorFactory and FontFactory, it’s time to build something visible. CollectionView is one of the most commonly used elements in the iHeartRadio app. Currently we have too many collectionView classes in our codebase. One goal for CompanionCollectionView is to have just one collectionView which supports all different UI styles, a highly configurable and reusable module which keeps design standards throughout the app.

In order to make this work, we’ve created viewModels for each layer of the UI hierarchies. Each model takes care of its own UI responsiblities:

With the clear one-line structure, we are able to customize the present styles individually per level of the hierarchy. We can now not only have different cell styles in different sections, but also be able to adjust any of them at runtime. By modifying styles in different models, we can use this one collectionView class to replace all of the nearly redundant classes. Now, even if there are future design changes, there is no more need for handwritten collectionViews!

Here are some of the configurable things we have:

.layout provides pre-made layoutStyles which are commonly used in the iHeartRadio app:

Single Cell

It also supports carousel:

Single Cell in Carousel
Triple Cell in Carousel

ContentStyle provides options for us to show different contents for same viewModel. For example, cells with contentStyle.secondary don’t show title or description. The only difference in code is just:

contentStyle = .secondary

There are additional features we implemented in CompanionCollectionView such as setting image style, tint and blur mode. You can also define HeaderStyle for each collectionView.

Optimization

CompanionCollectionView is following the MVVM design pattern. It is a protocol for data handling and user interactions, making editing an existing view easy. Add, update, remove actions are all through collectionView’s viewModel. Actions and interactions are handled in collectionView’s delegate. With this design, code from outside of this project now only has to know this main basic collectionView and its viewModels. We’ve hidden the complex internal operations inside of the iOSCompanion project.

One challenge we had was the integration of carousels. A carousel is basically a collectionView that we have to fit in another parent collectionView. We want to seamlessly port carousel into our CompanionCollectionView system. To make this work, we need to translate the carousel’s IndexPath for the parent collectionView to use. We also need to feed the right datasource from the parent collectionView model layer into the carousel.

The indexPath of a cell is managed by the collectionView. In other words, a cell (or a carousel as a cell of it’s parent collectionView) has no idea where itself is located. For example, the indexPath of the 2nd cell in a carousel is IndexPath(item: 1, section: 0). If the carousel is in 2nd section of the collectionView, the correct indexPath supposed to be IndexPath(item: 1, section: 1).

In order to let the viweModel of the collectionView knows the correct indexPath, we need to tanslate the carouselCell’s indexPath to fit the parent collectionView’s coordination by using section info from the collectionView and item info from the carousel. Such as:

IndexPath(item: theCarousel(indexPathOfCell: theCell).item, section: theCollectionView(indexPathOfCell: theCarousel).section)

To provide a dynamic dataSource for carousels, we created a protocol based section(caruosel) dataSource as a bridge between two layers of collectionViews. With the indexPath translation, CompanionCollectionView can now treat a carousel as a normal collectionViewSection.

But wait, there’s more!

We didn’t stop there. With this, we created three different ways to add custom views to any position of the app. You can add as many custom UIViews as cells, sections, or decoration views as you want.

For example:

A custom view in a cell can be used as a showMoreCell:

A custom view in a section can be used as a banner:

A custom decorationView is a way to add a view to the collectionView without affecting the indexPath. To implement, override each item’s position in the collectionView. By inserting some empty space into the collectionView’s flow layout, we can now add views to the empty space as add-ons of the collectionView.

Another advantage of using custom view as a decoration view: when you add a new item to the section, the custom view will stay in it’s designated place.

It might also be a good way to insert custom views into collectionViews to show some temporary content to users, like some additional marketing info such as a promotion.

How does this help?

For developers, the iOSCompanion Project could save them a large amount of time writing UI code. For example, the Playlists tab is our most complex view with multiple layouts and content styles. Here is how we build the UI using CompanionCollectionView:

  1. Create YourPlaylist SectionModel with items and one showMoreCell:
let yourPlaylistsModels = [newCell(), newCell(), testCellModelMore(with: "Show All Your Playlists")]let yourPlaylistsStyle = CompanionCollectionViewSectionStyle()let sectionYourPlaylists = CompanionCollectionViewSectionModel(sectionTitle: "YOUR PLAYLISTS", sectionStyle: yourPlaylistsStyle, with: yourPlaylistsModels)

2. Create Moods SectionModel with items. Set the layout style to triple carousel. Set contentStyle to .secondary to hide title and description:

let moodsModels = [newCell(), newCell(), newCell(), newCell(), newCell(), newCell()]let moodsStyle = CompanionCollectionViewSectionStyle()moodsStyle.layout = .carouselTriplemoodsStyle.contentStyle = .secondarylet sectionMoods = CompanionCollectionViewSectionModel(sectionTitle: "FEATURED ARTIST RADIO", sectionStyle: moodsStyle, with: moodsModels)

3. Create Decades SectionModel with items and the same styles as Create Moods:

let decadesModels = [newCell(), newCell(), newCell(), newCell(), newCell(), newCell()]let decadesStyle = CompanionCollectionViewSectionStyle()decadesStyle.layout = .carouselTripledecadesStyle.contentStyle = .secondarylet sectionDecades = CompanionCollectionViewSectionModel(sectionTitle: "FEATURED DECADES", sectionStyle: decadesStyle, with: decadesModels)

4. Create the Featured SectionModel with six items. Set the layout style to single carousel:

let featuredModels = [newCell(), newCell(), newCell(), newCell(), newCell(), newCell()]let featuredStyle = CompanionCollectionViewSectionStyle()featuredStyle.layout = .carouselSinglelet sectionFeatured = CompanionCollectionViewSectionModel(sectionTitle: "FEATURED ARTIST RADIO", sectionStyle: featuredStyle, with: featuredModels)

5. Create the Genres SectionModel with ten items. Set the layout style to double. Set contentStyle to .secondary to hide title and description:

let genresModels = [newCell(), newCell(), newCell(), newCell(), newCell(), newCell(), newCell(), newCell(), newCell(), newCell()]let genresStyle = CompanionCollectionViewSectionStyle()genresStyle.layout = .doubleRollgenresStyle.contentStyle = .secondarylet sectionGenres = CompanionCollectionViewSectionModel(sectionTitle: "GENRES", sectionStyle: genresStyle, with: genresModels)

6. Finally, create the viewModel for the CompanionCollectionView. Set the headerStyle. Then, create CompanionCollectionView with the viewModel:

let viewModel = CompanionCollectionViewModel(collectionTitle: "Playlists", with: [sectionYourPlaylists, sectionMoods, sectionDecades, sectionFeatured, sectionGenres])viewModel.headerStyle = .titleself.viewModel = viewModellet collectionView = CompanionCollectionView(viewModel: viewModel)collectionView.delegate = self

Now we can build any of our main tabs in no more than 50 lines of code!

Playlists tab of iHeartRadio app in Companion Test app

Now for designers, since we now have only one class for each type that fulfills all the design specs, it allows designers to think in terms of element types when building UI. Instead of designing a view, working with reusable components not only saves time, but also helps designers maintain good design standards. Who doesn’t love an app that looks clean and unified?

--

--