Form is an iOS Swift library for building and styling UIs. A toolbox of highly composable utilities for solving common UI related problems, such as:
- Forms - Building table like UIs with mixed row types.
- Tables - Populate tables and collection views.
- Layout - Laying out and updating view hierarchies.
- Styling - Styling of UI components.
- Keyboard - Adjusting for keyboards.
- Values - Displaying and edit custom types.
Even though Form is flexible, it is also opinionated and has a preferred way of building UIs:
- Build and layout UIs programmatically.
- Use reactive programming for event handling.
- Promote small reusable components and extensions to subclassing.
- Prefer being explicit and declarative using value types.
The Form framework builds heavily upon the Flow framework to handle event handling and lifetime management.
To showcase the main ideas behind Form 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:
Form makes it easy to build form like interfaces that are styled and laid out as table views that are so common in iOS applications:
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
}
}
}
Form extends several UI components with initializers accepting a style parameter that often has a default that can be globally overridden by your app:
Where the form shown above is built using stack views, Form also provides helpers to populate UITableView
s for improved performance when you have larger or dynamic tables:
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.
})
}
}
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
}
}
Both forms and tables are using the same styling allowing you to seamlessly intermix tables and forms to get the benefit of both.
- Xcode
9.3+
- Swift 5
- iOS
9.0+
github "iZettle/Form" >= 3.0
platform :ios, '9.0'
use_frameworks!
target 'Your App Target' do
pod 'FormFramework', '~> 3.0'
end
- Forms - Building table like UIs with mixed row types.
- Tables - Populate table and collection views with your model types.
- Layout - Work with layouts and view hierarchies.
- Styling - Create custom UI styles.
- Keyboard - Adjust your UI for keyboards.
- Values - Display and edit custom types.
Most of Form's APIs for working with end-user displayable texts accept values conforming to DisplayableString
instead of a plain string. You can still use plain strings when using these APIs as String
already conforms to DisplayableString
. However, if your app is localized, we highly recommend implementing your own type for localized strings, for example like:
struct Localized: DisplayableString {
var key: String
var displayValue: String { return translate(key) }
}
let label = UILabel(value: Localized("InfoKey"))
Or if you prefer to be more concise:
prefix operator §
prefix func §(key: String) -> Localized {
return Localized(key: key)
}
let label = UILabel(value: §"InfoKey")
We highly recommend that you also check out the Presentation framework. Form and Presentation were developed closely together and share many of the same underlying design philosophies.
Form was developed, evolved and field-tested over the course of several years, and is pervasively used in iZettle's highly acclaimed point of sales app.
You can collaborate with us on our Slack workspace. Ask questions, share ideas or maybe just participate in ongoing discussions. To get an invitation, write to us at [email protected]