-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Måns Bernhardt
committed
Jun 29, 2018
0 parents
commit 4288b0f
Showing
118 changed files
with
13,370 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
github "iZettle/Flow" ~> 1.1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
``` |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.