Swiftpack.co - Package - iZettle/Form

Build Status Platforms Carthage Compatible

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 9.3+
  • Swift 5
  • iOS 9.0+

Installation

Carthage

github "iZettle/Form" >= 3.0

Cocoa Pods

platform :ios, '9.0'
use_frameworks!

target 'Your App Target' do
  pod 'FormFramework', '~> 3.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 ios-oss@izettle.com

Github

link
Stars: 94

Dependencies

Used By

Total: 0

Releases

Form 3.0.2 - 2020-04-29 09:05:17

  • Bugfix: Prevent indefinite looping when pinning views to both the top and bottom of a scroll view.

Form 3.0.1 - 2020-04-14 11:16:14

3.0.1

  • Change: ValueField: clear the .shouldResetOnInsertion flag when deleting characters (iZettle/Form#152).

Form 3.0.0 - 2020-04-03 10:54:41

  • Added: .shouldResetOnInsertion to ValueField.
  • Added: reset(), defaultValue, and .shouldResetOnInsertion to the TextEditor protocol (breaking change).
  • Changed: Removed deprecated symbols.

Form 2.2.0 - 2020-04-02 13:56:46

  • Added: Swift 5.2 support
  • Bugfix: Remove circular reference from ParentChildRelational

Form v2.1.0 - 2019-12-28 09:05:05

[Added] Optional adjustsFontForContentSizeCategory property to specify on a TextStyle

Form v2.0.6 - 2019-12-03 17:36:44

  • Bugfix: Prevent corner case TableKit crash when the table is updated while reloading the view

Form v2.0.5 - 2019-12-02 15:24:28

  • Bugfix: Prevent TableKit scrolling on row selection when no rows are visible (see #145).

Form v2.0.4 - 2019-10-28 12:47:12

  • Bugfix: Re-apply form style to table cell backgrounds when reusing them and reloading data

Form v2.0.3 - 2019-10-21 07:50:04

  • Improvement: Improved size class handling to better support iPad multitasking, removed all hard references to device type.
  • Tech: Moved CI build and test completely to CircleCI config.yml

Form v2.0.2 - 2019-10-01 08:35:49

  • Bugfix: Fix issue with scrollToRevealFirstResponder (#122)

Form v2.0.1 - 2019-09-26 12:41:31

  • Bugfix: Fixed a bug where ButtonStateStyle.init was wrongfully failable, which could lead to buttons not receiving their text style.

Form v2.0 - 2019-09-25 14:16:29

  • Added: Support for Swift Package Manager
  • Added: New initializers for RowsSelection that works with the latest version of Presentation framework. The old one is deprecated.
  • Change: Bump the minimum required version of Presentation to 1.9.0
  • Change: Removed deprecated functions, symbols etc.
  • Change: Deprecate registerViewForSupplementaryElement and add a new one that works with UICollectionView headers and footers

Upgrade migration guide

  • [Removed] UIImage.init(border:bottomSeparator:topSeparator:background:position:) is removed, use SegmentBackgroundStyle(…).image().
  • [Removed] EitherRow has been removed. Use Either directly in the future.
  • [Removed] ScrollViewDelegate.willEndDraggingWithVelocity. Use ScrollViewDelegate.willEndDragging.
  • [Removed] UIScrollView.disableScrollingIfContentFits(). Use UIScrollView.alwaysBounceVertical = false instead.
  • [Removed] TextStyle.kerning. Use UIScreen.letterSpacing.
  • [Removed] UIScreen.thinestLineWidth. Use UIScreen.hairlineWidth.
  • [Removed] UITraitCollection.thinestLineWidth. Use UITraitCollection.hairlineWidth.
  • [Removed] CGFloat.thinestLineWidth. Use CGFloat.hairlineWidth.
  • CollectionKit
    • [Removed] UICollectionView.dequeueSupplentaryView(at:for). Use UICollectionView.dequeueSupplementaryView(forItem:kind:at:).
    • [Renamed] init(table:layout:bag:cellForRow:)init(table:layout:holdIn:cellForRow:)
    • [Renamed] init(table:layout:bag:)init(table:layout:holdIn:)
  • TableKit
    • [Removed] TableKit.viewForEmptyTable. Use TableKit.viewForEmptyTable(fadeDuration:)
    • [Removed] UITableView.dequeueCell(forItem:style:reuseIdentifier). Use UITableView.dequeueCell(forItem:style)
    • [Renamed] init(table:style:view:bag:headerForSection:footerForSection:cellForRow:)init(table:style:view:holdIn:headerForSection:footerForSection:cellForRow:)
    • [Renamed] init(table:style:view:bag:headerForSection:footerForSection:)init(table:style:view:holdIn:headerForSection:footerForSection:)
    • [Renamed] init(table:style:view:bag:footerForSection:)init(table:style:view:holdIn:footerForSection:)
    • [Renamed] init(table:style:view:bag)init(table:style:view:holdIn)

Form v1.10.5 - 2019-09-09 14:07:34

  • Bugfix: Fixed scroll-to-top not working correctly.
  • Addition: Add new initializers for TableKit and CollectionKit that have an explicit holdIn parameter to keep subscriptions.
  • Change: Change the deprecation warning of TableKit and CollectionKit initializers to point to the new ones

Form v1.10.3 - 2019-08-15 15:03:58

  • Bugfix: Fix table section header/footer height calculation on iOS 10

Form v1.10.2 - 2019-08-12 12:31:46

  • [Bug fix] Remove wrong scrollview top pinning constant on iOS 11 causing a gap for pinned views in navigation controller

Form v1.10.1 - 2019-07-25 11:00:07

  • Bugfix: Apply view styling based on initial trait collection to prevent bugs where styling is not applied if the initial trait collection did not change

Form v1.10.0 - 2019-07-25 10:59:47

  • TableKit and CollectionKit do no longer require the passing of a bag to their initializers. This means that the life-time of a kit instance is no longer kept alive by a provided bag. For most usages that should not change the behaviour but if the kit is prematurely deallocted you can always explicity hold on to it bag.hold(kit).
  • TableKit's and CollectionKit's initializers taking a bag parameter have been deprecated. Instead use the new initializers introduced above.

Form v1.9.1 - 2019-05-16 10:08:19

  • Bugfix: fix pinning a view to a scrollview on iOS 9 and 10 (issue #104)
  • Layout fix: properly layout multiline row view titles

Form v1.9.0 - 2019-04-16 11:04:37

  • Bugfix: Make sure to remove the old empty state view from a table after setting a new empty state view #99.
  • Add minimum scale factor to TextStyle. When a custom value is set that can also affect other controls using TextStyle, e.g UIButton.

Form v1.8.0 - 2019-04-04 12:16:53

  • Migrate to Swift 5

Form v1.7.1 - 2019-03-20 15:30:37

  • Bugfix: Fix UILabel styling bug when a styled label's text is set to nil and then updating its value does nothing.

Form v1.7.0 - 2019-03-20 15:20:21

  • Adds new HeaderFooterReusable protocol to allow providing separate Reusable types for rendering a section's header and footer.
  • Adds letter spacing and line height to TextStyle.
  • Adds target offset to willEndDragging signal of ScrollViewDelegate.
  • Adds will display cell signal to CollectionViewDelegate.

Form v1.6.3 - 2019-02-25 14:05:14

  • Performance. Added custom count implementation for TableSection to improve performance of e.g. Table.isValidIndex that might be called a lot for large tables.

Form v1.6.2 - 2019-02-22 09:15:12

  • Bugfix: Setting table directly on TableKit or CollectionKit did not reload the view correctly with the updated table.

Form v1.6.1 - 2019-01-29 17:42:35

  • Bugfix: Fix layout problem caused by pinning a view to UITransitionView that is no longer shown on iOS 9/10
  • Bugfix: Activate constraints before calls to layoutIfNeeded to prevent crashes on iOS 9/10 when embedding views in a scrollView

Form v1.6.0 - 2019-01-22 12:01:46

  • Add sizeForItemAt for CollectionViewDelegate
  • Bugfix: Fix table view cells reorder control position to respect insets

Form v1.5.0 - 2018-11-21 10:16:59

  • Add optional preferred minimum size to ButtonStyle

Form v1.4.1 - 2018-11-09 13:03:43

  • Fixes related to UITableViewCell styling.
  • Revert a change that caused bug in Form insets.

Form v1.4.0 - 2018-10-27 09:47:02

  • Added TextFieldDelegate similar to ScrollViewDelegate and friends.
  • Added UITextField and UIScrollView install() delegate helpers.
  • Added remaining methods to ScrollViewDelegete.
  • Added GestureRecognizerDelegate.
  • Fixed a bug in UIScrollView.adjustContentOffset for scrollviews with a non-zero frame.origin.y.

Form v1.3.3 - 2018-10-16 16:18:19

  • Bugfix: Remove hardcoded value used for determing smallest resizable image size causing not smooth borders of rounded images
  • Bugfix: Fix a bug where highligted segment turns gray if it's also selected
  • Bugfix: Add extra space to the resizable image rect to prevent the border taking the whole image