Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ DerivedData
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Checkouts

Carthage/Build
53 changes: 53 additions & 0 deletions Documents/4_pincode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#### 4. Pincode 🔐

While for some forms like the one presented in [Example 1. Form 🐥](1_form.md) the validity of the input as well as the result as a whole rely only on the current input, there are also more complicated situations in which previous values are just as important as the current ones.

Take, for example, a form to change your pincode. First, you'll have to input your current pincode to authorize yourself, then you'll enter a new pincode that also needs to be confirmed by entering the same new pincode a second time. Whats more, you are not allowed to use the same pincode as you had before.

In this example, the most interesting bits are:

1. The use of `Action` to generate a stream of inputs.
2. The implementation of a state machine in `PincodeViewModel`.

Let's start with the `Action` first:

`PincodeViewModel` has a `MutableProperty` called `input` which will be bound to the input by the `PincodeViewController` (In this example, a simple `UITextField` is used for the input instead of fancy number buttons).
`enterCodeAction` is an `Action` that, invoked by tap on the button, will send the current value of `input` once.

```swift
enterCodeAction = Action(state: input, enabledIf: inputValid) { state, _ in
SignalProducer(value: state)
}
```

Thus, `enterCodeAction.values` is a `Signal` on which for each tap on the buttn, the current input value is sent.


The second part is to turn the stream of input events into the the current state. To achieve this, the `scan` operator is used on the signal of input values. Maybe you are already familiar with the [`reduce`](https://developer.apple.com/documentation/swift/array/2298686-reduce) operator in the swift standard library which combines all values of a sequence, like an array, into a final accumluated value. In the context of ReactiveCocoa, `reduce` works the same way, just on signals. `scan` works almost the same as `reduce`, but while `reduce` produces only one final accumulated value, `scan` also produces each intermediary result.
This behavior of `scan` is important since we are not just interested in the final state of the form, but in each state after a new input.

```swift
let state = enterCodeAction.values.scan(State.initial) { (currentState, pincode) in
let nextState: State
switch currentState {
case .initial:
// Initially, the pin code entered has to match the current pincode
nextState = (pincode == currentPincode) ?
.oldPincodeCorrect(message: nil) :
.error(reason: "Wrong pincode")
...
...
default:
// After the new pincode was confirmed or an error, nothing changes anymore
nextState = currentState
}
return nextState
}
```

With each new value on the `enterCodeAction.values` signal, the code in `scan` is executed once with the `currentState` (started with the initial state) and the current `pincode` that was entered. Based on these two values, the next state has to be generated and returned. E.g. in the initial state, the input has to match the current pincode. If thats the case, the next step is to input a new input. Otherwise, the next state ist the error state (with an according message that can be shown to the user).
Cases for all the other possible states are implemented to handle all other transitions in the state machine.

What remains is to wire up the viewmodel to the interface in `PincodeViewController`.

---
24 changes: 24 additions & 0 deletions RACNest.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
5BC29BC91F62B970007D2AA7 /* PincodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC29BC81F62B970007D2AA7 /* PincodeViewController.swift */; };
5BC29BCC1F62B992007D2AA7 /* PincodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC29BCB1F62B992007D2AA7 /* PincodeViewModel.swift */; };
B25D051B1EA2CCBD0039D323 /* ReactiveSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B25D051A1EA2CCBD0039D323 /* ReactiveSwift.framework */; };
C72D32E21C470F5300F88B11 /* RACNestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72D32E11C470F5300F88B11 /* RACNestTests.swift */; };
C72D32F81C470FE000F88B11 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72D32ED1C470FE000F88B11 /* AppDelegate.swift */; };
Expand Down Expand Up @@ -43,6 +45,8 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
5BC29BC81F62B970007D2AA7 /* PincodeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PincodeViewController.swift; sourceTree = "<group>"; };
5BC29BCB1F62B992007D2AA7 /* PincodeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PincodeViewModel.swift; sourceTree = "<group>"; };
B25D051A1EA2CCBD0039D323 /* ReactiveSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReactiveSwift.framework; path = Carthage/Build/iOS/ReactiveSwift.framework; sourceTree = "<group>"; };
C72D32C91C470F5200F88B11 /* RACNest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RACNest.app; sourceTree = BUILT_PRODUCTS_DIR; };
C72D32DD1C470F5300F88B11 /* RACNestTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RACNestTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -94,6 +98,23 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
5BC29BC71F62B92D007D2AA7 /* Pincode */ = {
isa = PBXGroup;
children = (
5BC29BC81F62B970007D2AA7 /* PincodeViewController.swift */,
5BC29BCA1F62B975007D2AA7 /* Components */,
);
path = Pincode;
sourceTree = "<group>";
};
5BC29BCA1F62B975007D2AA7 /* Components */ = {
isa = PBXGroup;
children = (
5BC29BCB1F62B992007D2AA7 /* PincodeViewModel.swift */,
);
path = Components;
sourceTree = "<group>";
};
C72D32C01C470F5200F88B11 = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -164,6 +185,7 @@
C72D32F61C470FE000F88B11 /* ViewControllers */ = {
isa = PBXGroup;
children = (
5BC29BC71F62B92D007D2AA7 /* Pincode */,
C7852C3B1C4ACF9500375089 /* Form */,
C76C20591C504B750083F4F5 /* Search */,
C7852C3E1C4ACFE000375089 /* Main */,
Expand Down Expand Up @@ -410,9 +432,11 @@
C7CB87121C51AB8E00ED9AE6 /* GenericTableCell.swift in Sources */,
C7852C461C4ACFE000375089 /* MainCellItem.swift in Sources */,
C7F740591C4B2A2F00519895 /* UserDefaults.swift in Sources */,
5BC29BC91F62B970007D2AA7 /* PincodeViewController.swift in Sources */,
C7852C331C4ACAAA00375089 /* StoryboardViewController.swift in Sources */,
C72D330F1C471B3000F88B11 /* TableViewProtocols.swift in Sources */,
C76C205B1C504B9D0083F4F5 /* SearchViewController.swift in Sources */,
5BC29BCC1F62B992007D2AA7 /* PincodeViewModel.swift in Sources */,
C72D330D1C471B0C00F88B11 /* CellProtocols.swift in Sources */,
C7852C4C1C4B0E2B00375089 /* FormViewModel.swift in Sources */,
C7852C351C4ACB6B00375089 /* Storyboard.swift in Sources */,
Expand Down
1 change: 1 addition & 0 deletions RACNest/AppRelated/StoryboardViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import UIKit
enum StoryboardViewController : String, StoryboardViewControllerType {
case Form = "FormViewController"
case Search = "SearchViewController"
case Pincode = "PincodeViewController"
}
Loading