App Coordinator for Adaptive User Interface Design
Leveraging Patterns in Software Design Architecture to Improve User Experience
Over the last several years, the App Coordinator software architecture pattern has gained popularity among developers because it helps manage many of the problems encountered when developing mobile applications. This pattern employs established software design principles, such as single-responsibility and dependency inversion, to help keep code robust and maintainable. More so, writing mobile applications with the App Coordinator pattern often leads to highly reusable software, which greatly facilitates feature development and helps to extend the life of an application’s code base.
In this article, we discuss our recent efforts at URBN to improve the user experience of our iPad applications (Anthropologie, Free People and Urban Outfitters) by utilizing the App Coordinator pattern to develop a novel catalog browsing experience that takes advantage of existing view components. In doing so, we’ll show that by invoking dependency inversion and protocol composition, our new interface requires minimal changes to our existing modules.
What are Coordinators?
Coordinators are objects responsible for implementing flow control within an application. This may include tasks such as navigation, authentication, networking and many more. In a specific case, Coordinators can be viewed as objects that “boss around” view controllers in an iOS application. But the overarching concept is that they promote segregation between your views and keep them focused on specific tasks rather than incurring bloat from implementing required flow control in an application.
At URBN we exploit the coordinator pattern by using them to manage flow control in our applications. The coordinators we use take several forms such as managing a view controller, creating child coordinators and performing one-off tasks, often in combination.
Product category navigation is an essential part of many e-commerce applications. Typically, a customer may be presented with a menu of selectable product categories which they can expand in order to see related child categories. Selecting one of these child categories may then take them to another view showing image tiles for associated products.
We implement the parent-child navigation paradigm with an accordion-style interface as shown below.
In our codebase, this view paradigm is implemented in
CategoryNavigationViewController and a truncated summary follows:
CategoryNavigationViewController displays navigation items (containing product categories and other destinations) in a
UITableView instance and responds to user input. If a selected cell corresponds to a product category with children, it either expands or collapses the accordion of associated categories as shown above.
But what happens if a selected item does not have any children? Should this view controller show the product tile view controller like in our example above? If so, should it be pushed onto the navigation stack or presented modally? Perhaps the user should be redirected to an entirely different part of the app? Clearly, managing all this business logic goes beyond the view controller’s responsibilities.
Rather than convolute our view code with complex business logic, we have the view controller depend on an abstraction defined by the
CategoryNavigationDelegate protocol. This technique is known as dependency inversion and allows us to decouple the view from the business logic in our application. We utilize the App Coordinator concept and define a Flow Coordinator object, which by conforming to our protocol, implements the desired behavior when a navigation item is selected.
Limitations of the Current Design
Our view controller and coordinator architecture is well-suited for iOS devices when the view’s horizontal size class is “compact,” as is the case for iPhone devices in portrait mode. However, the design does not scale well when the view’s horizontal size class is “regular,” which includes many iPad devices. Consider the following screen where the view controller is implemented on an iPad.
It’s clear this design wastes screen real estate as the cells contain little more than a category name. This deficiency led us to explore other solutions for presenting catalog data in a more efficient and visually pleasing manner.
Developing a Better Experience
In order to improve our browsing experience on the iPad, our engineering and UX design teams worked closely to set forth clear goals for our new design:
The design should be adaptive. On large devices the UX design should appropriately take advantage of available screen real estate in order to provide users with a rich, organic browsing experience. On smaller devices the existing navigation experience should be maintained to keep the UX simple and uncluttered.
The design should reuse components. When possible, we would like to leverage existing components in our app. Not only does this minimize overall development time, but it will help us produce an experience that is familiar and intuitive for users.
The design should introduce minimal changes to existing code. We wish to avoid introducing new bugs and lengthy QA regressions.
Using these goals as our guidelines, we developed a split navigation UX which we call the
CategoryBrowseViewController shown below.
The design concept retains the familiar accordion design in the left pane but instead of stretching the accordion across the screen for larger devices, we implement
ProductBrowseViewController in the right pane to display a scrolling list of product tiles. On smaller devices, the new view controller only displays
CategoryNavigationViewController and maintains the existing functionality. Thus, the new design is adaptive for any size device and repurposes existing modules in our app.
Now, our iPad users have a browsing experience that couples categories with their associated products without ever leaving the view.
Building the Interface
At first glance, our new view controller strongly resembles UIKit’s
UISplitViewController. However, we opted to write a custom container view controller for several reasons:
- More control over the width of the primary and secondary view controllers and the ability to override their trait collections
- The flexibility to use the view anywhere in the navigation stack (Apple recommends using
UISplitViewControlleras a root view controller)
CategoryBrowseViewController is initialized with two child view controllers:
CategoryNavigationViewController(primary) is required
ProductBrowseViewController(secondary) is optional since there may not be associated products for the category
isExpanded property determines the child view controller layout and is based on the view controller’s horizontal size class- if it’s “compact”, only the primary view controller is shown. If it’s “regular”, both primary and secondary view controllers are shown.
If the view controller view is expanded, the secondary view controller can be manipulated using the
updateBrowseViewController(_ :) function, which removes the existing
ProductBrowseViewController instance and replaces it. This allows a user to switch between categories on the same view.
More functionality, more responsibility?
In our new view controller’s implementation, there is no mechanism that allows the child view controllers to directly message each other. So how does the view controller know what to do when a navigation item is selected? What should it do if a product tile is selected or search, sort and filter functions are activated?
Again, we run into an issue of deciding where this business logic should live. At first, we might try to have
CategoryBrowseViewController conform to the protocols that abstract the functionality for both of its child view controllers. These protocols are below:
But this is problematic for a few reasons:
- We want to avoid putting business logic in our view code
- Reimplementing these would be redundant because our app already handles the existing functionality to route navigation items and implement product browse actions (product selection, search, sort and filter).
The true change in our new design is determining if the new view controller is showing an expanded view and if so, updating the child view controller in response to a navigation item being selected. All other functionality should remain as it were.
Coordinating the View Controllers
In order to implement the new functionality from our design and maintain existing behavior, we implement a new module, the
CategoryBrowseCoordinator into our app. Its responsibilities are:
- Maintain an instance of
- Conform to the
- Maintain a reference to a
forwardingDelegateobject (more on this in a bit)
The gist for this coordinator is below:
By conforming to
CategoryNavigationDelegate, this coordinator functions as a “proxy” between our view controller and our app’s Flow Coordinator by handling navigation item selection as follows:
- If the view controller is expanded, update the secondary child view controller with a new instance of
- If the view is collapsed, pass the selection to the
forwardingDelegateso it can decide how to route the navigation item
So what exactly is the
forwardingDelegate? It's an object that conforms to a new protocol called
CategoryBrowseDelegate, which uses Swift protocol composition to combine the interfaces defined by the
This delegate is an important object in our coordinator for a few reasons:
- It points to an existing object that can handle our desired routing behavior when the view controller’s interface is collapsed
- It's used to initialize new instances of
ProductBrowseViewControllerwhen the view is expanded, since the delegate points to an existing object that implements that view controller’s interface methods.
The new coordinator gives us the flexibility to handle our application’s business logic in a context-sensitive manner based on the view configuration. Additionally, using protocol composition for our event-forwarding delegate allows us to reuse modules that implement existing functionality thereby avoiding the need to add redundant code to our app.
We can show the relationship between our view modules, interfaces and coordinators in the diagram below:
We’ve shown that the App Coordinator design pattern is a powerful tool for developing new user experiences from existing components. When user-facing view modules depend on abstractions, coordinators can implement business logic in a context-sensitive manner without modifying existing view code. We also show that protocol composition can be used to reroute functionality to existing modules to avoid adding redundant code. Beyond our category browsing experience, we’ve leveraged this approach to improve other features in our apps by reusing existing code and UX designs from our iPhone experience.