Chapters

Hide chapters

Advanced iOS App Architecture

Fourth Edition · iOS 15 · Swift 5.5 · Xcode 13.2

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

Section I

Section 1: 9 chapters
Show chapters Hide chapters

7. Architecture: Elements, Part 1
Written by René Cacheaux & Josh Berlin

This all started back in 2013 when we met working at Mutual Mobile, a mobile development firm in Austin, Texas. The company asked us to fly to New York and work with an iOS team at Google. It was an immediate, “Yes!” from both of us.

We weren’t sure what to expect. Google didn’t hire “iOS Engineers” at that time and expected their engineers to switch programming languages depending on the project. Most of the team had strong Java backgrounds and learned Objective-C specifically for this project. They seriously impressed us with their knowledge of the iOS SDK and the nuances of Objective-C. While we were able to teach them things like Core Animation, they were able to teach us about software development as a whole.

Their Java background came with an ingrained idea that every project needed dependency injection. At the time, and even today, the use of dependency injection in iOS projects is rare. Apple doesn’t have a built-in framework to help manage dependencies, which doesn’t help. To the Google engineers, this was absurd and they decided to use a third-party framework called Objection to handle dependencies in the project.

Injecting dependencies into each view controller allowed us to mock objects and write unit tests. For this project, every change request required unit tests. No exceptions. This was a departure from some of the more lax projects at Mutual Mobile. But it was a good departure. Dependency injection and unit tests changed our view on iOS development and sparked our interest in software architecture.

Back in Austin, one of the developers at Mutual Mobile started a brown bag group to watch Robert Martin — better known as Uncle Bob — videos. His videos were full of great insights about software architecture. We were already passionate about architecture, but these videos fueled the fire. Uncle Bob took ideas from multiple architectures and broke them down to their core objective: separation of concerns. He used this idea to create clean architecture, which divides software into multiple layers including business rules and interface adapters.

At about the same time, we started working on a brand new greenfield iOS project for a major American brand. We knew this project was going to be difficult, but we were excited to use our new-found passion for architecture to help the team succeed. The client asked us not only to build an awesome iOS app, but an SDK so other apps within the company could reuse the app’s core functionality. René was the tech lead and knew architecture would be key to the project’s success. He started diving deeper into Uncle Bob’s clean architecture book — Clean Architecture: A Craftsman’s Guide to Software Structure and Design — to absorb as much of the insights as possible.

The app’s core feature was a chat service. We were asked to use XMPP, Extensible Messaging and Presence Protocol, but were told that we might have had to switch to a different chat protocol in 3–6 months. Our first thought was, “Are you serious?” Our next thought was the realization that, if and when we had to rewrite the chat layer, our data storage and user interface layers shouldn’t need to be touched.

René decided the best way to separate the chat layer from the user interface was to create two different projects: a “Core” SDK to hold the chat code and a user interface layer, which was the actual Xcode project. This forced us to build the user interface layer in a way that isn’t tied to a specific chat protocol, but gets handed immutable data objects such as a chat group or chat message. The user interface layer didn’t care if they came from XMPP or MQTT, Message Queuing Telemetry Transport, or push notifications.

The clear separation allowed engineers to work on the chat layer and the user interface layer in parallel. We were able to make rapid changes to isolated layers of the project without affecting other layers. The patterns developed and used throughout the app’s source code could be applied to many other iOS projects. What resulted was Elements.

Introducing Elements

Elements is an architecture meant to make iOS development fun and flexible. Elements organizes your codebase and makes your project easy for anyone to navigate. This organization allows you to make changes to layers of your app without affecting stability. A set of “Elements” make up the architecture. The cool thing is that you can choose which pieces to use in your own apps — there’s an Element for every layer of your app, from networking to the user interface.

Elements is our take on architecture, grabbing bits and pieces from industry best practices. The theory is not completely original since it pulls from many sources based on our experience. The set of Elements was created by mixing these best practices with our ideas and has evolved over time as more architectures make their way into the iOS world.

Organization is key to well-architected project. Specific pieces of logic should be easy to find. Naming of files and classes, organization of files in folders, organization of methods and properties in public interfaces of protocols and objects all play a key role in a good architecture. A better-organized project makes for a more flexible architecture. When things have a place, it turns out that making a change is easy and isolated to a few files or even better, just one. Point is, organization is important to architecture.

There shouldn’t be one and only one way to architect software. Software architecture is an artwork with some science mixed in. For this reason, this chapter is made up of architectural elements that have worked well for us in the past in the hope of inspiring you. Take what you like and change it if it makes sense. Use an Element as-is or make it it your own.

There’s no such thing as one architecture to rule them all. Well-architected apps are made up of modular components. Bits and pieces made up of different structures. Elements is not a take it or leave it approach. Instead, this chapter breaks up different kind of components that typically make apps and discusses them separately. You can take bits and pieces into your own apps or come up with entirely new elements to fit your needs.

Note: We feel that it’s important to emphasize that most of the concepts that make up Elements are not new. Elements is a collection of existing best practices that we’ve found most helpful when architecting iOS apps. We’ve taken these practices and evolved them over time to best fit iOS development.

Elements is designed on top of some core underlying concepts. Let’s take a look at these underlying concepts.

Underlying concepts of Elements

Entities allow objects to communicate

Every app has a domain. Yelp! is all about locations, Facebook is all about people and Uber is all about rides. Entities represent your app’s domain. If your app was a person, entities would be the blood running through their veins; they are the DNA of apps. Entities are the only values that travel across architectural layers. By holding this true, you end up building extremely flexible software: Software that doesn’t require you to rewrite everything every time something changes — every time a product manager comes up with a new feature, a UI designer wants to restyle a screen, a UX designer wants to change a flow or a server engineer wants to change an API.

Protocols make software flexible

Designing great protocols is key to building flexible software. They define the what and leave the how up to the implementations. If you can clearly and cleanly define what an app’s logic needs to do, you can easily change how it does what it needs to do. Flexibility is the ability to effortlessly swap implementations. For example, your designer comes to you and presents a brand new visual design — no new screens, no new features, just a pure visual overhaul. If up taking such a change breaks functionality, that’s a problem. Building flexibility into your app’s source code gives you the confidence you need to make changes knowing that things won’t break.

Encapsulation enables safe change

Let’s say your team is asked to swap out the backend from an old, raggedy API to some new trendy cloud database. Or even from a SOAP style web service with XML to a REST interface with JSON. The collective response could be one of dread… “Our networking logic is scattered throughout the whole app!” Or one of excitement… “No problem, this will be easy!” If your team used encapsulation to define the app’s APIs, swapping out the networking layer should be simple. Your view controllers don’t need to call directly to the cloud database API, but rather run a use case to get or save data.

Encapsulation allows you to abstract all of your backend logic in remote API classes. So when your team is asked to switch to a new backend, no one will freak out.

Elements

Elements are separated into two main categories: Core Logic and User Interface Logic. Core Logic contains the app’s business logic such as retrieving data from an API and caching data. User Interface Logic contains the presentation logic such as handling user input and navigation. In this section, you can read a brief description of each Element. Later in the chapter, each Element is covered in more detail.

Core Logic

Entity

Entities, also known as data-model objects, are light-weight structured data containers. They don’t do anything other than store values. Entities flow throughout the app, passed between architectural layers. They’re considered foundational to your app’s architecture. They constitute a contract between different object interfaces. When building an object’s interface, methods typically take in an entity object and perform some task. Sometimes, the method returns a new or modified entity object. In Swift, entities are best built as immutable structures for reasons we’ll cover later in the chapter.

Data store

Data stores take care of the CRUD, Create, Read, Update and Delete, operations. They abstract away the underlying data storage mechanism you want to use. They can implement Core Data, NSCoder and even remote data store network logic. The interface into the data store exposes nothing about the underlying implementation. Data stores take in and pump out entity objects, another reason entities are foundational to your app.

Remote API

Remote APIs talk to the network. They can create the endpoint and handle the response. Remote APIs know if you’re getting data from a cloud API like Firebase, a custom server or even a hardcoded JSON file. View controllers don’t care where the data comes from. By moving these implementation details out of view controllers and into remote APIs, view controller code becomes more readable.

Remote APIs are normally used to create and update data on a remote data store, but can make any type of network call.

Use case

Use cases represent the user stories that make up your app. They have names that everyone involved in the project can understand. If you were asked to describe what your users can do with the app, you would be naming use cases. Use cases are the main unit of work. Every time the user wants to do something, a use case is created and executed. Use cases cleanly separate your app’s core logic from your app’s user interface logic. Think of it this way: After you build all the use cases, you should be able to build a command line interface for your app using the use cases you’ve defined.

Broadcaster

Broadcasters notify subscribers when something in your app happens. Multiple objects can subscribe to a single broadcaster. As an example, you could create a reusable keyboard broadcaster that subscribes to the relevant system keyboard notifications. Encapsulating this functionality removes the need for multiple objects to directly subscribe to Notification Center notifications and instead conform to the broadcaster’s protocol.

User Interface Logic

Displayable entity

Displayable entity objects contain data that is presentable to a user. Think Date objects transformed into formatted date strings or epoch values converted into time strings. They are are created from entities, which contain only raw data. Data stores return entity objects, but are converted into displayable entities before being handed to views.

Observer

Observers are objects that receive external events. These events are input signals to view controllers. Observers know how to subscribe to events, process events and deliver processed events to view controllers. For example, a KeyboardObserver can process keyboard-related notifications from Notification Center when the keyboard is shown or hidden and knows which method to call on the view controller. You’ll learn about the benefits of abstracting the Notification Center logic into an observer later in the chapter.

User interface

User interfaces are, well… user interfaces. These objects allow you to configure what is rendered on the screen. Each view controller’s view has a user interface protocol. They expose methods such as enableSignInButton() or startEditingFirstName(). They don’t however expose implementation details such as UIKit objects. These objects express every possible change you can make to the user interface.

Interaction responder

User interface objects are dumb. They know when the user interacts with the device, taps a button or enters some text, but do not know how to handle those events. That’s where the interaction responder comes in. The user interface tells the interaction responder what to do, and the interaction responder knows how to do it. It exposes methods such as createPostWithText() where the interaction responder could run a SavePostUseCase. Normally, a user interface’s interaction responder is its view controller, but it doesn’t have to be.

That wraps up the introduction of Elements. In the next four sections, you’ll take a deep dive into the four main elements: user interface, interaction responder, observer and use case.

Note: To see examples of the other elements in action, take a look at the Elements version of Koober’s Xcode project that accompanies this chapter. If you’d like to see other elements covered in a future edition of this book, let us know in the book forum.

User interface

User interface objects describe the views in your app. They allow you to configure and change what the users sees and interacts with. They usually expose methods such as showSuccessMessage() or displayWidget(). They don’t however expose the guts of the interface. For example, they don’t expose UILabels, UIButtons or UITextFields. User interface objects are meant to express what the user interface is capable of doing — the what instead of the how. The how is an implementation detail. If you do this well, you should be able to implement a completely new design by reimplementing only the user interface object.

Mechanics

This section explains how user interfaces are created and used. It’s meant to briefly explain the concepts. You’ll see code examples later on.

Instantiating

You create a user interface protocol for each view. Usually each view controller’s root view has a single user interface protocol. Concrete instances of user interfaces are initialized with references to objects that handle user interactions. These are called interaction responders. This is so the user interface can wire its controls to methods that notify the interaction responder.

Providing

User interface objects are created outside and injected into their view controllers, either through the view controller’s constructor or by setting a property. In most cases, the user interface object is the view controller’s root view. In iOS, each view controller already has a root UIView set as its view property by default. Usually you want that view to conform to a user interface protocol. So, in the view controller’s loadView() method, you set the injected user interface object to the view property.

Using

All calls to change the UI should go directly to the injected user interface object. Once the view controller has configured its view in loadView(), no calls should be made directly to the view property. The user interface protocol should expose every possible change to the view.

Injecting the user interface object into the view controller means you can mock the protocol object to write unit tests. You can verify that the correct user interface methods get called at the right times. This wouldn’t work if you set your view to a concrete instance of a UIView.

Types

User interface protocol

All user interface objects implement their own protocol describing what the view is capable of doing. The protocol should not expose implementation details about how the internals operate. It shouldn’t have any references to UIKit.

Next, check out the user interface protocol for the Sign-in view.

protocol SignInUserInterface {
  func render(newState: SignInViewState)
  func configureViewAfterLayout()
  func moveContentForDismissedKeyboard()
  func moveContent(forKeyboardFrame keyboardFrame: CGRect)
}

The protocol methods describe only what the view can do. The render(newState: SignInViewState) describes transitions between different states of the sign-in user interface. The configureViewAfterLayout() performs any extra configuration such as scrolling after the view has finished laying out its subviews.

The key takeaway here is that the user interface isn’t specific to a UIView. Any object can conform to this protocol. You want the user interface protocol to describe every possible configuration change a caller can make on the view, without exposing implementation details. You’ll see this user interface in more detail below in the example section.

One good example is a map user interface. Let’s say your company is deciding between Apple Maps and Google Maps. In six months, you could completely remove Google Maps and replace them with Apple Maps.

Instead of having the view controller hold on to a concrete instance of a Google map view, GMSMapView, or an Apple map view, MKMapView, it should hold on to a MapUserInterface that describes everything the map can do. That way, when you decide to switch internals, the view controller doesn’t need to change, only the underlying implementation of your MapUserInterface.

User interface view

In order to provide a user-interface object to a view controller on initialization, you have to create a concrete UIView object. Ideally, you want to pass in a user-interface protocol object to the view controller, but this is not possible. View controllers need a UIView to operate. As a compromise, you can create a typealias that conforms to the user-interface protocol and is also a UIView. Here’s an example:

typealias SignInUserInterfaceView = SignInUserInterface & UIView

SignInUserInterfaceView is a UIView that conforms to the SignInUserInterface protocol. Passing in a SignInUserInterfaceView to a view controller satisfies the requirement that the view controller needs a root UIView and also gives the flexibility of using a user-interface protocol.

You can also mock SignInUserInterfaceView in tests and validate the right user-interface protocol methods are called by the view controller.

Example

In this section, you’ll walk through an example so you can see how all of the user-interface pieces work in practice. The example is a simplified version of Koober’s sign-in screen.

Note: To ease readability, the example code in this section has been simplified from the code in the example Xcode project.

Koober’s sign-in screen is implemented by SignInViewController. This view controller sets its root view to a SignInUserInterfaceView, which you saw earlier.

Since the user interface view conforms to a protocol, a dependency container should inject the concrete instance into SignInViewController on initialization.

The user interface object could be a real view or mock object, so you don’t want the view controller to create that internally.

The view’s journey starts in the KooberOnboardingDependencyContainer, where SignInViewController is initialized:

class KooberOnboardingDependencyContainer {
  // ...

  func makeSignInViewController() -> SignInViewController {
    // User interface element
    // 1
    let userInterface = SignInRootView()

    // Observer element
    let statePublisher =
      makeSignInViewControllerStatePublisher()
    let observer = ObserverForSignIn(state: statePublisher)

    let signInViewController =
      SignInViewController(
        userInterface: userInterface,
        signInStateObserver: observer)

    // Wire responders
    // 2
    userInterface.ixResponder = signInViewController
    observer.eventResponder = signInViewController

    return signInViewController
  }

  // ...
}

Here are a couple highlights from the above code:

  1. The makeSignInViewController() creates the user-interface object, SignInRootView and passes it to the sign-in view controller. You’ll see the view in more detail later, but it’s a concrete instance of a UIView that conforms to SignInUserInterface. The view controller doesn’t care about which concrete type is as long as it can call SignInUserInterface methods on the object.

  2. The dependency container method also sets the ixResponder to the view controller. The main reason to do this here instead of in the view controller is that you pass in a SignInUserInterface and UIView object that doesn’t have a property setter for the responder. The view controller can’t set the responder object internally, so it has to be done at injection time on the concrete instance, SignInRootView. This responder object gets notified when the user interacts with the view. Think of it like a delegate. The view controller just needs to conform to the responder protocol and implement the responder callback methods. You’ll see more about the responder in the interaction responder section.

Creating the sign-in view controller’s dependencies in the container helps keep the initializer clean:

class SignInViewController: NiblessViewController {

  // MARK: - Properties
  let userInterface: SignInUserInterfaceView

  // ...

  // MARK: - Methods
  init(
    userInterface: SignInUserInterfaceView,
    signInStateObserver: Observer) {

    self.userInterface = userInterface
    self.signInStateObserver = signInStateObserver
    super.init()
  }

  override func loadView() {
    view = userInterface
  }

  // ...
}

The SignInViewController interacts with the SignInUserInterfaceView and can make calls to SignInUserInterface. Even though the view controller knows it’s a UIView, it doesn’t need to make calls to the UIView directly. The user interface protocol encapsulates all configuration changes to the view it needs to make.

In loadView, you set the view controller’s view property to the user-interface object. Since userInterface is a UIView subclass, this call works fine.

Have a look at the SignInUserInterface in more detail:

protocol SignInUserInterface {
  func render(newState: SignInViewState)
  func configureViewAfterLayout()
  func moveContentForDismissedKeyboard()
  func moveContent(forKeyboardFrame keyboardFrame: CGRect)
}

public struct SignInViewState: Equatable {

  // MARK: - Properties
  public internal(set) var emailInputEnabled = true
  public internal(set) var passwordInputEnabled = true
  public internal(set) var signInButtonEnabled = true
  public internal(set) 
    var signInActivityIndicatorAnimating = false

  // MARK: - Methods
  public init() {}
}

The render(newState:) method transitions between every possible state of the sign-in user interface. SignInViewState contains flags that configure the user interface:

  • The emailInputEnabled, passwordInputEnabled, and signInButtonEnabled flags determine whether the tappable elements are enabled or disabled. The inputs are enabled by default and disabled while the sign in API call is in progress.
  • The signInActivityIndicatorAnimating flag determines whether or not the activity spinner is shown. The spinner is shown after the user taps the sign in button which fires the sign in API call.

The SignInViewState is pretty simple. The key takeaway is the state struct describes every possible state of the view’s user interface.

You could, of course, also accomplish this by splitting out the state into single methods like enableEmailField() and disableEmailField().

The configureViewAfterLayout() method gives the user interface a chance to update its layout after the view has laid out its subviews. View controllers get notified when this layout completes in the viewDidLayoutSubviews() method, so they are usually the ones to call the configure method.

The other two methods, moveContentForDismissedKeyboard() and moveContent(forKeyboardFrame:) handle adjusting the layout when the keyboard is shown or hidden. These methods don’t get any information about UIKit elements, just that the content needs to move with the keyboard when the methods are called.

Now, take a look at how user-interface methods are called In the SignInViewController:

class SignInViewController: NiblessViewController {
  // ...

  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    userInterface.configureViewAfterLayout()
  }

  // ...
}

extension SignInViewController:
  ObserverForSignInEventResponder {

  func received(newViewState viewState: SignInViewState) {
    userInterface.render(newState: viewState)
  }

  func keyboardWillHide() {
    userInterface.moveContentForDismissedKeyboard()
  }

  func keyboardWillChangeFrame(keyboardEndFrame: CGRect) {
    let convertedKeyboardEndFrame = view.convert(
       keyboardEndFrame,
       from: view.window)

    userInterface.moveContent(
      forKeyboardFrame: convertedKeyboardEndFrame)
  }
}

Since the view controller is already configured to respond to the ObserverForSignInEventResponder, it just makes the necessary calls to the user interface on certain events. When the observer notifies the view controller that a new SignInViewState is ready, the view controller immediately calls userInterface.render(newState: viewState). The view controller just passes along the state from the observer, which removes a lot of logic from the view controller code.

Another thing to note is there’s no layout code in the view controller. The user interface abstracts all of its layout away from its owner and deals with updates when its notified of a change. If you need to change the way the user interface handles keyboard updates internally, you shouldn’t need to modify the view controller code.

Lastly, look at some highlights from the SignInRootView:

class SignInRootView: NiblessView {

  // MARK: - Properties
  let emailField: UITextField = {
    let field = UITextField()
    field.placeholder = "Email"
    // Other field configuration here...
    return field
  }()

  let passwordField: UITextField = {
    let field = UITextField()
    // field configuration here...
    return field
  }()

  let signInButton: UIButton = {
    let button = UIButton(type: .custom)
    // button configuration here...
    return button
  }()
}

extension SignInRootView: SignInUserInterface {

  ...

  func render(newState: SignInViewState) {
    emailField.isEnabled = newState.emailInputEnabled
    passwordField.isEnabled = newState.passwordInputEnabled
    signInButton.isEnabled = newState.signInButtonEnabled

    switch newState.signInActivityIndicatorAnimating {
    case true:
      signInActivityIndicator.startAnimating()
    case false:
      signInActivityIndicator.stopAnimating()
    }
  }

  func moveContentForDismissedKeyboard() {
    resetScrollViewContentInset()
  }

  func moveContent(forKeyboardFrame keyboardFrame: CGRect) {
    var insets = scrollView.contentInset
    insets.bottom = keyboardFrame.height
    scrollView.contentInset = insets
  }
}

The view contains all the UIKit elements needed to render the sign-in screen. The emailField and passwordField are UITextFields and signInButton is a UIButton object, all laid out by this view.

When the render(newState:) method gets called, the isEnabled fields are updated on each of the elements based on state. The activity indicator also starts or stops spinning.

The keyboard user-interface methods adjust the scroll view’s content inset based on whether the keyboard is displayed or dismissed. This allows the input fields to stay visible and not hide under the keyboard.

The SignInRootView code should be pretty familiar since it’s just a normal UIView. The big difference is it must conform to the SignInUserInterface and handle those methods properly. That’s it for the user-interface section. Next, you’ll see how the interaction responder works.

Interaction responder

The interaction responder handles user interactions. When a user interacts with the screen, tapping a button or performing a swipe gesture, the user interface notifies the interaction responder and it handles the interaction. User-interface objects only know when the user performs an interaction. They don’t actually know how to handle the interaction. They’re dumb objects by design.

The user interface tells the interaction responder what to do, not what happened. This removes any user interface terms from the interaction responder.

For example, the responder might expose a method that says createPost() instead of createPostButtonTapped(). If you swap out the user interface element to sign in for something other than a button, the interaction responder protocol stays the same.

Usually, a view controller is the user interface’s interaction responder, but you can create a new object that handles user interactions and inject it into the view controller. Either way works. The standalone object is easier to test since you don’t have to create a view controller. The view controller approach requires less code since you can just make the view controller conform to the responder protocol. Koober uses the view controller approach.

Mechanics

This section explains how interaction responders are created and used. It’s meant to briefly explain the concepts. You’ll see code examples further down.

Instantiating

You create an interaction-responder protocol for each user interface. Interaction responders are then provided as a reference to the user interface. Since Koober view controllers are the interaction responders, they get created in a dependency container.

Providing

User-interface objects have an interaction responder property that gets set after initialization. This is so the user interface can wire its controls to methods that notify the interaction responder on user interactions.

The view controller that conforms to the interaction responder protocol gets created with a user interface object. After the view controller is initialized, it gets set as the user interface’s interaction responder.

Using

The user interface makes calls directly to its interaction responder. The view controller just needs to implement the right interaction responder methods.

Creating an interaction responder

Error: This image is missing a width attribute

Please provide one in the form of ![width=50%](images/Theory/InteractionHandlerProviding_bw.png)

The image has been hidden until this issue is resolved.

The above diagram shows how the view controller gets initialized with a user interface and set as the interaction responder.

  1. First, the dependency container creates the view controller and injects the user interface object.
  2. Then, the dependency container sets the view controller as the user interface’s interaction responder.
  3. From then on, all user interactions are handled by the view controller, which conforms to the interaction responder protocol.

Types

Interaction responder protocol

Each view has its own interaction-responder protocol. Any interaction the user can perform on the view is described in the protocol. It doesn’t expose any details about how the internals are implemented. The protocol should have no references to UIKit and nothing about what kind of user interface element triggered the user interaction.

Take a look at the sign-in view’s interaction responder:

typealias Secret = String

protocol SignInIxResponder: AnyObject {
  func signIn(email: String, password: Secret)
}

The sign-in responder protocol is simple and contains a single method to sign in the user. The only user interaction a user can perform in the sign-in screen is tapping the Sign in button after entering an email and password.

There are two main takeaways from this protocol:

  1. The responder doesn’t expose any details about how the sign in was initiated, such as tapping a sign-in button or tapping the next button on the keyboard. If you redesign the user interface you shouldn’t have to update the interaction responder protocol.
  2. The user interface tells the interaction responder what to do, not what happened. The method isn’t signInButtonTapped(), but rather signIn(), telling the responder what action to perform next. The interaction responder doesn’t care about what happened. It’s the user interface’s responsibility to convert button taps and gestures into an actionable item for the interaction responder.

Example

In this section, you’ll walk through an example so you can see how the interaction responder is used in practice. The example is from Koober’s sign-in screen.

Note: To ease readability, the example code in this section has been simplified from the code in the example Xcode project.

The SignInViewController is the interaction responder for its SignInUserInterfaceView, which you saw in the user interface section. When the user interacts with the SignInUserInterfaceView, the user interface makes a call to the interaction responder.

When the SignInViewController gets created in the KooberOnboardingDependencyContainer, it’s set as the user interface’s interaction responder. You saw the initialization in the user interface section, but let’s go over it once more as a refresher:

class KooberOnboardingDependencyContainer {
  // ...

  func makeSignInViewController() -> SignInViewController {
    // User interface element
    let userInterface = SignInRootView()

    // Observer element
    let statePublisher =
      makeSignInViewControllerStatePublisher()
    let observer = ObserverForSignIn(state: statePublisher)

    // Use case element
    let signInUseCase = makeSignInUseCase()

    let signInViewController =
      SignInViewController(
        userInterface: userInterface,
        signInStateObserver: observer,
        // 1
        signInUseCase: signInUseCase)

    // Wire responders
    // 2
    userInterface.ixResponder = signInViewController
    observer.eventResponder = signInViewController

    return signInViewController
  }

  // ...
}

A couple highlights from this code:

  1. The only interaction responder method has is to sign in the user. The sign-in use case actually performs this work. It’s a dependency of the SignInViewController, so the interaction responder method can run the use case when the user is ready to sign in.

  2. The user interface is the object that makes calls to the interaction responder. Since the sign-in view controller gets set as the user interaction responder in the dependency creation code, the view controller only has to implement the protocol methods.

    Also, since SignInViewController only takes in a SignInUserInterfaceView, which is a SignInUserInterface and a UIView, you have to set the interaction responder on the concrete instance, SignInRootView. Only the dependency creation code knows about the concrete type, so the dependency code needs to configure the responder.

The user interface wires its components to methods that call the interaction responder. In the SignInRootView, a sign in UIButton tap makes the call:

class SignInRootView: NiblessView, SignInUserInterface {

  // MARK: - Properties
  weak var ixResponder: SignInIxResponder?

  // ...

  override func didMoveToWindow() {
    super.didMoveToWindow()
    wireController()
    // other set up goes here ...
  }

  func wireController() {
    signInButton.addTarget(
      self,
      action: #selector(signIn),
      for: .touchUpInside)
  }

  @objc
  func signIn() {
    ixResponder?.signIn(
      email: emailField.text ?? "",
      password: passwordField.text ?? "")
  }

  // ...
}

The root view configures the target / action pairs as soon as it moves to the window. You could alternatively do this when the view is initialized.

The signInButton is configured to call the signIn() method whenever it’s tapped. The signIn() method calls the interaction responder’s signIn() method with the current email and password input. This is fairly straightforward code.

The key here is that the UIButton target / action can be switched to a tap gesture recognizer on another component and the interaction responder doesn’t need to change.

Take a look at the interaction responder implementation:

extension SignInViewController: SignInIxResponder {

  func signIn(email: String, password: Secret) {
    let useCase = signInUseCase()
    useCase.start()
  }
}

Normally, the interaction responder methods run a use case. That’s it. The bulk of the work to make API calls and change the state of the user interface is handled inside the use case.

The interaction responder implementations are only responsible for knowing which use cases to run and running them. This may seem overly simple, but you want to remove as much code from the view controller as possible. You’ll get a better understanding of what happens after start() is called on the use case in the use case section.

That’s it for the interaction responder! There’s still a lot of ground to cover with the next two elements. In Part 2, you’ll see how to implement Observer and Use Case elements. Before jumping into Part 2, feel free to take a break, stretch and grab a coffee or your drink of choice.

Key points

  • Elements is an architecture meant to make iOS development fun and flexible. A set of “Elements” make up the architecture. The cool thing is you can choose which pieces to use in your own applications.
  • Elements is designed on top of some core underlying concepts: entities allow objects to communicate, protocols make software flexible and encapsulation enables safe change.
  • Elements are separated into two main categories: Core Logic and User Interface Logic.
  • This edition of this book dives deep into the four main elements: user interface, interaction responder, observer and use case.
  • User-interface objects describe the views in your app. They allow you to configure and change what the users sees and interacts with.
  • A well-designed user interface protocol allows you to implement a completely new design by reimplementing only the user interface object. No view controller changes necessary.
  • Interaction responders handle user interactions. When a user interacts with the screen the user interface notifies the interaction responder.
  • Interaction responders allow you to safely change a view hierarchy without needing to change any view controller code. This is because interaction responders express what a user can do with a user interface as opposed to what specific view triggers what task.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.