Introducing Form — Layout, styling, and event handling

Building UIs for iOS applications means to layout, style and react to events. Even though UIKit provides most of what we need, there are still areas where the experience can be improved. For example, the indirection enforced by the pervasive use of delegates and selectors forces your code to be split apart. It would also be nice if we could have access to more of Swift’s expressive power such as value types and generics when working with UIKit.

With the introduction of the Flow framework we presented a fundamental programming model for working with UI events. We followed up with the Presentation framework for a more formalized way of presenting view controllers from model to result. Form, introduced in this article, concludes by focusing on the content inside our view controllers.

To showcase the main ideas behind the Form framework we will build a simple messages application based on a Message model:

struct Message: Hashable {
var title: String
var body: String
}

The application will consist of a view listing our messages and a view for composing new messages:

Messages and compose views using system styling

Building table-like UIs

We start out by building a form for composing new messages. We want our form to be styled and laid out as a table view. Table-like layouts are common in iOS applications, such as iOS’s general settings. However, building forms using table views can be tricky because of the indirection enforced by data sources, delegates, and cells. If we on top of that introduce some dynamism to our table’s content, this can result in some serious juggling of section and row indices.

Form provides UI components that with the help of Flow, removes this indirection by building tables using stack views laid out and styled as table views.

In our compose message example below, we extend UIViewController with a presentComposeMessage() method that adds two fields for editing the title and body of a message. The method also adds navigation items for canceling and saving, that when tapped will complete the future returned from the method to notify that we are done:

extension UIViewController {
func presentComposeMessage() -> Future<Message> {
self.displayableTitle = "Compose Message"

let form = FormView()
let section = form.appendSection()

let title = section.appendRow(title: "Title").append(UITextField(placeholder: "title"))
let body = section.appendRow(title: "Body").append(UITextField(placeholder: "body"))

let isValid = combineLatest(title, body).map {
!$0.isEmpty && !$1.isEmpty
}

let save = navigationItem.addItem(UIBarButtonItem(system: .save), position: .right)
let cancel = navigationItem.addItem(UIBarButtonItem(system: .cancel), position: .left)

return Future { completion in
let bag = DisposeBag()

bag += isValid.atOnce().bindTo(save, \.enabled)

bag += save.onValue {
let message = Message(title: title.value, body: body.value)
completion(.success(message))
}

bag += cancel.onValue {
completion(.failure(CancelError()))
}

bag += self.install(form) { scrollView in
bag += scrollView.chainAllControlResponders(shouldLoop: true, returnKey: .next)
title.provider.becomeFirstResponder()
}

return bag
}
}
}

When creating the fields and bar items above we use convenience initializers that take a style parameter with a default value. The defaults used for these initializers can be globally overridden such as shown in the following screenshot:

Messages and compose views using custom styling

However, you can create your own styles and explicitly pass them when constructing your UI components:

extension TextStyle {
static let header = TextStyle(font: ...)
}

let header = UILabel(value: "Welcome", style: .header)

Populate table views

While you can use forms (introduced above) for building tables with uniform row types, such as:

let messages: [Message]
for message in messages {
section.appendRow(title: message.title, subtile: message.body)
}

this would not be efficient for large tables. To solve this, Form provides utilities for rendering these using UITableViews instead. This is showcased in the view controller presenting the messages list presented below. In this example, we pass the messages as a signal. That way we can listen for changes and update the UI when the messages list is updated, for example after composing a new message:

extension UIViewController {
// Returns a `Disposable` to keep activities alive while being presented.
func present(messages: ReadSignal<[Message]>) -> Disposable {
displayableTitle = "Messages"
let bag = DisposeBag()

let tableKit = TableKit<EmptySection, Message>(bag: bag)

bag += messages.atOnce().onValue {
tableKit.set(Table(rows: $0))
}

bag += install(tableKit)

return bag
}
}

To be able to present our Message model in a table cell, we need to describe how to create views from it and how to reuse them. This is handled by conforming your model to the Reusable protocol:

extension Message: Reusable {
static func makeAndConfigure() -> (make: RowView, configure: (Message) -> Disposable) {
let row = RowView(title: "", subtitle: "")
return (row, { message in
row.title = message.title
row.subtitle = message.body
// Returns a `Disposable` to keep activities alive while being presented.
return NilDisposer() // No activities.
})
}
}

Both the messages’ and compose view’s tables, even though implemented differently, are styled using the same styles and use the same defaults.

Bring it all together

To complete our sample app, we will show how we can add a compose button to the messages view to present the compose view and then update our messages model with the newly composed message:

let bag = DisposeBag()
let messages = ReadWriteSignal([Message(...), ...])
let messagesController = UIViewController()

// Handle presentation of messagesController

bag += messagesController.present(messages: messages.readOnly())

bag += messagesController.navigationItem
.addItem(UIBarButtonItem(system: .compose), position: .right)
.onValue {
let composeController = UIViewController()
composeController.presentComposeMessage().onValue { message in
messages.value.insert(message, at: 0)
// Handle dismissal of composeController
}

// Handle presentation of composeController
}

Summary

If you have read the Introducing Presentation article you will recognize that it builds on the same messages example shown above. However, in that article, we focused on the presentation of view controllers and the data going in and out, whereas in this article we have focused on what goes into a view controllers view.

Even though Presentation and Form can be used independently, they were evolved and designed to work together. At iZettle, we use both frameworks throughout the iZettle app with the benefit of having a more consistent and robust code-base that is easier to reason and more fun to maintain.

In this article, we have only showcased a small part of what Form has to offer. If you want to learn more we recommend that have a look at GitHub.