Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Måns Bernhardt committed Jun 29, 2018
0 parents commit 4288b0f
Show file tree
Hide file tree
Showing 118 changed files with 13,370 additions and 0 deletions.
15 changes: 15 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
language: objective-c
osx_image: xcode9.3
env:
global:
- LC_CTYPE=en_US.UTF-8
- LANG=en_US.UTF-8
matrix:
- COMMAND="test-iOS"
- COMMAND="examples"
script:
- set -o pipefail
- xcodebuild -version
- xcodebuild -showsdks
- swift -version
- sh build.sh "$COMMAND"
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# 1.0

This is the first public release of the Form library.
1 change: 1 addition & 0 deletions Cartfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github "iZettle/Flow" ~> 1.1
169 changes: 169 additions & 0 deletions Documentation/Forms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Forms - Building table like UIs with mixed row types

It is quite common with UIs laid out and styled as table views. But these tables are sometimes using rows with mixed types. A good example is iOS's general settings. Building tables with mixed row types are often hard to get right, especially if the rows being displayed might differ based on some configuration. If you ever attempted to maintain similar UIs, especially when backed by a `UITableView`, you are probably aware of the difficulties involved.

To mitigate this, Form provides three helper views; `FormView`, `SectionView` and `RowView` for building table like UIs. These views are backed by `UIStackView`s, and laid out and styled to look like `UITableView`s. They were designed for convenience and are best suited for smaller tables. For more performant tables, Form provides [`TableKit`](./Tables.md) for working with `UITableView`s and reusable rows.

Building forms using `FormView`, `SectionView` and `RowView` is straightforward:

```swift
let form = FormView()
let section = form.appendSection(header: "About")
let row = section.appendRow(title: "Credits")
bag += row.onValue { /* show credits */ }
```

Here we can see that we can build our UI more declaratively and directly. This is in sharp contrast to using table views where you have an indirection using indices, data sources and cells.

As you build you table using code it is also simple to make them dynamic based on some configuration parameters:

```swift
if hasFeature {
let section = form.appendSection(header: "Feature")
if hasSubFeature {
let row = section.appendRow(title: "Sub feature")
}
}
```

To build this using table view's indirection would require a delicate juggling of section and row indices.

## FormView

At the root of a form is the `FormView` that holds vertically laid out section views:

```swift
let form = FormView()
let section = SectionView(header: ..., footer: ...)
form.append(section)
```

As adding sections to a form is so common there are convenience helpers to write this more succinctly:

```swift
let section = form.appendSection(header: ..., footer: ...)
```

But it is worth pointing out that you can append any view to a form, not only section views, making it easier to build custom UI:

```swift
form.append(customView)
```

## SectionView

A `SectionView` holds an array of vertically laid out row views, optionally starting with header and ending with a footer.

Similar to `FormView` you can add any view to a section:

```swift
let section = form.appendSection()
section.append(customView)
```

But more commonly, you add row views instead:

```swift
let row = RowView(title: ...)
section.append(row)
```

Or more succinctly:

```swift
let row = form.appendRow(title: ...)
```

By using `RowView`s we also ensure the layout is updated to use the provided `SectionStyle`'s `rowInsets` and `itemSpacing`:

```swift
let style = SectionStyle.default.restyle { style in
style.rowInset.left = 40
style.itemSpacing = 20
}

let section = form.appendSection(style: style)
```

## RowView

A `RowView` holds an array of horizontally laid out views. You typically build a row starting out with a title (and optionally subtitle) and then appends (or prepends) more views to it:

```swift
let row = RowView(title: "title", subtitle: "subtitle")
.prepend(iconImage)
.append("details")
.append(.chevron)

section.append(row)
```

Or more conveniently:

```swift
let row = section.appendRow(title: "title", subtitle: "subtitle")
.prepend(iconImage)
.append("details")
.append(.chevron)
```

## RowAndProvider

When adding a `RowView` to a section it returns a `RowAndProvider` holding both the row view and a `Signal<()>` for observing selections of the row:

```swift
bag += section.appendRow(title: "title") // -> RowAndProvider<Signal<()>>
.onValue { /* row tapped */ }
```

A `RowAndProvider` behaves much like a standalone `RowView`, and you can continue appending and prepending views to its row:

```swift
let row = section.appendRow(title: "title") // - RowAndProvider
.prepend(iconImage) // - RowAndProvider
.append("details") // - RowAndProvider
.onValue { /* row tapped */ }
```

But as seen above `RowAndProvider` also takes the role of a signal so you can in the case above call `onValue` to observe the row being tapped.

`RowAndProvider` is generic on a `Provider` type conforming to `SignalProvider`. As for the example above, the provider was just a basic signal `Signal<()>` for observing row taps. But if you append a view that conforms to `SignalProvider`, such as many `UIControl`s, `append` will return an updated `RowAndProvider` holding the added view as its provider:

```swift
let enabledSwitch = UISwitch(...)
let row = RowView(title: "title") // -> RowView
.append(enabledSwitch) // -> RowAndProvider<UISwitch>
```

Now `RowAndProvider` holds the switch and provides the switch's signal for convenience:

```swift
bag += row.onValue { enabled in /* switch updated */ }
```

If you add another providing view to a `RowAndProvider` it will change to provide the latest view.

```swift
bag += section.appendRow(title: "title") // -> RowAndProvider<Signal<()>>
.append(enabledSwitch) // -> RowAndProvider<UISwitch>
.onValue { enabled in ... }
```

If you want to opt out of changing the provider you can cast the appended provider to a `UIView`:

```swift
bag += section.appendRow(title: "title") // -> RowAndProvider<Signal()>
.append(enabledSwitch as UIView) // -> RowAndProvider<Signal()>
.onValue { /* row tapped */ }

bag += enabledSwitch.onValue { enabled in ... }
```

By using the power of Flow's signals together with forms we can build our UI and logic in a more declarative way:

```swift
let feature: ReadWriteSignal<Bool>
bag += section.appendRow(title: "Feature")
.append(UISwitch()) // -> The providedSignal is ReadWriteSignal<Bool>
.bidirectionallyBindTo(feature.atOnce())
```
92 changes: 92 additions & 0 deletions Documentation/Keyboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Keyboard - Adjusting for keyboards

Working with keyboards on iOS can be a challenge as they are virtual and take up a considerable amount of the screen once presented. As we do not want the keyboard to cover important UI our layout has to react to keyboard changes.

## Scroll views

Scroll views are great for handling dynamic content as well as different screen sizes. Scroll views are also really useful for handling keyboards as their insets can be adjusted. This is something Form takes advantage of:

```swift
bag += scrollView.adjustInsetsForKeyboard()
```

Form can also make sure the current first responder view is kept visible:

```swift
bag += scrollView.scrollToRevealFirstResponder()
```

For convenience Form's `UIViewController.install()` helper will set these up when using the default options:

```swift
// Will install the view in a scroll view and setup keyboard avoidance.
bag += viewController.install(view)
```

## Keyboard events

Sometimes you need to make other adjustments based on keyboard events. It is important that these adjustments are performed in order. Form solves this by delivering keyboard events to parents before their children. That is why `keyboardSignal()` is called on an instance of a view:

```swift
bag += view.keyboardSignal().onValue { keyboardEvent in
keyboardEvent.animation.animate {
// Animate updates to match the keyboard animation.
}
}
```

If you need to affect the order of events delivered to a specific view, you can optionally provide a priority (`KeyboardEventPriority`). This is useful for views such as scroll views that do several independent adjustments.

## View port events

To simplify keyboard adjustments it is sometimes useful to know what area of the screen that is not covered by the keyboard so you can update the frame of some UI to fit within that visible area. For this Form provides the `viewPortSignal`.

```swift
bag += view.viewPortSignal().onValue { viewPort in
self.frame = /// use viewPort to calculate.
}
```

As well as the `viewPortEventSignal` when you need access to the animation parameters:

```swift
bag += viewPortEventSignal().onValue { event in
event.animation.animate {
self.frame = /// use event.viewPort to calculate.
}
}
```

# Working with responders

Form also comes with several helpers to make it easier to work with responders. By using `setNextResponder()` you could set up a control to set a new first responder once it ends editing on exit:

```swift
let emailField = UITextField(...)
let passwordField = UITextField(...)
bag += emailField.setNextResponder(passwordField)
```

Or more conveniently you can chain several controls together:

```swift
bag += chainResponders(emailField, passwordField)
```

And update the `returnKey` of these controls:

```swift
bag += chainResponders(emailField, passwordField, returnKey: .next)
```

As well as specifying whether the last controller should loop back to the first one:

```swift
bag += chainResponders(emailField, passwordField, shouldLoop: true)
```

There is also a powerful helper that finds all descendant controls of a view and chains them together ordered top-left to bottom-right:

```swift
bag += rootView.chainAllControlResponders(returnKey: .next)
```
106 changes: 106 additions & 0 deletions Documentation/Layout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Layout - Laying out and updating view hierarchies

The layout of a typical iOS application's UI consists of organizing views into hierarchies and letting auto layout position them by setting up constraints between views. Even though UIKit has great support for handling most of our layout needs, there is always room for some handy extensions and helpers to make working with layouts and view hierarchies even nicer.

## Scroll views

Few layouts can rely on a static layout that will always fit within a screen. Often we have to work with devices of many different screen sizes. This means that we can seldom guarantee that all content will fit and hence we often place our content in scroll views.

Form adds several helpers to make it easier to work with scroll views. For example, to embed a view in a scroll view and to set up the required constraints:

```swift
scrollView.embedView(view, scrollAxis: .vertical)
```

If you want to embed multiple views where spacing is added between views to evenly fill up to the scroll views height, you can use:

```swift
bag += scrollView.embedWithSpacingBetween(views)
```

Form also provides helpers to pin a view to an edge of a scroll view and update the insets accordingly:

```swift
bag += scrollView.embedPinned(button, edge: .bottom, minHeight: 44)
```

### View controller installation

As it is so common to set up your views in scroll views and also to set up your view controller to use this scroll view, Form provides the install helper:

```swift
bag += viewController.install(view)
```

This will create a scroll view and embed the view setup with constraints for vertical scrolling. Furthermore, it will by default setup the scroll view to adjust its insets to make room for a keyboard as well as scroll any first responder into view if it got covered by the keyboard. You can customize the behavior of `install()` by passing an explicit options parameter (`InstallOptions`).

You can also provide a configure closure for further setup once the created scroll view has been added to a window:

```swift
bag += viewController.install(view) { scrollView in
// Not called until the scroll view has been added to a window.
}
```

Install can also be used for adding multiple views to a scroll view, and by default, space is added between views to evenly fill up to the scroll views height:

```swift
bag += viewController.install(topView, bottomView)
```

## View embedding

Embedding views and setting up proper constraints for common scenarios can be repetitive and hence Form comes with some nice helpers to handle the most common cases. You can optionally provide customization such as edge insets or which edges to pin to:

```swift
parent.embedView(child, edgeInsets: ..., pinToEdge:..)
```

And for convenience you can use initializers as well:

```swift
let parent = UIView(embeddedView: child, edgeInsets: ..., pinToEdge:.. )
```

## View hierarchies

Form comes with several helpers to work with view hierarchies such as getting a view's all ascendents or descendants and variants thereof. These are implemented as extensions on the `ParentChildRelational` protocol that `UIView`, as well as `UIViewController` and `CALayer`, conform to.

```swift
let controls = view.allDescendants(ofType: UIControl.self)
```

## Working with constraints

Even though the many helpers provided by UIKit and Form will set up the constraints for you, you sometimes need to set some up by hand. Form comes with some convenience helpers to make it even more readable to work with layout anchors:

```swift
let topConstraint = self.topAnchor == view.topAnchor - margin
activate(
topConstraint,
self.centerXAnchor == view.centerXAnchor,
self.widthAnchor == view.widthAnchor*2,
self.bottomAnchor == view.bottomAnchor + margin)
```

You can use ==, <= and >= to construct constraints between anchors and adjusting the constant with + and - and the multiplier with * and /. You can also use `activate()` for activating the constraints and `deactivate()` to deactivate them.

## UINavigationItem

Navigation bars can hold several items to either the left or the right of a navigation bar. Form comes with some helpers to add items that also returns the added items for convenience:

```swift
let item = navigationItem.addItem(.init(system: .done), position: .right)
bag += item.onValue { ... }
```

## SubviewOrderable

`SubviewOrderable` is a simple protocol for working with views such as `UIStackView` that has an array of ordered views. In Form, views such as `SectionView` and the helper `RowAndProvider` also conforms to this protocol. This allows us to add convenience helpers to `SubviewOrderable` such as different kinds of append and prepend helpers that will be available to all these conforming types. These are especially useful for building `RowView`s:

```swift
let row = RowView(title: "title", subtitle: "subtitle")
.prepend(iconImage)
.append("details")
.append(.chevron)
```
Binary file added Documentation/MessagesCustom.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Documentation/MessagesSystem.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 4288b0f

Please sign in to comment.