Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Swift] Rename compose to render and update docs. #301

Merged
merged 1 commit into from
Apr 18, 2019
Merged
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
40 changes: 20 additions & 20 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,31 +30,31 @@ public protocol Workflow: AnyWorkflowConvertible {

func workflowDidChange(from previousWorkflow: Self, state: inout State)

func compose(state: State, context: WorkflowContext<Self>) -> Rendering
func render(state: State, context: RenderContext<Self>) -> Rendering

}

```

```kotlin
interface Workflow<in I : Any, S : Any, out O : Any, out R : Any> {
interface Workflow<in InputT : Any, StateT : Any, out OutputT : Any, out RenderingT : Any> {

fun initialState(input: I): S
fun initialState(input: InputT): StateT

fun onInputChanged(
old: I,
new: I,
state: S
): S = state
old: InputT,
new: InputT,
state: StateT
): StateT = state

fun compose(
input: I,
state: S,
context: WorkflowContext<S, O>
): R
fun render(
input: InputT,
state: StateT,
context: RenderContext<StateT, OutputT>
): RenderingT

fun snapshotState(state: S): Snapshot
fun restoreState(snapshot: Snapshot): S
fun snapshotState(state: StateT): Snapshot
fun restoreState(snapshot: Snapshot): StateT

}

Expand Down Expand Up @@ -91,35 +91,35 @@ When the workflow is first started, it is queried for an initial state value. Fr

### Workflows produce an external representation of their state via `Rendering`

Immediately after starting up, or after a state transition occurs, a workflow will have its `compose(state:context:)` method called. This method is responsible for creating and returning a value of type `Rendering`. You can think of `Rendering` as the "external state" of the workflow. While a workflow's internal state may contain more detailed or comprehensive state, the `Rendering` (external state) is a type that is useful outside of the workflow.
Immediately after starting up, or after a state transition occurs, a workflow will have its `render(state:context:)` method called. This method is responsible for creating and returning a value of type `Rendering`. You can think of `Rendering` as the "external state" of the workflow. While a workflow's internal state may contain more detailed or comprehensive state, the `Rendering` (external state) is a type that is useful outside of the workflow.

When building an interactive application, the `Rendering` type is commonly (but not always) a view model that will drive the UI layer.


### Workflows form a hierarchy (they may have children)

As they produce a `Rendering` value, it is common for workflows to delegate some portion of that work to a _child workflow_. This is also done via the `WorkflowContext` that is passed into the `compose` method. In order to delegate to a child, the parent workflow instantiates the child within the `compose` method. The parent then calls `compose` on the context, with the child workflow as the single argument. The infrastructure will spin up the child workflow (including initializing its initial state) if this is the first time this child has been used, or, if the child was also used on the previous `compose` pass, the existing child will be updated. Either way, `compose` will ultimately be called on the child (by the Workflow infrastructure), and the resulting `Child.Rendering` value will be returned to the parent.
As they produce a `Rendering` value, it is common for workflows to delegate some portion of that work to a _child workflow_. This is also done via the `RenderContext` that is passed into the `render` method. In order to delegate to a child, the parent workflow instantiates the child within the `render` method. The parent then calls `render` on the context, with the child workflow as the single argument. The infrastructure will spin up the child workflow (including initializing its initial state) if this is the first time this child has been used, or, if the child was also used on the previous `render` pass, the existing child will be updated. Either way, `render` will ultimately be called on the child (by the Workflow infrastructure), and the resulting `Child.Rendering` value will be returned to the parent.

This allows a parent to return complex `Rendering` types (such as a view model representing the entire UI state of an application) without needing to model all of that complexity within a single workflow.


### Workflows can respond to UI events

The `WorkflowContext` that is passed into `compose` as the second parameter provides some useful tools to assist in creating the `Rendering` value.
The `RenderContext` that is passed into `render` as the second parameter provides some useful tools to assist in creating the `Rendering` value.

If a workflow is producing a view model, it is common to need an event handler to respond to UI events. The `WorkflowContext` has API to create an event handler that, when called, will advance the workflow by dispatching an action back to the workflow.
If a workflow is producing a view model, it is common to need an event handler to respond to UI events. The `RenderContext` has API to create an event handler that, when called, will advance the workflow by dispatching an action back to the workflow.


### Workflows can subscribe to external event sources

If a workflow needs to respond to some external event source (e.g. push notifications), the workflow can ask the context to listen to those events from within the `compose` method.
If a workflow needs to respond to some external event source (e.g. push notifications), the workflow can ask the context to listen to those events from within the `render` method.


### Workflows can perform asynchronous tasks (Workers)

`Workers` are very similar in concept to child workflows. Unlike child workflows, however, workers do not have a `Rendering` type; they only exist to perform a single asynchronous task before sending an output event back up the tree to their parent.

A workflow can ask the infrastructure to await the result of a worker by handing that worker to the context within a call to the `compose` method.
A workflow can ask the infrastructure to await the result of a worker by handing that worker to the context within a call to the `render` method.

### Workflows are advanced by `Action`s

Expand Down
2 changes: 1 addition & 1 deletion docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
|---|---|---|---|
| **Modularity** | `Component` | TK | `Workflow` is analogous to React's `Component` |
| **State** | Each `Component` has a `state` property that is read directly and updated via a `setState` method. | State is called `Model` in Elm. | `Workflow`s have an associated state type. The state can only be updated when the input changes, or with a `WorkflowAction`. |
| **Views** | `Component`s have a `render` method that returns a tree of elements. | Elm applications have a `view` function that returns a tree of elements. | Since workflows are not tied to any particular UI view layer, they can have an arbitrary rendering type. The `compose()` method returns this type. |
| **Views** | `Component`s have a `render` method that returns a tree of elements. | Elm applications have a `view` function that returns a tree of elements. | Since workflows are not tied to any particular UI view layer, they can have an arbitrary rendering type. The `render()` method returns this type. |
| **Dependencies** | React allows parent components to pass "props" down to their children. | TK | In Swift, `Workflow`s are often structs that need to be initialized with their dependencies and configuration data from their parent. In Kotlin, they have a separate type parameter (`InputT`) that is always passed down from the parent. `Workflow` instances can also inject dependencies, and play nicely with dependency injection frameworks.
| **Composability** | TK | TK | TK |
| **Event Handling** | TK | TK | TK |
Expand Down
36 changes: 18 additions & 18 deletions docs/swift/building-a-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ extension DemoWorkflow {

}

func compose(state: State, context: WorkflowContext<DemoWorkflow>) -> String {
func render(state: State, context: RenderContext<DemoWorkflow>) -> String {
return "Hello, \(name)"
}

Expand All @@ -39,31 +39,31 @@ A type conforming to `Workflow` represents a single node in the workflow tree. I

Configuration parameters, strings, network services… If your workflow needs access to a value or object that it cannot create itself, they should be passed into the workflow's initializer.

Every workflow defines its own `State` type to contain any data that should persist through subsequent compose passes.
Every workflow defines its own `State` type to contain any data that should persist through subsequent render passes.

## Compose
## Render

Workflows are only useful when they render a value for use by their parent (or, if they are the root workflow, for display). This type is very commonly a view model, or `Screen`. The `compose(state:context:)` method has a couple of parameters, so we’ll work through them one by one.
Workflows are only useful when they render a value for use by their parent (or, if they are the root workflow, for display). This type is very commonly a view model, or `Screen`. The `render(state:context:)` method has a couple of parameters, so we’ll work through them one by one.

```swift
func compose(state: State, context: WorkflowContext<DemoWorkflow>) -> Rendering
func render(state: State, context: RenderContext<DemoWorkflow>) -> Rendering
```

### `state`

Contains a value of type `State` to provide access to the current state. Any time the state of workflow changes, `compose` is called again to take into account the change in state.
Contains a value of type `State` to provide access to the current state. Any time the state of workflow changes, `render` is called again to take into account the change in state.

### `context`

The workflow context:
The render context:
- provides a way for a workflow to defer to nested (child) workflows to generate some or all of its rendered output. We’ll walk through that process later on when we cover composition.
- allows a workflow to request the execution of asynchronous tasks (`Worker`s)
- generates event handlers for use in constructing view models.

In order for us to see the anything in our app, we'll need to return a `Screen` that can be turned into a view controller:

```swift
func compose(state: State, context: WorkflowContext<DemoWorkflow>) -> DemoScreen {
func render(state: State, context: RenderContext<DemoWorkflow>) -> DemoScreen {
return DemoScreen(title: "A nice title")
}
```
Expand Down Expand Up @@ -102,12 +102,12 @@ There are two things that the `apply(toState:)` method is responsible for:
- Transitioning state
- (Optionally) emitting an output event

Note that the `compose(state:context:)` method is called after every state change, so you can be sure that any state changes will be reflected.
Note that the `render(state:context:)` method is called after every state change, so you can be sure that any state changes will be reflected.

Since we have a way of expressing an event from our UI, we can now use the callback on our view model to send that event back to the workflow:

```swift
func compose(state: State, context: WorkflowContext<DemoWorkflow>) -> DemoScreen {
func render(state: State, context: RenderContext<DemoWorkflow>) -> DemoScreen {
// Create a sink of our Action type so we can send actions back to the workflow.
let sink = context.makeSink(of: Action.self)

Expand Down Expand Up @@ -170,10 +170,10 @@ struct AsyncWorker: Worker {

Because a Worker is a declarative representation of work, it also needs to define an `isEquivalent` to guarantee that we are not running more than one at the same time. For the simple example above, it is always considered equivalent as we want only one of this type of worker running at a time.

In order to start asynchronous work, the workflow requests it in the compose method, looking something like:
In order to start asynchronous work, the workflow requests it in the render method, looking something like:

```swift
public func compose(state: State, context: WorkflowContext<DemoWorkflow>) -> DemoScreen {
public func render(state: State, context: RenderContext<DemoWorkflow>) -> DemoScreen {

context.awaitResult(for: AsyncWorker()) { output -> Action in
switch output {
Expand Down Expand Up @@ -204,25 +204,25 @@ Workflows can define an output type, which may then be returned by Actions.

Composition is the primary tool that we can use to manage complexity in a growing application. Workflows should always be kept small enough to be understandable – less than 150 lines is a good target. By composing together multiple workflows, complex problems can be broken down into individual pieces that can be quickly understood by other developers (including future you).

The context provided to the `compose(state:context:)` method defines the API through which composition is made possible.
The context provided to the `render(state:context:)` method defines the API through which composition is made possible.

### The Workflow Context
### The Render Context

The useful role of children is ultimately to provide rendered values (typically screen models) via their `compose(state:context:)` implementation. To obtain that value from a child workflow, the `rendered(with context:key:)` method is invoked on the child workflow.
The useful role of children is ultimately to provide rendered values (typically screen models) via their `render(state:context:)` implementation. To obtain that value from a child workflow, the `rendered(with context:key:)` method is invoked on the child workflow.

When a workflow is rendered with the context, the context will do the following:
- Check if the child workflow is new or existing:
- If a workflow with the same type was used during the last render pass, the existing child workflow will be updated with the new workflow.
- Otherwise, a new child workflow node will be initialized.
- The child workflow's `compose(state:context:)` method is called.
- The child workflow's `render(state:context:)` method is called.
- The rendered value is returned.

In practice, this looks something like this:

```swift
struct ParentWorkflow: Workflow {

func compose(state: State, context: WorkflowContext<ParentWorkflow>) -> String {
func render(state: State, context: RenderContext<ParentWorkflow>) -> String {
let childWorkflow = ChildWorkflow(text: "Hello, World")
return childWorkflow.rendered(with: context)
}
Expand All @@ -235,7 +235,7 @@ struct ChildWorkflow: Workflow {

// ...

func compose(state: State, context: WorkflowContext<ChildWorkflow>) -> String {
func render(state: State, context: RenderContext<ChildWorkflow>) -> String {
return String(text.reversed())
}
}
Expand Down
2 changes: 1 addition & 1 deletion docs/swift/using-a-workflow-for-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,4 @@ let container = ContainerViewController(
viewRegistry: viewRegistry)
```

Now, when the `ContainerViewController` is shown, it will start the workflow and `compose` will be called returning the `DemoScreen`. The container will use the view registry to map the `DemoScreen` to a `DemoScreenViewController` and add it to the view hierarchy to display.
Now, when the `ContainerViewController` is shown, it will start the workflow and `render` will be called returning the `DemoScreen`. The container will use the view registry to map the `DemoScreen` to a `DemoScreenViewController` and add it to the view hierarchy to display.
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ extension ___VARIABLE_productName___Workflow {

extension ___VARIABLE_productName___Workflow {

func compose(state: ___VARIABLE_productName___Workflow.State, context: WorkflowContext<___VARIABLE_productName___Workflow>) -> String {
func render(state: ___VARIABLE_productName___Workflow.State, context: RenderContext<___VARIABLE_productName___Workflow>) -> String {
#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"
}
Expand Down
6 changes: 3 additions & 3 deletions swift/Workflow/Sources/AnyWorkflow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ extension AnyWorkflow {
/// That type information *is* present in our storage object, however, so we
/// pass the context down to that storage object which will ultimately call
/// through to `context.render(workflow:key:reducer:)`.
internal func render<Parent>(context: WorkflowContext<Parent>, key: String, outputMap: @escaping (Output) -> AnyWorkflowAction<Parent>) -> Rendering {
internal func render<Parent>(context: RenderContext<Parent>, key: String, outputMap: @escaping (Output) -> AnyWorkflowAction<Parent>) -> Rendering {
return storage.render(context: context, key: key, outputMap: outputMap)
}

Expand All @@ -76,7 +76,7 @@ extension AnyWorkflow {
/// This type is never used directly.
fileprivate class AnyStorage {

func render<Parent>(context: WorkflowContext<Parent>, key: String, outputMap: @escaping (Output) -> AnyWorkflowAction<Parent>) -> Rendering {
func render<Parent>(context: RenderContext<Parent>, key: String, outputMap: @escaping (Output) -> AnyWorkflowAction<Parent>) -> Rendering {
fatalError()
}

Expand Down Expand Up @@ -113,7 +113,7 @@ extension AnyWorkflow {
return T.self
}

override func render<Parent>(context: WorkflowContext<Parent>, key: String, outputMap: @escaping (Output) -> AnyWorkflowAction<Parent>) -> Rendering {
override func render<Parent>(context: RenderContext<Parent>, key: String, outputMap: @escaping (Output) -> AnyWorkflowAction<Parent>) -> Rendering {
let outputMap: (T.Output) -> AnyWorkflowAction<Parent> = { [outputTransform] output in
return outputMap(outputTransform(output))
}
Expand Down
6 changes: 3 additions & 3 deletions swift/Workflow/Sources/AnyWorkflowConvertible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ extension AnyWorkflowConvertible {
/// - Parameter key: A string that uniquely identifies this workflow.
///
/// - Returns: The `Rendering` generated by the workflow.
public func rendered<Parent>(with context: WorkflowContext<Parent>, key: String = "") -> Rendering where Output: WorkflowAction, Output.WorkflowType == Parent {
public func rendered<Parent>(with context: RenderContext<Parent>, key: String = "") -> Rendering where Output: WorkflowAction, Output.WorkflowType == Parent {
return asAnyWorkflow().render(context: context, key: key, outputMap: { AnyWorkflowAction($0) })
}

public func rendered<Parent>(with context: WorkflowContext<Parent>, key: String = "") -> Rendering where Output == AnyWorkflowAction<Parent> {
public func rendered<Parent>(with context: RenderContext<Parent>, key: String = "") -> Rendering where Output == AnyWorkflowAction<Parent> {
return asAnyWorkflow().render(context: context, key: key, outputMap: { $0 })
}

Expand All @@ -46,7 +46,7 @@ extension AnyWorkflowConvertible where Output == Never {
/// - Parameter key: A string that uniquely identifies this workflow.
///
/// - Returns: The `Rendering` generated by the workflow.
public func rendered<T>(with context: WorkflowContext<T>, key: String = "") -> Rendering {
public func rendered<T>(with context: RenderContext<T>, key: String = "") -> Rendering {
// Convenience for workflow that have no output allowing them to be rendered with any context

return asAnyWorkflow()
Expand Down
Loading