An iOS Swift library for building and styling UIs
Form
Form is an iOS Swift library for building and styling UIs. A toolbox of highly composable utilities for solving common UI related problems.
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.
Example usage
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.
Requirements
- Xcode
9.3+
- Swift 4.1
- iOS
9.0+
Installation
Carthage
github "iZettle/Form" >= 1.0
Cocoa Pods
platform :ios, '9.0'
use_frameworks!
target 'Your App Target' do
pod 'FormFramework', '~> 1.0'
end
Introductions
- 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.
Localization
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")