Chapters

Hide chapters

Advanced iOS App Architecture

Third Edition · iOS 13 · Swift 5.2 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section I

Section 1: 10 chapters
Show chapters Hide chapters

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

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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.

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.

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.

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.

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.

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

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

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.

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
  }

  // ...
}
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
  }

  // ...
}
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() {}
}
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)
  }
}
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
  }
}

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.

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.

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

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.

typealias Secret = String

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

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.

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
  }

  // ...
}
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 ?? "")
  }

  // ...
}
extension SignInViewController: SignInIxResponder {

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

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now