On Building a Fluid User Interface

By Rishi Mody

May 13, 2021

In late 2019 we launched Threads from Instagram, our standalone messaging app designed to help you stay connected to your close friends. Threads was built from the ground up as an entirely new experience, and presented an opportunity for us to rethink our approach to user interfaces.

We wanted to deliver an experience that felt both fluid and responsive, and it was clear that relying on standard UIKit components was not going to achieve what we had in mind. Mobile design patterns may have evolved over the years, but many of the underlying components have not.

Through a highly iterative process, a small team of engineers and designers worked closely together to rethink some of our standard user interface components, in order to develop the experience that you see in the app today.

While not comprehensive, in this post we’ll share insights into a few of the creative implementations that give the Threads app its unique feel.

Responsive Buttons

Buttons are one of the most basic components of any user interface, and yet are rarely given much thought during development. In fact, buttons in UIKit have changed surprisingly little since iOS 7, when they dropped their original skeuomorphic design.

Standard UIButtons generally respond to user input by lightening the color of their content (i.e. the title or icon) in order to indicate when they’re highlighted. This response tends to feel somewhat static. It doesn’t give you the sense that you’re physically interacting with the component because it doesn’t respond like a physical component would respond.

We wanted to reimagine this simple interaction, and identified a few properties that make for a more satisfying touch response.

  • Physical — The response should feel physical. A button should depress as you touch down, and elevate as you release. We used scale transform animations for this.
  • Proportional — The response should be proportional to the amount of input. A long press should depress a button more than a quick tap. We used interruptible property animators for this.
  • Tactile — The response should feel tactile. A button should provide tangible feedback when you interact with it. We used light haptic feedback for this.

This button interaction is implemented via a UIView extension. Internally in any UIView, you can override a protocol called UIResponder which gives you access to the touch events, allowing you to implement any custom behavior that you want.

The scale transforms are implemented by applying standard CGAffineTransforms to the view’s layer, and the animations are implemented using interruptible UIViewPropertyAnimators, with spring timing parameters for a smooth animation curve.

Elastic Scrolling

In any standard UITableView or UICollectionView, content generally scrolls as one static group. Even when bouncing as the content reaches the edge of the scroll view’s bounds, individual cells remain fixed relative to one other. This breaks the illusion that each cell is a distinct component.

We thought scrolling should feel more dynamic, and introduced a unique elastic scrolling mechanic where the content cells of a scroll view will actually stretch independently as you pull on them.

This elastic scrolling mechanic is implemented via a custom UICollectionViewFlowLayout.

Every UICollectionView is backed by a collection view layout which dictates how that collection view works. Generally, this is used to define things like the size of the cells, the scroll direction, etc. By subclassing the UICollectionViewFlowLayout, however, you can actually redefine the scroll behavior internally, which at a high level is what we’re doing here. Each time the list is scrolled, we recalculate the UICollectionViewLayoutAttributes for each cell relative to the current content offset, giving the illusion that the cells are actually stretching.

One detail that’s worth noting here is that for tappable components within a UIScrollView, at the moment the user touches down it’s unclear yet whether the intent of the gesture is to tap or to scroll.

For standard cells in a UITableView or UICollectionView where the default touch response is to adjust the background color of the cell to indicate when it’s highlighted, it can be quite jarring to immediately handle the touch because the cell under the user’s finger will appear to flash every time they start scrolling. The standard UIKit solution to this is to delay handling touches in a scrolling context until it’s clear what the intent of the gesture is — a rather inelegant solution. There’s a corresponding property on every UIScrollView called delaysContentTouches which is set to true by default.

However, because tappable components in Threads rely on scale transform animations that are proportional to the input, we can simply handle touches immediately and adjust accordingly if the user starts scrolling.

It’s a minor detail, but one that makes this type of interaction feel significantly more responsive.

Fluid Navigation

For standard navigation in UIKit, new screens are generally presented via either a UINavigationController or a modal presentation. In either case it involves a view being pushed onscreen from somewhere offscreen using uninterruptible animations and limited gesture controls.

We wanted to design a navigation system that felt fluid, allowing the user to navigate freely using lightweight, intuitive gestures. In order to give the user a sense of space and direction, we introduced contextual transitions where views are presented directly from the interactive components that represent them.

Key to this transition style is the concept of configureable mirror views. These are views that represent specific content items and support various layout configurations (e.g. an inbox cell and a thread header), with the ability to animate smoothly between them. During transitions, these views are used to mirror the source and destination content at the beginning and end of the transition, respectively, giving the appearance that an interactive component is actually expanding into the presented screen.

Our navigation transitions are implemented using a custom presentation controller that we call a fluid presentation dispatcher. This controller manages the transition animations, view hierarchy, and gesture handling.

Instead of the standard presentViewController:animated:completion: interface, the input parameters to this presentation are a series of configureable code blocks that define how the transition should operate. There are blocks for configuring the source view, the destination view, the mirror view, and the mirror view transition, as well as for configuring additional views at the beginning and end of the transition. Internally, the fluid presentation dispatcher functions like a state machine, executing each corresponding block in coordination with the various stages of the transition animation.

This type of configureable interface makes this presentation controller extremely flexible, able to support fluid navigation transitions for a variety of content types across various surfaces in the app.

In order to feel responsive, it’s essential that a navigation system be able to handle gestures from the user at any time, even if a transition is already in flight.

Whether redirecting a view that’s already transitioning or interacting with an underlying component that’s partially visible, handling these types of gestures intuitively is important to give the user a sense of direct manipulation. Using interruptible animators and custom gesture handling, our fluid presentation dispatcher allows the user to navigate as quickly as they can gesture.

Dynamic Theming

We knew from the beginning that we wanted to support theming in Threads. But keep in mind this was back in late 2018, long before dark mode had been implemented in the Instagram app (there’s a great post about that here).

We introduced a dynamic theming system that supports five distinct themes, each with its own app icon, as well as an automatic mode that respects the device settings.

Through a process called appearance binding, this system handles theme updates in realtime, allowing us to reconfigure individual views in the app without reloading each view.

Using an appearance provider class that vends the current theme’s colors, we bind individual objects to a given code block during initialization, in which we configure properties of the view that are dependent on the theme (i.e. the background color, the text color, etc.). Relying on NSNotificationCenter internally, this system announces theme updates to each appearance provider, which in turn calls each view’s configuration block as necessary.

It’s a relatively simple system, but has proven to be an effective way to manage theming.

Final Thoughts

Threads is in many ways a product of the broader Instagram culture. We place a high value on craft, and firmly believe that the details are important when you’re building products used by millions of people. We have a strong prototyping culture, which enables some of our most creative engineers and designers to do their best work, often driven by passion rather than a roadmap. These qualities are reflected in the work we do, and Threads is a great reminder of why that matters.

If you are interested in joining one of our engineering teams, please visit our careers page.

Rishi Mody is a software engineer on the Instagram Threads team.

RELATED ARTICLES

Check out more articles about engineering