Introducing Presentation — Formalizing presentations from model to result

View controllers play a central role when building iOS applications. However, building, managing and presenting view controllers is not always straightforward. There are many questions that need to be answered, and many alternatives to consider:

  • How is model data passed to a controller?
  • How is a result passed back to the caller?
  • When and where to perform setup and cleanup?
  • Where to put our business logic?
  • How to test our UI?
  • Should we use storyboards or build our UI programmatically?
  • Should we use IBOutlets and delegates, or reactive programming?
  • How to present other controllers?
  • Where to place common code shared between controllers?

The Presentation library for Swift and iOS aims to improve the situation by:

  • Formalizing the driving of presentations from model to result.
  • Introducing tools and patterns for improving separation of concerns.
  • Providing conveniences for presenting view controllers and managing of their lifetime.

Even though Presentation is flexible it is also opinionated and has a preferred way of building presentations:

  • Be explicit about model data and results.
  • Build and layout UIs programmatically.
  • Use reactive programming for event handling.
  • Prefer small reusable components and extensions to subclassing.

The Presentation framework builds heavily upon the Flow framework to handle event handling, asynchronous flows, and lifetime management. For an introduction to Flow, we recommend reading the Introducing Flow article.

To showcase the main ideas behind Presentation we will build a simple messages application.

Listing messages

The main UI of our sample app will be a listing of messages described by the model:

struct Messages {
let messages: ReadSignal<[Message]>
}

The Messages model describes the data required to present the messages UI. Here we use a basic value type Message to describe a message. As messages can be added and removed, for example, if being refreshed from a backend service or by our own UI, it will be modeled using a Signal. A signal is an abstraction for observing events over time that is part of the Flow library.

Given an instance of Messages, we want to materialize this into something we can present. This is formalized by the Presentable protocol:

public protocol Presentable {
associatedtype Matter
associatedtype Result

func materialize() -> (Matter, Result)
}

For a view controller presentation, the type of Matter is a UIViewController or a sub-class of thereof. For our Messages, the Result type will be a Disposable to indicate that there is no explicit result. Disposable abstracts lifetime management and is used to keep observers and other activities with a lifetime alive while being presented. Once the presentation is done and dismissed the returned disposable will be disposed to end those activities.

We conform Messages to Presentable by implementing materialize(). For our messages UI we need to set up a table view together with its data source and then update the table based on the messages signal:

extension Messages: Presentable {
func materialize() -> (UIViewController, Disposable) {
// Construct viewController and setup data source...
let viewController = UITableViewController()
let dataSource = ...

// Setup event handling
let bag = DisposeBag()

// Update source and table when messages is updated
bag += messages.atOnce().onValue { messages in
dataSource.messages = messages
viewController.tableView.reloadData()
}

return (viewController, bag)
}
}

Conforming Messages to Presentable not only provides a formalized way to build presentations but also provides conveniences to present those:

let messages = Messages(...)
presentingViewController.present(messages)

Composing a message

Let us continue by extending our app to allow the composing of new messages:

struct ComposeMessage { }

Our ComposeMessage presentable does not need any model data. But in comparison to Messages, it will have a result, the composed message. As this result will be produced asynchronously it will be returned as a Future<Message>. The Future type represents a result that might not yet be available and is also part of Flow.

A ComposeMessage is materialized by constructing a plain view controller and setting its view. We also set up button observers to complete the returned future when tapping send or cancel:

extension ComposeMessage: Presentable {
func materialize() -> (UIViewController, Future<Message>) {
// Setup view controller and views
let viewController = UIViewController()
let cancelButton = UIBarButtonItem(...)
let postButton = UIBarButtonItem(...)
viewController.view = ...

return (viewController, Future { completion in
// Setup event handling
let bag = DisposeBag()

bag += cancelButton.onValue {
completion(.failure(PresentError.dismissed))
}

bag += postButton.onValue {
let message = Message(...) // Construct from text fields
completion(.success(message))
}

return bag
})
}
}

When present() is being called with a presentable returning a future, it will in its turn also return a future that will complete with the presentable's result once the presentation is done:

let compose = ComposeMessage(...)
presentingViewController.present(compose).onValue { message in
// Called with a composed message on dismiss
}

We can now extend Messages to provide a Presentation of a ComposeMessage.

struct Messages {
let messages: ReadSignal<[Message]>
let composeMessage: Presentation<ComposeMessage>
}

By adding a ComposeMessage to the model, we relieve the Messages presentable from the need to know how to construct a ComposeMessage. This is a great way to decouple the two UIs and removes the need to forward any model data needed to construct and present other presentations.

Moreover, by also wrapping the composeMessage in a Presentation we can remove details how to present it. The Presentation wrapper is basically a presentable bundled together with presentation parameters such the style of presentation to use (modal, popover, etc.).

Let us extend Messages with a compose button that will open the compose view by extending its materialize with:

let composeButton = UIBarButtonItem(...)

bag += composeButton.onValue {
viewController.present(self.composeMessage)
}

Initialize our presentables

We have shown how to define and materialize our presentables, but not how to initialize them. It is important to realize that the data needed to initialize a presentable is not necessary data that the presentable itself need to know about. To make this decoupling of initialization and presentation more explicit, it could be useful to separate these into separate files, and potentially separate modules as well. The initializer could, for example, be given access to resources not available from the presentable’s own module.

So let us wrap up our example by writing an initializer for Messages:

extension Messages {
init(messages: [Message]) {
let messagesSignal = ReadWriteSignal(messages)
self.messages = messagesSignal.readOnly()

let compose = Presentation(ComposeMessage(), style: .modal)
self.composeMessage = compose.onValue { message in
messagesSignal.value.insert(message, at: 0)
}
}
}

Here we can see that Presentation also allows adding actions and transformations to be called upon presentation and dismissal. In this case, we will set up an action to be called once the compose presentation is being successfully dismissed with the newly composed message. The new message will just be prepended to our messagesSignal that will signal our Messages presentable to update its table view.

Note that the above initialization of Messages is just one of many potential implementations. In a more realistic example, we might include network requests and some kind of persistence store such as CoreData. But however we chose to implement this, it will not affect our presentable type and its materialize implementation. We might even have several initializers for different purposes such as production, unit-testing and sample apps.

A useful way to view a presentable is as a recipe on how to present something. Having an instance of a presentable does not mean that any UI has yet been constructed. The user might never choose to compose a message, or might compose more than one. But we still just have one instance of the compose presentable.

Summary

In this article, we demonstrated how we can formalize the presentation of view controllers and how the Presentation framework can help us out. We saw how we decoupled different presentables from each other as well as how we decoupled the presentation from the initialization of these. By being more formal and explicit about what is needed to present some UI and what the result of that presentation will be, our code will be easier to reason about, more testable, and easier to maintain. Presentation also makes the presentation and dismissals of view controller easier to work with and removes many issues related to view controller subclassing and handling of their lifetimes.

We hope that this article introduced you to new ways to think about view controllers and their presentation. By applying these ideas throughout our iZettle app, we not only have a more robust app, but its UI code is now more consistent and exposed for unit testing. If you want to learn more about Presentation and the ideas introduced in this article we recommend that have a look at GitHub.