The New Swipe Right with Swift


Mike Hall

By Garo Hussenjian, iOS Architect & Engineering Manager

When it comes to living or working in small spaces, great architecture is essential. When you step into the house of Tinder, you see a simple, modern design. But if these walls could talk, they’d have a lot to say.

From the moment the app was first built, form followed function. Over time, as more people visited, the space had to adapt. Behind the clean aesthetic, you’ll find layers of innovation. Tinder hosts a large set of premium features like Passport, Rewind, Super Like, Boost, and Smart Photos. The core experience also integrates UI for onboarding, managing preferences, viewing profiles, messaging, and swiping with friends.

In late 2016, we needed to redesign our card stack for a more streamlined user experience. At the same time, we wanted to improve our code quality with a more sustainable architecture. But, if you’re working in tech, the expectation for speed is as high as the expectation for quality. We knew that packing more features and interactions into a single controller was no longer scalable, and without rethinking our architecture, it would take even longer to implement our new design.

It was the perfect storm where timing, resources and collective wisdom would converge to make the case to invest in long-term goals over short-term gains. With the support of our forward-thinking leadership team, we dedicated ourselves to step back and rethink the strategy to implement our product vision. With a new look and feel, a new app architecture, and Apple’s new open-source programming language, we set out to reinvent the Tinder card stack.

A New Look and Feel

Since its introduction, Tinder has consistently raised the bar on app design. In early 2016, we expanded the service to support swiping with friends, streamlined our game buttons to allow for larger cards, and updated our content layering to allow for cards to break out of their container's bounds.

For 2017, we set out to expand our profile photos to fill out our new "edgeless" card design. To maximize flexibility for future design improvements, we re-engineered our card stack to allow us to inject our new design and continue to experiment going forward. As a result, we are now able to iterate more rapidly while reducing the risk of introducing new bugs. Our new design helped pave the way for a completely new app architecture that would not only be valuable to Tinder, but also of interest to the iOS community.

Introducing DISCOVER - An Architecture Born at Tinder

Tinder, like many iOS apps, was originally built on MVC, with our more recent components built using MVVM. While these pattern languages provide some degree of separation of concerns, they are often misunderstood as architectures. They can't always handle the growing complexity introduced when layering new features over time. With limited real estate, mobile apps are especially vulnerable to packing more features over a smaller number of screens. This imbalance gives rise to views and controllers, which, despite our best efforts, have too much responsibility.

MVC alternatives like MVVM and VIPER work very well to limit the responsibilities of our controllers, but view models and presenters, like controllers, are UI-facing with respect to component design. We needed some additional abstraction around our business logic away from our presentation layer in order to help prevent that logic from leaking back into our UI components.

In response to these demands, we combined two distinctly modern architectures, DCI (by Trygve Reenskaug and Jim Coplien, also known as "Lean Architecture") and Clean Architecture (by Robert “Uncle Bob” Martin). At the heart of both DCI and Clean is an emphasis on use cases as the structural building blocks of the application. Both architectures continue to leverage MVC in their user interface component design, while expanding on the underlying implementation of our business models and use cases.

DCI advocates for implementation of use cases in Contexts, offering additional guidance in decomposing behaviors into object roles. Similarly, Clean Architecture promotes the use of Interactors, which implement a transactional request and response model. Combining these architectures, we are able to model both long-lived and transactional behaviors in clear and repeatable patterns. DCI Contexts serve as the environment for our use case execution. Clean Interactors implement the data transformations within that environment. DCI Roles serve to extend our core domain entities, adding methods only as needed within a Context or Interactor.

We leveraged the agility of DCI and the discipline of Clean Architecture to create something new, we call it DISCOVER.

Here are some of the objects and roles within the DISCOVER architecture:

  • Data - Transactional inputs and outputs, often related to a use case or behavior.
  • Interactor - An object that encapsulates a transaction executing within a context.
  • Service - An interface adapter to a resource, framework, or external dependency.
  • Context - An object encapsulating the behaviors associated with a single feature or use case.
  • Observer - A role implemented by a view model that updates the view.
  • View - An “MVC” or “MVVM” UI component. Includes View Models and View Controllers.
  • Entity - Core model objects representing key concepts in the business domain.
  • Routing - High-level flow controllers, modal presentation and dismissal, deep linking.

Swift 3 - A Language for Modern Applications

We’ve always loved Objective-C. It’s the language of the Mac and iOS. Many of the important lessons we’ve learned as software engineers, we’ve learned while writing Obj-C. This comes from reading it, writing it, building patterns, debugging, and shipping great apps. Objective-C has been the language of Tinder on iOS since the very beginning, but the time had come to move to Swift.

Swift is no longer the future of iOS development. Swift is the present.

Swift is particularly well-suited to implement DCI Roles within its protocol-oriented paradigm. When defining protocols and protocol extensions, we enable objects to adopt a variety of behaviors in tandem that are not otherwise achievable with traditional class-inheritance. For example, the card stack component is able to adopt protocols for swipe handling, batch updating, animation queueing, and custom transitioning. Each of these behaviors is modeled as a protocol and extended to allow shared behaviors to be utilized by other components in the system, or leveraged by test doubles as needed.

public protocol BatchUpdatable {  
    var updateItems: [UpdateItem]? { get set }
    func beginUpdates()
    func insertItem(at index: Int)
    func removeItem(at index: Int)
    func endUpdates()
}

public protocol AnimationQueuing {  
    var animationQueue: NSOperationQueue { get }
    func suspendAnimations()
    func resumeAnimations()
    func cancelAnimations()
}



public protocol RevealTransitioning {  
    var view: UIView { get }
    func update(withProgress progress: Double)
    func finish(withDuration duration: TimeInterval)
    func cancel(withDuration duration: TimeInterval)
}

Additionally, Swift enumerations provide considerable advantages to Objective-C when processing API responses. Rather than triggering callbacks with optional response and error parameters, we are able to model an API transaction result with a non-optional Result enumeration. This promotes proper handling of success or error cases, and bundles only the relevant data as associated values. This goes a long way to ensure that we handle all of our error cases and build a data pipeline that's easy to understand, yet difficult to break.

public final class LoadRecommendationsInteractor {

    public enum Result {
        case success([Recommendation])
        case error(Error)
    }

    public enum Error: Swift.Error {
        case authError
        case httpError(Swift.Error)
        case parseError(Swift.Error)
        case importError(Swift.Error)
        case recsExhausted
        case recsTimeout
    }

    public func load(completion: @escaping (Result) -> Void) {
        // Load recommendations from the API service, pass the response in a Result enum...
    }
}

Deeper in the model, our User, Group, and Recommendation entities share a common create-or-update batch algorithm which is implemented in a single protocol extension. Input and Output are left undefined in the protocol and are left to the implementation to provide the actual types like RecommendationJSON as Input, or Recommendation as Output.

public protocol BatchImport {  
    associatedtype Input
    associatedtype Output

    var input: [Input] { get }

    func inID(_ input: Input) -> String
    func outID(_ output: Output) -> String
    func fetchBatch(_ inputIDs: [String]) throws -> [Output]
    func insert(_ with: Input) throws -> Output
    func update(_ existing: Output, _ with: Input)
}

extension BatchImport {  
    func process() throws -> [String: Output] {
        var results: [String: Output] = [:]
        // A reusable, optimized create/update algorithm!
        return results
    }
}

struct RecommendationBatchImport: BatchImport {  
    typealias Input = RecommendationJSON
    typealias Output = Recommendation

    // We conform to the protocol, and get the process() algorithm for free!
}

Finally, Swift is more compact than Objective-C. It allows us to factor our code into smaller parts, without separately maintaining header and implementation files. These are significant advantages, both reducing the length of our files, and cutting the number of source files in half. By improving the signal-to-noise ratio, Swift helps us better understand the structure of our code in terms of modular, feature-oriented components. From concept to execution, our language and our code remain consistent and clear.

Built to Last

A ground-up rewrite of the Tinder card stack would not have been successful without the hard work and contributions of our talented designers, iOS engineers, and leadership. Working together we were able to dedicate our time and attention to hundreds of details, all of which are vital to the overall polish of Tinder. Our new code is cleaner because of our new look and feel. Our new architecture is leaner because of our new language. And our new swipe experience is smoother because of our new architecture. As it all came together, we found that going back to the blueprint was a bold swipe in the right direction.