Skip to content
/ Form Public

Form is an iOS Swift library for building and styling UIs

License

Notifications You must be signed in to change notification settings

iZettle/Form

Repository files navigation

Build Status Platforms Carthage Compatible Xcode version

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.

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:

Messages and compose views using system styling

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:

Messages and compose views using custom styling

Where the form shown above is built using stack views, Form also provides helpers to populate UITableViews 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 10.0+
  • Swift 5
  • iOS 12.0+

Installation

github "iZettle/Form" >= 4.0
platform :ios, '9.0'
use_frameworks!

target 'Your App Target' do
  pod 'FormFramework', '~> 4.0'
end

Introductions

  • 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.

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")

Presentation framework

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.

Field tested

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.

Collaborate

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]