Update Tutorial 1
MODIFY
Samples/Tutorial/Tutorial1.md
Samples/Tutorial/Tutorial1.md
@@ -34,7 +34,7 @@ Let's start with what a `Screen` is, and how it relates to the view controller.
The `Screen` protocol is a marker protocol, intended to describe the view model that will be used to drive a view controller.
-For out welcome screen, we'll define what it needs for a backing view model:
+For our welcome screen, we'll define what it needs for a backing view model:
```swift
struct WelcomeScreen: Screen {
/// The current name that has been entered.
@@ -50,7 +50,7 @@ struct WelcomeScreen: Screen {
}
```
-Now add the (convenient) `WelcomeView` to our view controller (if you would like to create and layout the view yourself, feel free to do it!). Add a `welcomeView` property to the view controller, and add and lay it out in `viewDidLoad` and `viewDidLayoutSubviews` respectively.
+Now add the (convenient) `WelcomeView` to our view controller (if you would like to create and lay out the view yourself, feel free to do so!). Add a `welcomeView` property to the view controller, and add and lay it out in `viewDidLoad` and `viewDidLayoutSubviews` respectively.
```swift
// Import the `TutorialViews` module for the `WelcomeView`
import TutorialViews
@@ -61,7 +61,6 @@ final class WelcomeViewController: ScreenViewController<WelcomeScreen> {
required init(screen: WelcomeScreen) {
self.welcomeView = WelcomeView(frame: .zero)
super.init(screen: screen)
- update(with: screen)
}
override func viewDidLoad() {
@@ -77,17 +76,19 @@ final class WelcomeViewController: ScreenViewController<WelcomeScreen> {
}
```
-The screen is passed into the view controller when it is initialized, as well as `screenDidChange` being called anytime the back screen is updated. The template provides a single method to handle updates to the screen, so fill that in now:
+The screen is passed into the view controller when it is initialized. `screenDidChange` is called as part of the view controller's initialization (see `ViewControllerDescription.buildViewController`) and anytime the backing screen is updated, so it's an ideal place to put update logic. Fill that in now:
```swift
- private func update(with screen: WelcomeScreen) {
- /// Update UI
+ override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) {
+ super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment)
+
+ // Update the UI
welcomeView.name = screen.name
welcomeView.onNameChanged = screen.onNameChanged
welcomeView.onLoginTapped = screen.onLoginTapped
}
```
-Any time the screen is updated, the `WelcomeViewController` will update the `name` and `onNameChanged` fields on the `WelcomeView`. We can't quite run yet, as we still need to fill in the basics of our workflow.
+Any time the screen is updated, the `WelcomeViewController` will now update the `name`, `onNameChanged`, and `onLoginTapped` fields on the `WelcomeView`. We can't quite run yet, as we still need to fill in the basics of our workflow.
### Workflows and Rendering Type
@@ -102,7 +103,7 @@ extension WelcomeWorkflow {
func render(state: WelcomeWorkflow.State, context: RenderContext<WelcomeWorkflow>) -> Rendering {
return WelcomeScreen(
name: "",
- onNameChanged: { _ in
+ onNameChanged: { newName in
},
onLoginTapped: {
})
@@ -112,7 +113,7 @@ extension WelcomeWorkflow {
### Setting up the ContainerViewController
-Now we have our `WelcomeWorkflow` rendering a `WelcomeScreen`, and have a view controller that knows how to display with a `WelcomeScreen`. It's time to bind this all together and get it displaying.
+Now we have our `WelcomeWorkflow` rendering a `WelcomeScreen`, and have a view controller that knows how to display with a `WelcomeScreen`. It's time to bind this all together and actually show it on the screen!
We'll update the `TutorialContainerViewController` to hold a child *ContainerViewController* that will host our workflow:
@@ -137,7 +138,7 @@ public final class TutorialContainerViewController: UIViewController {
Now, we've created our `ContainerViewController` with the `WelcomeWorkflow` as the root.
-We can finally run the app again! It will look the exact same as before, but now powered by our workflow.
+We can finally run the app again! It will look exactly the same as before, but now it is powered by our workflow.
## Driving the UI from Workflow State
@@ -145,9 +146,10 @@ Right now, the workflow isn't handling any of the events from the UI. Let's upda
### State
-All workflows have a `State` type that represents the internal state of the workflow. This should be all of the data that this workflow is _responsible_ for - usually corresponds to the state for the UI.
+All workflows have a `State` type that represents the internal state of the workflow. This should be all of the data for which *this* workflow is _responsible_. It usually corresponds to the state for the UI.
+
+Let's model the first part of state that we want to track: the login `name`. Update the `State` type to include a name property. We will also need to update `makeInitialState` to give an initial value:
-We will model the first part of state that we want to track, the login `name`. Update the `State` type to include a name. We will also need to update `makeInitialState` to give an initial value:
```swift
// MARK: State and Initialization
@@ -164,7 +166,7 @@ extension WelcomeWorkflow {
// ...
```
-Now that we have the state modelled, we'll send it to the UI every time a render pass happens - the text field will overwrite it's value with whatever was provided.
+Now that we have the state modeled, we'll send it to the UI every time a render pass happens. The text field will overwrite its value with whatever was provided.
```swift
// MARK: Rendering
@@ -176,7 +178,7 @@ extension WelcomeWorkflow {
func render(state: WelcomeWorkflow.State, context: RenderContext<WelcomeWorkflow>) -> Rendering {
return WelcomeScreen(
name: state.name,
- onNameChanged: { _ in
+ onNameChanged: { newName in
},
onLoginTapped: {
})
@@ -184,15 +186,16 @@ extension WelcomeWorkflow {
}
```
-If you run the app again, you'll see that it still behaves the same, letting your type into the name field. This is because we only have rendered a screen once.
+If you run the app again, you'll see that it still behaves the same, letting your type into the name field. This is because we have only rendered the screen once.
-To update the workflow's internal state, we need to add an "Action":
+You may have noticed that your workflow only has access to its `State` in a few functions, and even then in many cases it is read-only. This is intentional. The Workflow infrastructure manages the state for the various workflows that are running and triggers a re-render when appropriate. In order to update the workflow's internal state, we need to add an "Action":
### Actions
-Actions define how a workflow handles events received from the outside world, like UI events (such a button presses), network requests, data stores, etc. Generally an `Action` type is an enum which make it easy to define all of the actions that this workflow will handle.
+Actions define how a workflow handles events received from the outside world, such as UI events (e.g. button presses), network requests, data stores, etc. Generally an `Action` type is an enum which makes it easy to define all of the actions that this workflow will handle.
Add a case to the existing `Action` called `nameChanged` to update our internal state:
+
```swift
// MARK: Actions
@@ -246,7 +249,7 @@ extension WelcomeWorkflow {
### The update loop
-If we run the app again, it will still behave the same but we are now capturing the name changes in our workflow's state, as well as having the UI show the name based upon the workflows internal state.
+If we run the app again, it will still behave the same but we are now capturing the name changes in our workflow's state, as well as having the UI show the name based upon the workflow's internal state.
To see this, change the `apply` method to append an extra letter on the name received, eg:
```swift
@@ -265,13 +268,13 @@ To see this, change the `apply` method to append an extra letter on the name rec
Running the app again will have the name field suffixed with a letter 'a' on every keypress. We probably want to undo this change, but it demonstrates that the UI is being updated from the internal state.
Here is what is happening on each keypress:
-1) The UI called `onNameChanged` whenever the contents of the text field change.
+1) The UI calls `onNameChanged` whenever the contents of the text field change.
2) The closure calls `sink.send(.nameChanged(name: name)`, which sends an action to be handled by the workflow.
3) The `apply` method on the action is called. The `state` parameter is an `inout` parameter, so when it is updated in `apply`, it updates the actual state.
- This is effectively the same as this method being written `func apply(fromState: State) -> (State, Output?)` where it transforms the previous state into a new state.
4) As an action was just handled, the workflow must now be re-rendered so the `Screen` (and from it, the UI) can be updated.
- `render` is called on the workflow. A new screen is returned with the updated `name` from the internal state.
-5) The view controller is provided the new screen with the call to `func screenDidChange(from previousScreen: WelcomeScreen)`. Generally, the update is handled in the convenience method on the template of `private func update(with screen: WelcomeScreen)`.
+5) The view controller is provided the new screen with the call to `func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment)`.
- This view controller updates the text field with the received name value, and also updates the callbacks for when the name changes or login is pressed.
6) The workflow waits for the next Action to be received, and then the goes through the same update loop.

MODIFY
Tooling/Templates/Screen (View Controller).xctemplate/___FILEBASENAME___Screen.swift
Tooling/Templates/Screen (View Controller).xctemplate/___FILEBASENAME___Screen.swift
@@ -17,14 +17,12 @@ struct ___VARIABLE_productName___Screen: Screen {
final class ___VARIABLE_productName___ViewController: ScreenViewController<___VARIABLE_productName___Screen> {
required init(screen: ___VARIABLE_productName___Screen, environment: ViewEnvironment) {
super.init(screen: screen, environment: environment)
- update(with: screen, environment: environment)
}
override func screenDidChange(from previousScreen: ___VARIABLE_productName___Screen, previousEnvironment: ViewEnvironment) {
- update(with: screen, environment: environment)
- }
+ super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment)
- private func update(with screen: ___VARIABLE_productName___Screen, environment: ViewEnvironment) {
- /// Update UI
+ // Update UI
}
}
+

MODIFY
Tooling/Templates/Workflow (Verbose).xctemplate/___FILEBASENAME___Workflow.swift
Tooling/Templates/Workflow (Verbose).xctemplate/___FILEBASENAME___Workflow.swift
@@ -56,7 +56,10 @@ extension ___VARIABLE_productName___Workflow {
// MARK: Rendering
extension ___VARIABLE_productName___Workflow {
- func render(state: ___VARIABLE_productName___Workflow.State, context: RenderContext<___VARIABLE_productName___Workflow>) -> String {
+ // TODO: Change this to your actual rendering type
+ typealias Rendering = String
+
+ func render(state: ___VARIABLE_productName___Workflow.State, context: RenderContext<___VARIABLE_productName___Workflow>) -> Rendering {
#warning("Don't forget your compose implementation and to return the correct rendering type!")
return "This is likely not the rendering that you want to return"
}

Update Tutorial 1