message: Clean up README and RELEASING; remove more docs stuff

old_path: .gitignore

new_path: .gitignore

diff:
# macOS
.DS_Store
-# Compiled class file
-*.class
-
-# Log file
-*.log
-
-# BlueJ files
-*.ctxt
-
-# Mobile Tools for Java (J2ME)
-.mtj.tmp/
-
-# Package Files #
-*.jar
-*.war
-*.nar
-*.ear
-*.zip
-*.tar.gz
-*.rar
-
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
-hs_err_pid*
-
-# Gradle
-out/
-.gradle/
-build/
-local.properties
-
-# Intellij
-*.iml
-.idea/
-
# cocoapods-generate
gen/
@@ -53,10 +19,5 @@ xcuserdata/
# Sample workspace
SampleApp.xcworkspace
-# Special Mkdocs files
-docs/kotlin/api/
-docs/swift/api/
-site/
-
# ios-snapshot-test-case Failure Diffs
FailureDiffs/

diff:
@@ -62,116 +62,7 @@ and assign it to @bencochran or @aquageek.*
git push origin trunk
```
-1. Publish the website. See below.
-
-## Deploying the documentation website
-
-Official Workflow documentation lives at <https://squareup.github.io/workflow>. The website content
-consists of three parts:
-
-1. Markdown documentation: Lives in the `docs/` folder, and consists of a set of hand-written
- Markdown files that document high-level concepts. The static site generator
- [mkdocs](https://www.mkdocs.org/) (with [Material](https://squidfunk.github.io/mkdocs-material/)
- theming) is used to convert the Markdown to static, styled HTML.
-1. Kotlin API reference: Kdoc embedded in Kotlin source files is converted to GitHub-flavored
- Markdown by Dokka and then included in the statically-generated website.
-1. Swift API reference: Markup comments from Swift files are converted Markdown by
- [Sourcedocs](https://github.com/eneko/SourceDocs) and then included in the statically-generated
- website.
-
-**Note: The documentation site is automatically built and deployed whenever a version tag is pushed.
-You only need these steps if you want to work on the site locally.**
-
-### Setting up the site generators
-
-If you've already done this, you can skip to _Deploying the website to production_ below.
-
-#### Kotlin: Dokka
-
-Dokka runs as a Gradle plugin, so you need to be able to build the Kotlin source with Gradle, but
-that's it. To generate the docs manually, run:
-
-```bash
-cd kotlin
-./gradlew dokka
-```
-
-#### Swift: Sourcedocs
-
-Sourcedocs generates a Markdown site from Swift files. You need Ruby, rubygems,
-bundler (2.x), Xcode 10.2+, CocoaPods, and of course Sourcedocs itself, to run it. Assuming you've
-already got Xcode, Ruby, and rubygems set up, install the rest of the dependencies:
-
-```bash
-gem install bundler cocoapods
-brew install sourcedocs
-```
-
-If that succeeded, you need to generate an Xcode project before running Sourcedocs:
-
-```bash
-cd swift/Samples/SampleApp/
-bundle exec pod install
-# If this is your first time running CocoaPods, that will fail and you'll need to run this instead:
-#bundle exec pod install --repo-update
-```
-
-You can manually generate the docs to verify everything is working correctly by running:
-
-```bash
-#cd swift/Samples/SampleApp/
-sourcedocs generate -- -scheme Workflow -workspace SampleApp.xcworkspace
-sourcedocs generate -- -scheme WorkflowUI -workspace SampleApp.xcworkspace
-sourcedocs generate -- -scheme WorkflowTesting -workspace SampleApp.xcworkspace
-```
-
-Note that currently sourcedocs only supports Xcode 10, if you run it with Xcode 11 you might see
-an error about Catalyst and only empty READMEs will get generated.
-
-#### mkdocs
-
-Mkdocs is written in Python, so you'll need Python 3 and pip in order to run it. Assuming those are
-set up, run:
-
-```bash
-pip install -r requirements.txt
-```
-
-Generate the site manually with:
-
-```bash
-mkdocs build
-```
-
-While you're working on the documentation files, you can run the site locally with:
-
-```bash
-mkdocs serve
-```
-
-### Deploying the website to production
-
-**Note: The documentation site is automatically built and deployed by a Github Workflow whenever a
-version tag is pushed. You only need these steps if you want to publish the site manually.**
-
-Before deploying the website for real, you need to export our Google Analytics key in an environment
-variable so that it will get added to the HTML. Get the key from one of the project maintainers,
-then add the following to your `.bashrc` and re-source it:
-
-```bash
-export WORKFLOW_GOOGLE_ANALYTICS_KEY=UA-__________-1
-```
-
-Now you're ready to publish the site! Just choose a tag or SHA to deploy from, and run:
-
-```bash
-./deploy_website.sh TAG_OR_SHA
-# For example:
-#./deploy_website.sh v0.18.0
-```
-
-This will clone the repo to a temporary directory, checkout the right SHA, build Kotlin and Swift
-API docs, generate HTML, and push the newly-generated content to the `gh-pages` branch on GitHub.
+1. Publish the website. See https://github.com/square/workflow/blob/trunk/RELEASING.md.
### Validating Markdown
@@ -190,41 +81,3 @@ Run the linter using the `lint_docs.sh`:
```
Rules can be configured by editing `.markdownlint.rb`.
-
----
-
-## Kotlin Notes
-
-### Development
-
-To build and install the current version to your local Maven repository (`~/.m2`), run:
-
-```bash
-./gradlew clean installArchives
-```
-
-### Deploying
-
-#### Configuration
-
-In order to deploy artifacts to a Maven repository, you'll need to set 4 properties in your private
-Gradle properties file (`~/.gradle/gradle.properties`):
-
-```
-RELEASE_REPOSITORY_URL=<url of release repository>
-SNAPSHOT_REPOSITORY_URL=<url of snapshot repository
-SONATYPE_NEXUS_USERNAME=<username>
-SONATYPE_NEXUS_PASSWORD=<password>
-```
-
-#### Snapshot Releases
-
-Double-check that `gradle.properties` correctly contains the `-SNAPSHOT` suffix, then upload
-snapshot artifacts to Sonatype just like you would for a production release:
-
-```bash
-./gradlew clean build && ./gradlew uploadArchives --no-parallel --no-daemon
-```
-
-You can verify the artifacts are available by visiting
-https://oss.sonatype.org/content/repositories/snapshots/com/squareup/workflow/.

change_type: DELETE


message: Fix paths in Podfiles

change_type: MODIFY

old_path: Samples/SampleApp/Podfile

new_path: Samples/SampleApp/Podfile

diff:
@@ -2,12 +2,12 @@ project 'SampleApp.xcodeproj'
platform :ios, '9.3'
target 'SampleApp' do
- pod 'Workflow', path: '../../../Workflow.podspec', :testspecs => ['Tests']
- pod 'WorkflowUI', path: '../../../WorkflowUI.podspec', :testspecs => ['Tests']
+ pod 'Workflow', path: '../../Workflow.podspec', :testspecs => ['Tests']
+ pod 'WorkflowUI', path: '../../WorkflowUI.podspec', :testspecs => ['Tests']
end
target 'SampleAppTests' do
- pod 'Workflow', path: '../../../Workflow.podspec', :testspecs => ['Tests']
- pod 'WorkflowUI', path: '../../../WorkflowUI.podspec', :testspecs => ['Tests']
- pod 'WorkflowTesting', path: '../../../WorkflowTesting.podspec', :testspecs => ['Tests']
+ pod 'Workflow', path: '../../Workflow.podspec', :testspecs => ['Tests']
+ pod 'WorkflowUI', path: '../../WorkflowUI.podspec', :testspecs => ['Tests']
+ pod 'WorkflowTesting', path: '../../WorkflowTesting.podspec', :testspecs => ['Tests']
end

old_path: Samples/SampleSwiftUIApp/Podfile

new_path: Samples/SampleSwiftUIApp/Podfile

diff:
@@ -2,6 +2,6 @@ project 'SampleSwiftUIApp.xcodeproj'
platform :ios, '13'
target 'SampleSwiftUIApp' do
- pod 'Workflow', path: '../../../Workflow.podspec', :testspecs => ['Tests']
- pod 'WorkflowSwiftUI', path: '../../../WorkflowSwiftUI.podspec'
+ pod 'Workflow', path: '../../Workflow.podspec', :testspecs => ['Tests']
+ pod 'WorkflowSwiftUI', path: '../../WorkflowSwiftUI.podspec'
end

old_path: Samples/Tutorial/Podfile

new_path: Samples/Tutorial/Podfile

diff:
@@ -2,8 +2,8 @@ project 'Tutorial.xcodeproj'
platform :ios, '11.0'
target 'Tutorial' do
- pod 'Workflow', path: '../../../Workflow.podspec', :testspecs => ['Tests']
- pod 'WorkflowUI', path: '../../../WorkflowUI.podspec', :testspecs => ['Tests']
+ pod 'Workflow', path: '../../Workflow.podspec', :testspecs => ['Tests']
+ pod 'WorkflowUI', path: '../../WorkflowUI.podspec', :testspecs => ['Tests']
pod 'BackStackContainer', path: '../BackStackContainer/BackStackContainer.podspec'
pod 'TutorialViews', path: 'Frameworks/TutorialViews/TutorialViews.podspec'
@@ -16,5 +16,5 @@ target 'Tutorial' do
end
target 'TutorialTests' do
- pod 'WorkflowTesting', path: '../../../WorkflowTesting.podspec', :testspecs => ['Tests']
+ pod 'WorkflowTesting', path: '../../WorkflowTesting.podspec', :testspecs => ['Tests']
end

license: Apache License 2.0

repo: square/workflow-swift

original_message: Fix paths in Podfiles


message: Update paths in swift.yaml

diff:
@@ -4,12 +4,16 @@ on:
push:
branches:
- trunk
- - '!gh-pages'
paths:
- '*.podspec'
- 'Gemfile*'
- 'Package.swift'
- - './**'
+ - 'Samples/**'
+ - 'Tooling/**'
+ - 'Workflow/**'
+ - 'WorkflowSwiftUI/**'
+ - 'WorkflowTesting/**'
+ - 'WorkflowUI/**'
- '.github/workflows/swift.yaml'
- '.buildscript/build_swift_docs.sh'
pull_request:
@@ -17,7 +21,12 @@ on:
- '*.podspec'
- 'Gemfile*'
- 'Package.swift'
- - './**'
+ - 'Samples/**'
+ - 'Tooling/**'
+ - 'Workflow/**'
+ - 'WorkflowSwiftUI/**'
+ - 'WorkflowTesting/**'
+ - 'WorkflowUI/**'
- '.github/workflows/swift.yaml'
- '.buildscript/build_swift_docs.sh'

original_message: Update paths in swift.yaml


message: Remove validate-documentation action

diff:
-name: Validate documentation site
-
-on:
- pull_request:
- paths:
- # Rebuild when workflow configs change.
- - .github/workflows/validate-documentation.yml
- # Or when documentation code changes.
- - 'docs/**'
- - '**.md'
- - mkdocs.yml
- - lint_docs.sh
- - .markdownlint.rb
-
-jobs:
- mkdocs:
- name: Build mkdocs to validate mkdocs.yml
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - name: Set up Python
- uses: actions/setup-python@v1
- with:
- python-version: 3.6
- - name: Upgrade pip
- run: python -m pip install --upgrade pip
- - name: Install dependencies
- run: pip install -r requirements.txt
- - name: Run mkdocs
- run: mkdocs build
-
- lint:
- name: Lint Markdown files
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - name: Set up Ruby 2.6
- uses: actions/setup-ruby@v1
- with:
- ruby-version: 2.6
- - name: Install dependencies
- run: gem install mdl
- - name: Lint docs
- run: ./lint_docs.sh

original_message: Remove validate-documentation action


message: Update SwiftPM tag in README

diff:
@@ -28,7 +28,7 @@ in `Package.swift`:
```swift
dependencies: [
- .package(url: "git@github.com:square/workflow-swift.git", from: "0.21.1")
+ .package(url: "git@github.com:square/workflow-swift.git", from: "0.28.1")
]
```

original_message: Update SwiftPM tag in README


message: Rename trunk -> main

diff:
@@ -12,7 +12,7 @@ env:
PREFIX_FOR_TEST: ${{ github.event.client_payload.test_prefix }}
jobs:
- bump-trunk:
+ bump-main:
runs-on: macos-latest
steps:
@@ -25,16 +25,16 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- - name: Checkout Trunk
+ - name: Checkout Main
uses: actions/checkout@v2
with:
- ref: ${{ env.PREFIX_FOR_TEST }}trunk
- path: trunk
+ ref: ${{ env.PREFIX_FOR_TEST }}main
+ path: main
- name: Setup Release Branch (major, minor)
if: env.RELEASE_TYPE == 'major' || env.RELEASE_TYPE == 'minor'
run: |
- cp -R trunk release
+ cp -R main release
cd release
git checkout -b ${{ env.RELEASE_BRANCH }}
@@ -47,23 +47,23 @@ jobs:
- name: Update Changelog
run: |
- cd trunk
+ cd main
../.buildscript/update_changelog.swift
cd ../release
../.buildscript/update_changelog.swift
- - name: Update Trunk Version (major, minor)
+ - name: Update Main Version (major, minor)
if: env.RELEASE_TYPE == 'major' || env.RELEASE_TYPE == 'minor'
run: |
- cd trunk
+ cd main
ls Workflow*.podspec | xargs sed -i '' -e "s/ s.version\( *=\).*/ s.version\1 '${{ env.WORKFLOW_VERSION }}'/"
- - name: Push changes to trunk
+ - name: Push changes to main
env:
GIT_USERNAME: ${{ github.actor }}
GIT_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
run: |
- cd trunk
+ cd main
git add -A . && git commit -m "Releasing ${{ env.WORKFLOW_VERSION }}" && git push -f
- name: Push Release Branch

diff:
@@ -3,7 +3,7 @@ name: Swift CI
on:
push:
branches:
- - trunk
+ - main
paths:
- '*.podspec'
- 'Gemfile*'

diff:
@@ -13,7 +13,7 @@ and assign it to @bencochran or @aquageek.*
---
1. Merge an update of [the change log](CHANGELOG.md) with the changes since the last release.
-1. Make sure you're on the `trunk` branch (or fix branch, e.g. `v0.1-fixes`).
+1. Make sure you're on the `main` branch (or fix branch, e.g. `v0.1-fixes`).
1. Create a commit and tag the commit with the version number:
```bash
@@ -38,7 +38,7 @@ and assign it to @bencochran or @aquageek.*
1. Push your commits and tag:
```
- git push origin trunk
+ git push origin main
# or git push origin fix-branch
git push origin v0.1.0
```
@@ -53,16 +53,16 @@ and assign it to @bencochran or @aquageek.*
1. If this is a pre-release version, check the pre-release box.
1. Hit "Publish release".
-1. If this was a fix release, merge changes to the trunk branch:
+1. If this was a fix release, merge changes to the main branch:
```bash
- git checkout trunk
+ git checkout main
git pull
git merge --no-ff v0.1-fixes
- # Resolve conflicts. Accept trunk's versions of gradle.properties and podspecs.
- git push origin trunk
+ # Resolve conflicts. Accept main's versions of gradle.properties and podspecs.
+ git push origin main
```
-1. Publish the website. See https://github.com/square/workflow/blob/trunk/RELEASING.md.
+1. Publish the website. See https://github.com/square/workflow/blob/main/RELEASING.md.
### Validating Markdown

original_message: Rename trunk -> main


message: Swap generic parameters in ContainerViewController

diff:
import Workflow
/// Drives view controllers from a root Workflow.
- public final class ContainerViewController<Output, ScreenType>: UIViewController where ScreenType: Screen {
+ public final class ContainerViewController<ScreenType, Output>: UIViewController where ScreenType: Screen {
/// Emits output events from the bound workflow.
public var output: Signal<Output, Never> {
return workflowHost.output
var wrapped: AnyWorkflow<Rendering, Output>
- init<W: AnyWorkflowConvertible>(_ wrapped: W) where W.Output == Output, W.Rendering == Rendering {
+ init<W: AnyWorkflowConvertible>(_ wrapped: W) where W.Rendering == Rendering, W.Output == Output {
self.wrapped = wrapped.asAnyWorkflow()
}


message: Remove deprecated SignalWorker

diff:
@@ -40,31 +40,3 @@ extension Signal: AnyWorkflowConvertible where Error == Never {
return SignalProducerWorkflow(signalProducer: SignalProducer(self)).asAnyWorkflow()
}
}
-
-/// A `Worker` that wraps a `Signal`
-@available(*, deprecated, message: "Use `Signal` as `Workflow` instead")
-public struct SignalWorker<Key: Equatable, Value>: Worker {
- let key: Key
- let signal: Signal<Value, Never>
-
- public init(key: Key, signal: Signal<Value, Never>) {
- self.key = key
- self.signal = signal
- }
-
- public func run() -> SignalProducer<Value, Never> {
- return SignalProducer(signal)
- }
-
- public func isEquivalent(to otherWorker: SignalWorker) -> Bool {
- return key == otherWorker.key
- }
-}
-
-extension Signal where Error == Never {
- @available(*, deprecated)
- @available(*, unavailable, message: "Use `Signal` as `Workflow` instead")
- public func asWorker<Key: Equatable>(key: Key) -> SignalWorker<Key, Value> {
- return SignalWorker(key: key, signal: self)
- }
-}

original_message: Remove deprecated SignalWorker


message: Remove deprecated context.awaitResult

diff:
@@ -157,7 +157,8 @@ extension DemoWorkflow {
refreshText = "Loading..."
refreshEnabled = false
- context.awaitResult(for: RefreshWorker()) { output -> Action in
+ RefreshWorker()
+ .mapOutput { output -> Action in
switch output {
case .success(let result):
return .refreshComplete(result)
@@ -165,6 +166,7 @@ extension DemoWorkflow {
return .refreshError(error)
}
}
+ .running(in: context)
}
let subscribeTitle: String

diff:
-/*
- * Copyright 2020 Square Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import Foundation
-import Workflow
-
-extension RenderContext {
- @available(*, deprecated, message: "Use `Worker().running(in:, outputMap:)` instead.")
- public func awaitResult<W, Action>(for worker: W, outputMap: @escaping (W.Output) -> Action) where W: Worker, Action: WorkflowAction, WorkflowType == Action.WorkflowType {
- worker
- .mapOutput { outputMap($0) }
- .running(in: self)
- }
-
- @available(*, deprecated, message: "Use `Worker().running(in:)` instead.")
- public func awaitResult<W>(for worker: W) where W: Worker, W.Output: WorkflowAction, WorkflowType == W.Output.WorkflowType {
- awaitResult(for: worker, outputMap: { $0 })
- }
-
- @available(*, deprecated, message: "Use `Worker().running(in:)` instead.")
- public func awaitResult<W>(for worker: W, onOutput: @escaping (W.Output, inout WorkflowType.State) -> WorkflowType.Output?) where W: Worker {
- awaitResult(for: worker) { output in
- AnyWorkflowAction<WorkflowType> { state in
- onOutput(output, &state)
- }
- }
- }
-}

original_message: Remove deprecated context.awaitResult


message: Remove deprecated rendered(with:key:)

diff:
@@ -84,26 +84,7 @@ extension AnyWorkflowConvertible where Rendering == Void, Output: WorkflowAction
}
}
-// MARK: - Deprecated
-
-extension AnyWorkflowConvertible {
- @available(*, deprecated, renamed: "rendered(in:key:)")
- public func rendered<Parent>(with context: RenderContext<Parent>, key: String = "") -> Rendering where Output: WorkflowAction, Output.WorkflowType == Parent {
- return rendered(in: context, key: key)
- }
-
- @available(*, deprecated, renamed: "rendered(in:key:)")
- public func rendered<Parent>(with context: RenderContext<Parent>, key: String = "") -> Rendering where Output == AnyWorkflowAction<Parent> {
- return rendered(in: context, key: key)
- }
-}
-
-extension AnyWorkflowConvertible where Output == Never {
- @available(*, deprecated, renamed: "rendered(in:key:)")
- public func rendered<T>(with context: RenderContext<T>, key: String = "") -> Rendering {
- return rendered(in: context, key: key)
- }
-}
+// MARK: -
extension AnyWorkflowConvertible {
/// Process an `Output`

original_message: Remove deprecated rendered(with:key:)


message: Make RenderContext.render(workflow:key:outputMap:) internal

diff:
@@ -63,8 +63,7 @@ public class RenderContext<WorkflowType: Workflow>: RenderContextType {
/// - Parameter key: A string that uniquely identifies this child.
///
/// - Returns: The `Rendering` result of the child's `render` method.
- @available(*, deprecated, message: "Use `rendered(in:outputMap:)` on Workflow itself.") // To remove this deprecation, mark this method `internal`
- public func render<Child, Action>(workflow: Child, key: String, outputMap: @escaping (Child.Output) -> Action) -> Child.Rendering where Child: Workflow, Action: WorkflowAction, WorkflowType == Action.WorkflowType {
+ func render<Child, Action>(workflow: Child, key: String, outputMap: @escaping (Child.Output) -> Action) -> Child.Rendering where Child: Workflow, Action: WorkflowAction, WorkflowType == Action.WorkflowType {
fatalError()
}


message: Delete the deprecated test APIs/helper types

diff:
-/*
- * Copyright 2020 Square Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#if DEBUG
- import Foundation
- import WorkflowTesting
- import XCTest
- @testable import WorkflowReactiveSwift
-
- @available(*, deprecated, message: "See `RenderTester` documentation for new style.")
- public struct ExpectedWorker {
- let worker: Any
- let expectedWorkflow: ExpectedWorkflow
-
- private let output: Any?
-
- public init<WorkerType: Worker>(
- worker: WorkerType,
- output: WorkerType.Output? = nil,
- file: StaticString = #file, line: UInt = #line
- ) {
- self.worker = worker
- self.output = output
-
- self.expectedWorkflow = ExpectedWorkflow(
- type: WorkerWorkflow<WorkerType>.self,
- key: "",
- rendering: (),
- output: output
- ) { workflow in
- guard !workflow.worker.isEquivalent(to: worker) else {
- return
- }
- XCTFail(
- "Expected worker of type: \(WorkerType.self) not equivalent",
- file: file,
- line: line
- )
- }
- }
-
- func isEquivalent<WorkerType: Worker>(to actual: WorkerType) -> Bool {
- guard let expectedWorker = worker as? WorkerType else {
- return false
- }
-
- return expectedWorker.isEquivalent(to: actual)
- }
- }
-
- @available(*, deprecated, message: "See `RenderTester` documentation for new style.")
- public extension RenderExpectations {
- init(
- expectedState: ExpectedState<WorkflowType>? = nil,
- expectedOutput: ExpectedOutput<WorkflowType>? = nil,
- expectedWorkers: [ExpectedWorker] = [],
- expectedWorkflows: [ExpectedWorkflow] = [],
- expectedSideEffects: [ExpectedSideEffect<WorkflowType>] = []
- ) {
- self.init(
- expectedState: expectedState,
- expectedOutput: expectedOutput,
- expectedWorkflows: expectedWorkflows + expectedWorkers.map { $0.expectedWorkflow },
- expectedSideEffects: expectedSideEffects
- )
- }
- }
-
- public extension RenderTester {
- @discardableResult
- @available(*, deprecated, message: "See `RenderTester` documentation for new style.")
- func render(
- file: StaticString = #file, line: UInt = #line,
- expectedState: ExpectedState<WorkflowType>? = nil,
- expectedOutput: ExpectedOutput<WorkflowType>? = nil,
- expectedWorkers: [ExpectedWorker] = [],
- expectedWorkflows: [ExpectedWorkflow] = [],
- expectedSideEffects: [ExpectedSideEffect<WorkflowType>] = [],
- assertions: (WorkflowType.Rendering) -> Void = { _ in }
- ) -> RenderTester<WorkflowType> {
- let expectations = RenderExpectations(
- expectedState: expectedState,
- expectedOutput: expectedOutput,
- expectedWorkers: expectedWorkers,
- expectedWorkflows: expectedWorkflows,
- expectedSideEffects: expectedSideEffects
- )
-
- return render(file: file, line: line, with: expectations, assertions: assertions)
- }
- }
-#endif

diff:
@@ -55,25 +55,6 @@ class WorkerTests: XCTestCase {
disposable?.dispose()
}
- @available(*, deprecated) // Marked to silence deprecation warnings
- func testExpectedWorkerDeprecatedTests() {
- SignalProducerTestWorkflow(key: "")
- .renderTester()
- .render(
- expectedState: ExpectedState(state: 1),
- expectedWorkflows: [
- ExpectedWorkflow(
- type: WorkerWorkflow<SignalProducerTestWorker>.self,
- key: "",
- rendering: (),
- output: 1,
- assertions: { _ in }
- ),
- ],
- assertions: { _ in }
- )
- }
-
// A worker declared on a first `render` pass that is not on a subsequent should have the work cancelled.
func test_cancelsWorkers() {
struct WorkerWorkflow: Workflow {

diff:
-/*
- * Copyright 2020 Square Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#if DEBUG
-
- import Workflow
- import XCTest
-
- /// A set of expectations for use with the `WorkflowRenderTester`. All of the expectations must be fulfilled
- /// for a `render` test to pass.
- @available(*, deprecated, message: "See `RenderTester` documentation for new style.")
- public struct RenderExpectations<WorkflowType: Workflow> {
- var expectedState: ExpectedState<WorkflowType>?
- var expectedOutput: ExpectedOutput<WorkflowType>?
- var expectedWorkflows: [ExpectedWorkflow]
- var expectedSideEffects: [AnyHashable: ExpectedSideEffect<WorkflowType>]
-
- public init(
- expectedState: ExpectedState<WorkflowType>? = nil,
- expectedOutput: ExpectedOutput<WorkflowType>? = nil,
- expectedWorkflows: [ExpectedWorkflow] = [],
- expectedSideEffects: [ExpectedSideEffect<WorkflowType>] = []
- ) {
- self.expectedState = expectedState
- self.expectedOutput = expectedOutput
- self.expectedWorkflows = expectedWorkflows
- self.expectedSideEffects = expectedSideEffects.reduce(into: [AnyHashable: ExpectedSideEffect<WorkflowType>]()) { res, expectedSideEffect in
- res[expectedSideEffect.storage.key] = expectedSideEffect
- }
- }
- }
-
- @available(*, deprecated, message: "See `RenderTester` documentation for new style.")
- public struct ExpectedOutput<WorkflowType: Workflow> {
- let output: WorkflowType.Output
- let isEquivalent: (WorkflowType.Output, WorkflowType.Output) -> Bool
-
- public init<Output>(output: Output, isEquivalent: @escaping (Output, Output) -> Bool) where Output == WorkflowType.Output {
- self.output = output
- self.isEquivalent = isEquivalent
- }
-
- public init<Output>(output: Output) where Output == WorkflowType.Output, Output: Equatable {
- self.init(output: output, isEquivalent: { expected, actual in
- expected == actual
- })
- }
-
- func verify(in result: RenderTesterResult<WorkflowType>, file: StaticString, line: UInt) {
- result.verifyOutput(file: file, line: line) {
- XCTAssert(isEquivalent(output, $0), "Output \($0) is not equal to expected \(output)", file: file, line: line)
- }
- }
- }
-
- @available(*, deprecated, message: "See `RenderTester` documentation for new style.")
- public struct ExpectedState<WorkflowType: Workflow> {
- let state: WorkflowType.State
- let isEquivalent: (WorkflowType.State, WorkflowType.State) -> Bool
-
- /// Create a new expected state from a state with an equivalence block. `isEquivalent` will be
- /// called to validate that the expected state matches the actual state after a render pass.
- public init<State>(state: State, isEquivalent: @escaping (State, State) -> Bool) where State == WorkflowType.State {
- self.state = state
- self.isEquivalent = isEquivalent
- }
-
- public init<State>(state: State) where WorkflowType.State == State, State: Equatable {
- self.init(state: state, isEquivalent: { expected, actual in
- expected == actual
- })
- }
-
- func verify(in result: RenderTesterResult<WorkflowType>, file: StaticString, line: UInt) {
- result.verifyState(file: file, line: line) {
- XCTAssert(isEquivalent(state, $0), "State \($0) is not equal to expected \(state)", file: file, line: line)
- }
- }
- }
-
- public struct ExpectedSideEffect<WorkflowType: Workflow> {
- fileprivate class Storage {
- let key: AnyHashable
-
- init(key: AnyHashable) {
- self.key = key
- }
-
- func expect(in tester: inout RenderTester<WorkflowType>, file: StaticString, line: UInt) {
- tester = tester.expectSideEffect(key: key, file: file, line: line)
- }
- }
-
- private final class StorageWithAction<ActionType: WorkflowAction>: Storage where ActionType.WorkflowType == WorkflowType {
- let action: ActionType
-
- init(key: AnyHashable, action: ActionType) {
- self.action = action
- super.init(key: key)
- }
-
- override func expect(in tester: inout RenderTester<WorkflowType>, file: StaticString, line: UInt) {
- tester = tester.expectSideEffect(key: key, producingAction: action, file: file, line: line)
- }
- }
-
- fileprivate let storage: Storage
-
- public init(key: AnyHashable) {
- self.storage = Storage(key: key)
- }
-
- public init<ActionType: WorkflowAction>(key: AnyHashable, action: ActionType) where ActionType.WorkflowType == WorkflowType {
- self.storage = StorageWithAction(key: key, action: action)
- }
-
- func expect(in tester: inout RenderTester<WorkflowType>, file: StaticString, line: UInt) {
- storage.expect(in: &tester, file: file, line: line)
- }
- }
-
- @available(*, deprecated, message: "See `RenderTester` documentation for new style.")
- public struct ExpectedWorkflow {
- fileprivate class AnyStorage {
- func expect<ParentWorkflowType: Workflow>(in tester: inout RenderTester<ParentWorkflowType>, file: StaticString, line: UInt) {
- fatalError()
- }
- }
-
- private final class Storage<ExpectedWorkflowType: Workflow>: AnyStorage {
- let key: String
- let rendering: ExpectedWorkflowType.Rendering
- let output: ExpectedWorkflowType.Output?
- let assertions: (ExpectedWorkflowType) -> Void
-
- init(key: String, rendering: ExpectedWorkflowType.Rendering, output: ExpectedWorkflowType.Output?, assertions: @escaping (ExpectedWorkflowType) -> Void) {
- self.key = key
- self.rendering = rendering
- self.output = output
- self.assertions = assertions
- }
-
- override func expect<WorkflowType: Workflow>(in tester: inout RenderTester<WorkflowType>, file: StaticString, line: UInt) {
- tester = tester.expectWorkflow(type: ExpectedWorkflowType.self, key: key, producingRendering: rendering, producingOutput: output, file: file, line: line)
- }
- }
-
- fileprivate let storage: AnyStorage
-
- public init<WorkflowType: Workflow>(type: WorkflowType.Type, key: String = "", rendering: WorkflowType.Rendering, output: WorkflowType.Output? = nil, assertions: @escaping (WorkflowType) -> Void = { _ in }) {
- self.storage = Storage<WorkflowType>(key: key, rendering: rendering, output: output, assertions: assertions)
- }
-
- func expect<ParentWorkflowType: Workflow>(in tester: inout RenderTester<ParentWorkflowType>, file: StaticString, line: UInt) {
- storage.expect(in: &tester, file: file, line: line)
- }
- }
-
-#endif

diff:
@@ -161,23 +161,3 @@ extension WorkflowActionTester where WorkflowType.Output: Equatable {
}
}
}
-
-extension WorkflowActionTester {
- private func legacyVerifyOutputShim(_ assertions: (WorkflowType.Output?) -> Void) -> WorkflowActionTester<WorkflowType, Action> {
- assertions(output)
- return self
- }
-
- @available(*, deprecated, message: "use `send(action:)` followed by `verifyOutput(_:)`, `verify(output:)` or `assertNoOutput()`")
- @discardableResult
- public func send(action: Action, outputAssertions: (WorkflowType.Output?) -> Void = { _ in }) -> WorkflowActionTester<WorkflowType, Action> {
- return send(action: action)
- .legacyVerifyOutputShim(outputAssertions)
- }
-
- @available(*, deprecated, renamed: "verifyState")
- @discardableResult
- public func assertState(_ assertions: (WorkflowType.State) -> Void) -> WorkflowActionTester<WorkflowType, Action> {
- return verifyState(assertions)
- }
-}

diff:
-/*
- * Copyright 2020 Square Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#if DEBUG
-
- import XCTest
-
- extension RenderTester {
- @available(*, deprecated, message: "See `RenderTester` documentation for new style.")
- @discardableResult
- public func render(file: StaticString = #file, line: UInt = #line, with expectations: RenderExpectations<WorkflowType>, assertions: (WorkflowType.Rendering) -> Void) -> RenderTester<WorkflowType> {
- var tester = self
-
- for expectedWorkflow in expectations.expectedWorkflows {
- expectedWorkflow.expect(in: &tester, file: file, line: line)
- }
-
- for (_, expectedSideEffect) in expectations.expectedSideEffects {
- expectedSideEffect.expect(in: &tester, file: file, line: line)
- }
-
- let result = tester.render(file: file, line: line, assertions: assertions)
-
- if let expectedState = expectations.expectedState {
- expectedState.verify(in: result, file: file, line: line)
- }
-
- if let expectedOutput = expectations.expectedOutput {
- expectedOutput.verify(in: result, file: file, line: line)
- }
-
- return RenderTester(
- workflow: tester.workflow,
- state: result.state
- )
- }
-
- @available(*, deprecated, message: "See `RenderTester` documentation for new style.")
- @discardableResult
- public func render(
- file: StaticString = #file, line: UInt = #line,
- expectedState: ExpectedState<WorkflowType>? = nil,
- expectedOutput: ExpectedOutput<WorkflowType>? = nil,
- expectedWorkflows: [WorkflowTesting.ExpectedWorkflow] = [],
- expectedSideEffects: [WorkflowTesting.ExpectedSideEffect<WorkflowType>] = [],
- assertions: (WorkflowType.Rendering) -> Void
- ) -> RenderTester<WorkflowType> {
- let expectations = RenderExpectations(
- expectedState: expectedState,
- expectedOutput: expectedOutput,
- expectedWorkflows: expectedWorkflows,
- expectedSideEffects: expectedSideEffects
- )
-
- return render(file: file, line: line, with: expectations, assertions: assertions)
- }
-
- @available(*, deprecated, renamed: "verifyState(_:)")
- @discardableResult
- public func assert(state assertions: (WorkflowType.State) -> Void) -> RenderTester<WorkflowType> {
- assertions(state)
- return self
- }
- }
-
-#endif

diff:
-/*
- * Copyright 2020 Square Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import Workflow
-import WorkflowTesting
-import XCTest
-
-@available(*, deprecated) // Marked to silence deprecation warnings
-final class WorkflowRenderTesterDeprecatedTests: XCTestCase {
- func test_assertState() {
- let renderTester = TestWorkflow(initialText: "initial").renderTester()
- var testedAssertion = false
-
- renderTester.assert { state in
- XCTAssertEqual("initial", state.text)
- XCTAssertEqual(.idle, state.substate)
- testedAssertion = true
- }
- XCTAssertTrue(testedAssertion)
- }
-
- func test_render() {
- let renderTester = TestWorkflow(initialText: "initial").renderTester()
- var testedAssertion = false
-
- renderTester.render(
- with: RenderExpectations(
- expectedState: ExpectedState(
- state: TestWorkflow.State(
- text: "initial",
- substate: .idle
- )
- )
- ),
- assertions: { screen in
- XCTAssertEqual("initial", screen.text)
- testedAssertion = true
- }
- )
- XCTAssertTrue(testedAssertion)
- }
-
- func test_simple_render() {
- let renderTester = TestWorkflow(initialText: "initial").renderTester()
-
- renderTester.render { screen in
- XCTAssertEqual("initial", screen.text)
- }
- }
-
- func test_action() {
- let renderTester = TestWorkflow(initialText: "initial").renderTester()
-
- renderTester.render(
- with: RenderExpectations(
- expectedState: ExpectedState(
- state: TestWorkflow.State(
- text: "initial",
- substate: .waiting
- )
- )
- ),
- assertions: { screen in
- XCTAssertEqual("initial", screen.text)
- screen.tapped()
- }
- )
- }
-
- func test_sideEffects() {
- let renderTester = SideEffectWorkflow().renderTester()
-
- renderTester.render(
- with: RenderExpectations(
- expectedState: ExpectedState(state: .success),
- expectedSideEffects: [
- ExpectedSideEffect(key: TestSideEffectKey(), action: SideEffectWorkflow.Action.testAction),
- ]
- ),
- assertions: { _ in }
- )
- }
-
- func test_output() {
- OutputWorkflow()
- .renderTester()
- .render(
- with: RenderExpectations(
- expectedOutput: ExpectedOutput(output: .success)
- ),
- assertions: { rendering in
- rendering.tapped()
- }
- )
- }
-
- func test_childWorkflow() {
- // Test the child independently from the parent.
- ChildWorkflow(text: "hello")
- .renderTester()
- .render(
- assertions: { rendering in
- XCTAssertEqual("olleh", rendering)
- }
- )
-
- // Test the parent simulating the behavior of the child. The worker would run, but because the child is simulated, does not run.
- ParentWorkflow(initialText: "hello")
- .renderTester()
- .render(
- with: RenderExpectations<ParentWorkflow>(expectedWorkflows: [
- ExpectedWorkflow(
- type: ChildWorkflow.self,
- rendering: "olleh",
- output: nil
- ),
- ]),
- assertions: { rendering in
- XCTAssertEqual("olleh", rendering)
- }
- )
- }
-
- func test_childWorkflowOutput() {
- // Test that a child emitting an output is handled as an action by the parent
- ParentWorkflow(initialText: "hello")
- .renderTester()
- .render(
- expectedState: ExpectedState(state: ParentWorkflow.State(text: "Failed")),
- expectedWorkflows: [
- ExpectedWorkflow(
- type: ChildWorkflow.self,
- rendering: "olleh",
- output: .failure
- ),
- ],
- assertions: { rendering in
- XCTAssertEqual("olleh", rendering)
- }
- )
- .assert { state in
- XCTAssertEqual("Failed", state.text)
- }
- }
-
- func test_impplict_expectations() {
- TestWorkflow(initialText: "hello")
- .renderTester()
- .render(
- expectedState: ExpectedState<TestWorkflow>(
- state: TestWorkflow.State(
- text: "hello",
- substate: .idle
- )
- ),
- expectedOutput: nil,
- expectedWorkflows: [],
- assertions: { rendering in
- XCTAssertEqual("hello", rendering.text)
- }
- )
- }
-
- func test_multiple_renders() {
- ParentWorkflow(initialText: "hello")
- .renderTester()
- .render(
- expectedState: ExpectedState(
- state: ParentWorkflow.State(text: "Failed")
- ),
- expectedWorkflows: [
- ExpectedWorkflow(
- type: ChildWorkflow.self,
- rendering: "olleh",
- output: .failure
- ),
- ],
- assertions: { rendering in
- XCTAssertEqual("olleh", rendering)
- }
- )
- .assert { state in
- XCTAssertEqual("Failed", state.text)
- }
- .render(
- expectedState: ExpectedState(
- state: ParentWorkflow.State(text: "deliaF")
- ),
- expectedWorkflows: [
- ExpectedWorkflow(
- type: ChildWorkflow.self,
- rendering: "olleh",
- output: .success
- ),
- ],
- assertions: { rendering in
- XCTAssertEqual("olleh", rendering)
- }
- )
- }
-}
-
-private struct TestWorkflow: Workflow {
- /// Input
- var initialText: String
-
- /// Output
- enum Output: Equatable {
- case first
- }
-
- struct State: Equatable {
- var text: String
- var substate: Substate
- enum Substate: Equatable {
- case idle
- case waiting
- }
- }
-
- func makeInitialState() -> State {
- State(text: initialText, substate: .idle)
- }
-
- func render(state: State, context: RenderContext<TestWorkflow>) -> TestScreen {
- let sink = context.makeSink(of: Action.self)
-
- switch state.substate {
- case .idle:
- break
- case .waiting:
- context.runSideEffect(key: "") { lifetime in
- sink.send(Action.asyncSuccess)
- }
- }
-
- return TestScreen(
- text: state.text,
- tapped: {
- sink.send(.tapped)
- }
- )
- }
-}
-
-extension TestWorkflow {
- enum Action: WorkflowAction, Equatable {
- typealias WorkflowType = TestWorkflow
-
- case tapped
- case asyncSuccess
-
- func apply(toState state: inout TestWorkflow.State) -> TestWorkflow.Output? {
- switch self {
- case .tapped:
- state.substate = .waiting
-
- case .asyncSuccess:
- state.substate = .idle
- }
- return nil
- }
- }
-}
-
-private struct OutputWorkflow: Workflow {
- enum Output {
- case success
- case failure
- }
-
- struct State {}
-
- func makeInitialState() -> OutputWorkflow.State {
- State()
- }
-
- enum Action: WorkflowAction {
- typealias WorkflowType = OutputWorkflow
-
- case emit
-
- func apply(toState state: inout OutputWorkflow.State) -> OutputWorkflow.Output? {
- switch self {
- case .emit:
- return .success
- }
- }
- }
-
- typealias Rendering = TestScreen
-
- func render(state: State, context: RenderContext<OutputWorkflow>) -> TestScreen {
- let sink = context.makeSink(of: Action.self)
-
- return TestScreen(text: "value", tapped: {
- sink.send(.emit)
- })
- }
-}
-
-private struct TestSideEffectKey: Hashable {
- let key: String = "Test Side Effect"
-}
-
-private struct SideEffectWorkflow: Workflow {
- enum State: Equatable {
- case idle
- case success
- }
-
- enum Action: WorkflowAction {
- case testAction
-
- typealias WorkflowType = SideEffectWorkflow
-
- func apply(toState state: inout SideEffectWorkflow.State) -> SideEffectWorkflow.Output? {
- switch self {
- case .testAction:
- state = .success
- }
- return nil
- }
- }
-
- typealias Rendering = TestScreen
-
- func render(state: State, context: RenderContext<SideEffectWorkflow>) -> TestScreen {
- context.runSideEffect(key: TestSideEffectKey()) { _ in }
-
- return TestScreen(text: "value", tapped: {})
- }
-
- func makeInitialState() -> State {
- .idle
- }
-}
-
-private struct TestScreen {
- var text: String
- var tapped: () -> Void
-}
-
-private struct ParentWorkflow: Workflow {
- typealias Output = Never
-
- var initialText: String
-
- struct State: Equatable {
- var text: String
- }
-
- func makeInitialState() -> ParentWorkflow.State {
- State(text: initialText)
- }
-
- enum Action: WorkflowAction {
- typealias WorkflowType = ParentWorkflow
-
- case childSuccess
- case childFailure
-
- func apply(toState state: inout ParentWorkflow.State) -> Never? {
- switch self {
- case .childSuccess:
- state.text = String(state.text.reversed())
-
- case .childFailure:
- state.text = "Failed"
- }
-
- return nil
- }
- }
-
- func render(state: ParentWorkflow.State, context: RenderContext<ParentWorkflow>) -> String {
- ChildWorkflow(text: state.text)
- .mapOutput { output -> Action in
- switch output {
- case .success:
- return .childSuccess
- case .failure:
- return .childFailure
- }
- }
- .rendered(in: context)
- }
-}
-
-private struct ChildWorkflow: Workflow {
- enum Output: Equatable {
- case success
- case failure
- }
-
- var text: String
-
- struct State {}
-
- func makeInitialState() -> ChildWorkflow.State {
- State()
- }
-
- enum Action: WorkflowAction {
- case outputSuccess
-
- typealias WorkflowType = ChildWorkflow
-
- func apply(toState state: inout ChildWorkflow.State) -> ChildWorkflow.Output? {
- switch self {
- case .outputSuccess:
- return .success
- }
- }
- }
-
- func render(state: ChildWorkflow.State, context: RenderContext<ChildWorkflow>) -> String {
- String(text.reversed())
- }
-}


message: Update Changelog for beta 1

diff:
Change Log
==========
+## Version 1.0.0-beta.1
+
+_2020-09-11_
+
+* Add convenience `onOutput` transformer for `AnyWorkflow` (#59)
+* Remove the APIs that were deprecated in the Workflow 1.0.0 alpha (#62, #63, #64, #65, #66). See the [migration guide](/MigrationGuide_v1.0.md) for more info.
+
## Version 1.0.0-alpha.6
-_2020-09-22_
+_2020-09-02_
* Lowering RxSwift version requirement from 5.1.1 to 4.4.0 (#60)

original_message: Update Changelog for beta 1


message:
Update `Workflow.render` documentation
Fixes This function is no longer public.

diff:
@@ -73,14 +73,13 @@ public protocol Workflow: AnyWorkflowConvertible {
/// - Parameter state: The current state.
func workflowDidChange(from previousWorkflow: Self, state: inout State)
- /// Called to "render" the current state into `Rendering`. A workflow's `Rendering` type is commonly a view or
- /// screen model.
+ /// Called by the internal Workflow infrastructure to "render" the current state into `Rendering`.
+ /// A workflow's `Rendering` type is commonly a view or screen model.
///
/// - Parameter state: The current state.
/// - Parameter context: The workflow context is the composition point for the workflow tree. To use a nested
- /// workflow, instantiate it based on the current state. The newly instantiated workflow is
- /// then used to invoke `context.render(_ workflow:)`, which returns the child's `Rendering`
- /// type after creating or updating the nested workflow.
+ /// workflow, instantiate it based on the current state, then call `rendered(in:key:outputMap:)`.
+ /// This will return the child's `Rendering` type after creating or updating the nested workflow.
func render(state: State, context: RenderContext<Self>) -> Rendering
}

original_message:
Update `Workflow.render` documentation

Fixes #68. This function is no longer public.


message: Update tutorial README

diff:
@@ -8,17 +8,32 @@ Oh hi! Looks like you want build some software with Workflows! It's a bit differ
The project has both a starting point, as well as an example of the completed tutorial.
-Nearly all of the software is in cocoapods under the `Frameworks` directory.
+Nearly all of the code is in the `Frameworks` directory.
-To help with the setup, there are already created a few helpers:
+To help with the setup, we have created a few helpers:
- `TutorialViews`: A set of 3 views for the 3 screens we will be building, `Welcome`, `TodoList`, and `TodoEdit`.
- `TutorialBase`: This is the starting point to build out the tutorial. It contains view controllers that host the views from `TutorialViews` to see how they display.
- Additionally, there is a `TutorialContainerViewController` that the AppDelegate sets as the root view controller. This will be our launching point for all of our workflows.
- `TutorialFinal`: This is an example of the completed tutorial - could be used as a reference if you get stuck.
-## Getting up and running
+## Getting started
-The tutorial uses cocoapods as the dependency management. To get set up, run `bundle install`, then `bundle exec pod install`. Open `Tutorial.xcworkspace`.
+The tutorial uses [CocoaPods](https://guides.cocoapods.org/using/index.html) for dependency management. To get set up, run the following:
+
+```sh
+$ cd Samples/Tutorial
+$ bundle install
+...
+Bundle complete!
+$ bundle exec pod install
+Analyzing dependencies
+Downloading dependencies
+Generating Pods project
+Integrating client project
+Pod installation complete!
+```
+
+Then open `Tutorial.xcworkspace` in Xcode.
# Tutorial Steps

original_message: Update tutorial README


message: Bump the deployment target to iOS 11 / macOS High Sierra

diff:
@@ -6,8 +6,8 @@ import PackageDescription
let package = Package(
name: "Workflow",
platforms: [
- .iOS("10.0"),
- .macOS("10.12"),
+ .iOS("11.0"),
+ .macOS("10.13"),
],
products: [
.library(

diff:
@@ -11,7 +11,7 @@ Pod::Spec.new do |s|
s.cocoapods_version = '>= 1.7.0'
s.swift_versions = ['5.0']
- s.ios.deployment_target = '10.0'
+ s.ios.deployment_target = '11.0'
s.source_files = 'Sources/**/*.swift'

diff:
@@ -11,7 +11,7 @@ Pod::Spec.new do |s|
s.cocoapods_version = '>= 1.7.0'
s.swift_versions = ['5.0']
- s.ios.deployment_target = '10.0'
+ s.ios.deployment_target = '11.0'
s.source_files = 'Sources/**/*.{swift}'

diff:
@@ -13,8 +13,8 @@ Pod::Spec.new do |s|
s.cocoapods_version = '>= 1.7.0'
s.swift_versions = ['5.0']
- s.ios.deployment_target = '10.0'
- s.osx.deployment_target = '10.12'
+ s.ios.deployment_target = '11.0'
+ s.osx.deployment_target = '10.13'
s.source_files = 'Workflow/Sources/*.swift'

diff:
@@ -13,8 +13,8 @@ Pod::Spec.new do |s|
s.cocoapods_version = '>= 1.7.0'
s.swift_versions = ['5.0']
- s.ios.deployment_target = '10.0'
- s.osx.deployment_target = '10.12'
+ s.ios.deployment_target = '11.0'
+ s.osx.deployment_target = '10.13'
s.source_files = 'WorkflowReactiveSwift/Sources/**/*.swift'

diff:
@@ -13,8 +13,8 @@ Pod::Spec.new do |s|
s.cocoapods_version = '>= 1.7.0'
s.swift_versions = ['5.0']
- s.ios.deployment_target = '10.0'
- s.osx.deployment_target = '10.12'
+ s.ios.deployment_target = '11.0'
+ s.osx.deployment_target = '10.13'
s.source_files = 'WorkflowReactiveSwift/Testing/**/*.swift'

diff:
@@ -13,8 +13,8 @@ Pod::Spec.new do |s|
s.cocoapods_version = '>= 1.7.0'
s.swift_versions = ['5.0']
- s.ios.deployment_target = '10.0'
- s.osx.deployment_target = '10.12'
+ s.ios.deployment_target = '11.0'
+ s.osx.deployment_target = '10.13'
s.source_files = 'WorkflowRxSwift/Sources/**/*.swift'

diff:
@@ -13,8 +13,8 @@ Pod::Spec.new do |s|
s.cocoapods_version = '>= 1.7.0'
s.swift_versions = ['5.0']
- s.ios.deployment_target = '10.0'
- s.osx.deployment_target = '10.12'
+ s.ios.deployment_target = '11.0'
+ s.osx.deployment_target = '10.13'
s.source_files = 'WorkflowRxSwift/Testing/**/*.swift'

diff:
@@ -13,8 +13,8 @@ Pod::Spec.new do |s|
s.cocoapods_version = '>= 1.7.0'
s.swift_versions = ['5.0']
- s.ios.deployment_target = '10.0'
- s.osx.deployment_target = '10.12'
+ s.ios.deployment_target = '11.0'
+ s.osx.deployment_target = '10.13'
s.source_files = 'WorkflowTesting/Sources/**/*.swift'

diff:
@@ -13,8 +13,8 @@ Pod::Spec.new do |s|
s.cocoapods_version = '>= 1.7.0'
s.swift_versions = ['5.0']
- s.ios.deployment_target = '10.0'
- s.osx.deployment_target = '10.12'
+ s.ios.deployment_target = '11.0'
+ s.osx.deployment_target = '10.13'
s.source_files = 'WorkflowUI/Sources/**/*.swift'


message: Remove iOS 10 availability check

diff:
@@ -308,9 +308,7 @@ extension WorkflowNode.SubtreeManager {
}
func handle(event: Output) {
- if #available(iOS 10.0, *) {
dispatchPrecondition(condition: .onQueue(DispatchQueue.workflowExecution))
- }
switch validationState {
case .preparing:

original_message: Remove iOS 10 availability check


message: Update Tutorial 1

diff:
@@ -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.

diff:
@@ -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
}
}
+

diff:
@@ -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"
}

original_message: Update Tutorial 1


message: Update finished Tutorial 1 to match docs

diff:
@@ -32,12 +32,12 @@ struct WelcomeScreen: Screen {
}
final class WelcomeViewController: ScreenViewController<WelcomeScreen> {
- var welcomeView: WelcomeView
+ private let welcomeView: WelcomeView
required init(screen: WelcomeScreen, environment: ViewEnvironment) {
- self.welcomeView = WelcomeView(frame: .zero)
+ welcomeView = WelcomeView(frame: .zero)
+
super.init(screen: screen, environment: environment)
- update(with: screen)
}
override func viewDidLoad() {
@@ -53,11 +53,9 @@ final class WelcomeViewController: ScreenViewController<WelcomeScreen> {
}
override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) {
- update(with: screen)
- }
+ super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment)
- private func update(with screen: WelcomeScreen) {
- /// Update UI
+ // Update UI
welcomeView.name = screen.name
welcomeView.onNameChanged = screen.onNameChanged
welcomeView.onLoginTapped = screen.onLoginTapped

diff:
@@ -56,10 +56,11 @@ Now add the (convenient) `WelcomeView` to our view controller (if you would like
import TutorialViews
final class WelcomeViewController: ScreenViewController<WelcomeScreen> {
- var welcomeView: WelcomeView
+ private let welcomeView: WelcomeView
required init(screen: WelcomeScreen) {
- self.welcomeView = WelcomeView(frame: .zero)
+ welcomeView = WelcomeView(frame: .zero)
+
super.init(screen: screen)
}


message: Add missing 'environment' argument to VC initializer

diff:
@@ -58,10 +58,10 @@ import TutorialViews
final class WelcomeViewController: ScreenViewController<WelcomeScreen> {
private let welcomeView: WelcomeView
- required init(screen: WelcomeScreen) {
+ required init(screen: WelcomeScreen, environment: ViewEnvironment) {
welcomeView = WelcomeView(frame: .zero)
- super.init(screen: screen)
+ super.init(screen: screen, environment: environment)
}
override func viewDidLoad() {


message: Fix warning message in Workflow Xcode template

diff:
@@ -60,7 +60,7 @@ extension ___VARIABLE_productName___Workflow {
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!")
+ #warning("Don't forget your render implementation and to return the correct rendering type!")
return "This is likely not the rendering that you want to return"
}
}


message: Follow UIKit best practices and defer creation of WelcomeView until it's needed

diff:
@@ -32,17 +32,18 @@ struct WelcomeScreen: Screen {
}
final class WelcomeViewController: ScreenViewController<WelcomeScreen> {
- private let welcomeView: WelcomeView
+ private var welcomeView: WelcomeView!
required init(screen: WelcomeScreen, environment: ViewEnvironment) {
- welcomeView = WelcomeView(frame: .zero)
-
super.init(screen: screen, environment: environment)
}
override func viewDidLoad() {
super.viewDidLoad()
+ welcomeView = WelcomeView(frame: view.bounds)
+ updateView(with: screen)
+
view.addSubview(welcomeView)
}
@@ -55,7 +56,12 @@ final class WelcomeViewController: ScreenViewController<WelcomeScreen> {
override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) {
super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment)
- // Update UI
+ guard isViewLoaded else { return }
+
+ updateView(with: screen)
+ }
+
+ private func updateView(with screen: WelcomeScreen) {
welcomeView.name = screen.name
welcomeView.onNameChanged = screen.onNameChanged
welcomeView.onLoginTapped = screen.onLoginTapped

diff:
@@ -56,17 +56,16 @@ Now add the (convenient) `WelcomeView` to our view controller (if you would like
import TutorialViews
final class WelcomeViewController: ScreenViewController<WelcomeScreen> {
- private let welcomeView: WelcomeView
+ private var welcomeView: WelcomeView!
required init(screen: WelcomeScreen, environment: ViewEnvironment) {
- welcomeView = WelcomeView(frame: .zero)
-
super.init(screen: screen, environment: environment)
}
override func viewDidLoad() {
super.viewDidLoad()
+ welcomeView = WelcomeView(frame: view.bounds)
view.addSubview(welcomeView)
}
@@ -77,12 +76,26 @@ final class WelcomeViewController: ScreenViewController<WelcomeScreen> {
}
```
-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:
+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. Keep in mind, however, that the `welcomeView` will be `nil` until the view is loaded, so we'll need to guard against that to avoid unwrapping the optional. Additionally, when `viewDidLoad` _is_ called and we create the `WelcomeView` we will need to update it with the contents of the `WelcomeScreen`. To accomplish both of these, we'll create a separate helper method that both code paths can utilize:
```swift
+ override viewDidLoad() {
+ super.viewDidLoad()
+
+ welcomeView = WelcomeView(frame: view.bounds)
+ view.addSubview(welcomeView)
+
+ updateView(with: screen)
+ }
+
override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) {
super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment)
- // Update the UI
+ guard isViewLoaded else { return }
+
+ updateView(with: screen)
+ }
+
+ private func updateView(with screen: WelcomeScreen) {
welcomeView.name = screen.name
welcomeView.onNameChanged = screen.onNameChanged
welcomeView.onLoginTapped = screen.onLoginTapped


message: Update Tutorial 3

diff:
@@ -51,18 +51,17 @@ extension RootWorkflow {
enum Action: WorkflowAction {
typealias WorkflowType = RootWorkflow
- case login(name: String)
- case logout
+ case logIn(name: String)
+ case logOut
func apply(toState state: inout RootWorkflow.State) -> RootWorkflow.Output? {
switch self {
- case .login(name: let name):
- // When the `login` action is received, change the state to `todo`.
+ case .logIn(name: let name):
state = .todo(name: name)
- case .logout:
- // Return to the welcome state on logout.
+ case .logOut:
state = .welcome
}
+
return nil
}
}
@@ -98,16 +97,16 @@ extension RootWorkflow {
let welcomeScreen = WelcomeWorkflow()
.mapOutput { output -> Action in
switch output {
- // When `WelcomeWorkflow` emits `didLogin`, turn it into our `login` action.
- case .didLogin(name: let name):
- return .login(name: name)
+ // When `WelcomeWorkflow` emits `didLogIn`, turn it into our `logIn` action.
+ case .didLogIn(name: let name):
+ return .logIn(name: name)
}
}
.rendered(in: context)
let welcomeBackStackItem = BackStackScreen.Item(
key: "welcome",
- screen: welcomeScreen.asAnyScreen(),
+ screen: AnyScreen(welcomeScreen),
// Hide the navigation bar.
barVisibility: .hidden
)
@@ -116,25 +115,25 @@ extension RootWorkflow {
backStackItems.append(welcomeBackStackItem)
switch state {
- // When the state is `.welcome`, defer to the WelcomeWorkflow.
case .welcome:
// We always add the welcome screen to the backstack, so this is a no op.
break
- // When the state is `.todo`, defer to the TodoListWorkflow.
case .todo(name: let name):
+ // When the state is `.todo`, defer to the TodoListWorkflow.
let todoBackStackItems = TodoListWorkflow(name: name)
.mapOutput { output -> Action in
switch output {
case .back:
- // When receiving a `.back` output, treat it as a `.logout` action.
- return .logout
+ // When receiving a `.back` output, treat it as a `.logOut` action.
+ return .logOut
}
}
.rendered(in: context)
- // Add the todoBackStackItems to our BackStackItems.
+ // Add the todoBackStackItems to our backStackItems
+
backStackItems.append(contentsOf: todoBackStackItems)
}

diff:
@@ -34,20 +34,19 @@ struct TodoEditScreen: Screen {
}
final class TodoEditViewController: ScreenViewController<TodoEditScreen> {
- // The `todoEditView` has all the logic for displaying the todo and editing.
- let todoEditView: TodoEditView
+ private var todoEditView: TodoEditView!
required init(screen: TodoEditScreen, environment: ViewEnvironment) {
- self.todoEditView = TodoEditView(frame: .zero)
-
super.init(screen: screen, environment: environment)
- update(with: screen)
}
override func viewDidLoad() {
super.viewDidLoad()
+ todoEditView = TodoEditView(frame: view.bounds)
view.addSubview(todoEditView)
+
+ updateView(with: screen)
}
override func viewDidLayoutSubviews() {
@@ -57,10 +56,14 @@ final class TodoEditViewController: ScreenViewController<TodoEditScreen> {
}
override func screenDidChange(from previousScreen: TodoEditScreen, previousEnvironment: ViewEnvironment) {
- update(with: screen)
+ super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment)
+
+ guard isViewLoaded else { return }
+
+ updateView(with: screen)
}
- private func update(with screen: TodoEditScreen) {
+ private func updateView(with screen: TodoEditScreen) {
// Update the view with the data from the screen.
todoEditView.title = screen.title
todoEditView.note = screen.note

diff:
@@ -140,6 +140,7 @@ extension TodoEditWorkflow {
))
)
)
+
return backStackItem
}
}

diff:
* limitations under the License.
*/
-import TutorialViews
import Workflow
import WorkflowUI
+import TutorialViews
struct TodoListScreen: Screen {
// The titles of the todo items
@@ -31,18 +31,19 @@ struct TodoListScreen: Screen {
}
final class TodoListViewController: ScreenViewController<TodoListScreen> {
- let todoListView: TodoListView
+ private var todoListView: TodoListView!
required init(screen: TodoListScreen, environment: ViewEnvironment) {
- self.todoListView = TodoListView(frame: .zero)
super.init(screen: screen, environment: environment)
- update(with: screen)
}
override func viewDidLoad() {
super.viewDidLoad()
+ todoListView = TodoListView(frame: view.bounds)
view.addSubview(todoListView)
+
+ updateView(with: screen)
}
override func viewDidLayoutSubviews() {
@@ -52,10 +53,14 @@ final class TodoListViewController: ScreenViewController<TodoListScreen> {
}
override func screenDidChange(from previousScreen: TodoListScreen, previousEnvironment: ViewEnvironment) {
- update(with: screen)
+ super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment)
+
+ guard isViewLoaded else { return }
+
+ updateView(with: screen)
}
- private func update(with screen: TodoListScreen) {
+ private func updateView(with screen: TodoListScreen) {
// Update the todoList on the view with what the screen provided:
todoListView.todoList = screen.todoTitles
todoListView.onTodoSelected = screen.onTodoSelected

diff:
@@ -37,9 +37,11 @@ extension TodoListWorkflow {
struct State {
var todos: [TodoModel]
var step: Step
+
enum Step {
// Showing the list of todo items.
case list
+
// Editing a single item. The state holds the index so it can be updated when a save action is received.
case edit(index: Int)
}
@@ -51,7 +53,7 @@ extension TodoListWorkflow {
TodoModel(
title: "Take the cat for a walk",
note: "Cats really need their outside sunshine time. Don't forget to walk Charlie. Hamilton is less excited about the prospect."
- ),
+ )
],
step: .list
)
@@ -90,7 +92,6 @@ extension TodoListWorkflow {
case .saveChanges(todo: let todo, index: let index):
// When changes are saved, update the state of that `todo` item and return to the list.
state.todos[index] = todo
-
state.step = .list
return nil
}
@@ -123,9 +124,8 @@ extension TodoListWorkflow {
// Define a sink to be able to send actions.
let sink = context.makeSink(of: Action.self)
- let titles = state.todos.map { (todoModel) -> String in
- todoModel.title
- }
+ let titles = state.todos.map(\.title)
+
let todoListScreen = TodoListScreen(
todoTitles: titles,
onTodoSelected: { index in
@@ -138,7 +138,7 @@ extension TodoListWorkflow {
key: "list",
screen: todoListScreen.asAnyScreen(),
barContent: .init(
- title: "Welcome \(name)",
+ title: "Welcome, \(name)",
leftItem: .button(.back(handler: {
// When the left button is tapped, send the .onBack action.
sink.send(.onBack)
@@ -154,8 +154,7 @@ extension TodoListWorkflow {
case .edit(index: let index):
// On the "edit" step, return both the list and edit screens.
- let todoEditItem = TodoEditWorkflow(
- initialTodo: state.todos[index])
+ let todoEditItem = TodoEditWorkflow(initialTodo: state.todos[index])
.mapOutput { output -> Action in
switch output {
case .discard:

diff:
@@ -32,17 +32,18 @@ struct WelcomeScreen: Screen {
}
final class WelcomeViewController: ScreenViewController<WelcomeScreen> {
- var welcomeView: WelcomeView
+ private var welcomeView: WelcomeView!
required init(screen: WelcomeScreen, environment: ViewEnvironment) {
- self.welcomeView = WelcomeView(frame: .zero)
super.init(screen: screen, environment: environment)
- update(with: screen)
}
override func viewDidLoad() {
super.viewDidLoad()
+ welcomeView = WelcomeView(frame: view.bounds)
+ updateView(with: screen)
+
view.addSubview(welcomeView)
}
@@ -53,11 +54,14 @@ final class WelcomeViewController: ScreenViewController<WelcomeScreen> {
}
override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) {
- update(with: screen)
+ super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment)
+
+ guard isViewLoaded else { return }
+
+ updateView(with: screen)
}
- private func update(with screen: WelcomeScreen) {
- /// Update UI
+ private func updateView(with screen: WelcomeScreen) {
welcomeView.name = screen.name
welcomeView.onNameChanged = screen.onNameChanged
welcomeView.onLoginTapped = screen.onLoginTapped

diff:
@@ -23,7 +23,7 @@ import WorkflowUI
struct WelcomeWorkflow: Workflow {
enum Output {
- case didLogin(name: String)
+ case didLogIn(name: String)
}
}
@@ -48,7 +48,7 @@ extension WelcomeWorkflow {
typealias WorkflowType = WelcomeWorkflow
case nameChanged(name: String)
- case didLogin
+ case didLogIn
func apply(toState state: inout WelcomeWorkflow.State) -> WelcomeWorkflow.Output? {
switch self {
@@ -58,9 +58,9 @@ extension WelcomeWorkflow {
// Return `nil` for the output, we want to handle this action only at the level of this workflow.
return nil
- case .didLogin:
- // Return an output of `didLogin` with the name.
- return .didLogin(name: state.name)
+ case .didLogIn:
+ // Return an output of `didLogIn` with the name.
+ return .didLogIn(name: state.name)
}
}
}
@@ -97,8 +97,8 @@ extension WelcomeWorkflow {
sink.send(.nameChanged(name: name))
},
onLoginTapped: {
- // Whenever the login button is tapped, emit the `.didLogin` action.
- sink.send(.didLogin)
+ // Whenever the login button is tapped, emit the `.didLogIn` action.
+ sink.send(.didLogIn)
}
)
}

original_message: Update Tutorial 3


message: Update Tutorial 4

diff:
@@ -51,18 +51,19 @@ extension RootWorkflow {
enum Action: WorkflowAction {
typealias WorkflowType = RootWorkflow
- case login(name: String)
- case logout
+ case logIn(name: String)
+ case logOut
func apply(toState state: inout RootWorkflow.State) -> RootWorkflow.Output? {
switch self {
- case .login(name: let name):
+ case .logIn(name: let name):
// When the `login` action is received, change the state to `todo`.
state = .todo(name: name)
- case .logout:
+ case .logOut:
// Return to the welcome state on logout.
state = .welcome
}
+
return nil
}
}
@@ -90,17 +91,15 @@ extension RootWorkflow {
typealias Rendering = BackStackScreen<AnyScreen>
func render(state: RootWorkflow.State, context: RenderContext<RootWorkflow>) -> Rendering {
- // Delete the `let sink = context.makeSink(of: ...) as we no longer need a sink.
-
// Our list of back stack items. Will always include the "WelcomeScreen".
var backStackItems: [BackStackScreen<AnyScreen>.Item] = []
let welcomeScreen = WelcomeWorkflow()
.mapOutput { output -> Action in
switch output {
- // When `WelcomeWorkflow` emits `didLogin`, turn it into our `login` action.
- case .didLogin(name: let name):
- return .login(name: name)
+ // When `WelcomeWorkflow` emits `didLogIn`, turn it into our `logIn` action.
+ case .didLogIn(name: let name):
+ return .logIn(name: name)
}
}
.rendered(in: context)
@@ -116,25 +115,24 @@ extension RootWorkflow {
backStackItems.append(welcomeBackStackItem)
switch state {
- // When the state is `.welcome`, defer to the WelcomeWorkflow.
case .welcome:
// We always add the welcome screen to the backstack, so this is a no op.
break
- // When the state is `.todo`, defer to the TodoListWorkflow.
case .todo(name: let name):
+ // When the state is `.todo`, defer to the TodoListWorkflow.
- // was: let todoBackStackItems = TodoListWorkflow(name: name)
let todoBackStackItems = TodoWorkflow(name: name)
.mapOutput { output -> Action in
switch output {
case .back:
- // When receiving a `.back` output, treat it as a `.logout` action.
- return .logout
+ // When receiving a `.back` output, treat it as a `.logOut` action.
+ return .logOut
}
}
.rendered(in: context)
+ // Add the todoBackStackItems to our backStackItems
backStackItems.append(contentsOf: todoBackStackItems)
}

diff:
@@ -25,6 +25,7 @@ import WorkflowUI
struct TodoListWorkflow: Workflow {
// The name is an input.
var name: String
+
// Use the list of todo items passed from our parent.
var todos: [TodoModel]
@@ -97,12 +98,11 @@ extension TodoListWorkflow {
typealias Rendering = BackStackScreen<AnyScreen>.Item
func render(state: TodoListWorkflow.State, context: RenderContext<TodoListWorkflow>) -> Rendering {
- // Define a sink to be able to send the .onBack action.
+ // Define a sink to be able to send actions.
let sink = context.makeSink(of: Action.self)
- let titles = todos.map { (todoModel) -> String in
- todoModel.title
- }
+ let titles = todos.map(\.title)
+
let todoListScreen = TodoListScreen(
todoTitles: titles,
onTodoSelected: { index in
@@ -115,7 +115,7 @@ extension TodoListWorkflow {
key: "list",
screen: todoListScreen.asAnyScreen(),
barContent: .init(
- title: "Welcome \(name)",
+ title: "Welcome, \(name)",
leftItem: .button(.back(handler: {
// When the left button is tapped, send the .onBack action.
sink.send(.onBack)

diff:
@@ -36,9 +36,11 @@ extension TodoWorkflow {
struct State {
var todos: [TodoModel]
var step: Step
+
enum Step {
// Showing the list of todo items.
case list
+
// Editing a single item. The state holds the index so it can be updated when a save action is received.
case edit(index: Int)
}
@@ -50,7 +52,7 @@ extension TodoWorkflow {
TodoModel(
title: "Take the cat for a walk",
note: "Cats really need their outside sunshine time. Don't forget to walk Charlie. Hamilton is less excited about the prospect."
- ),
+ )
],
step: .list
)
@@ -93,7 +95,7 @@ extension TodoWorkflow {
typealias WorkflowType = TodoWorkflow
case discardChanges
- case saveChanges(index: Int, todo: TodoModel)
+ case saveChanges(todo: TodoModel, index: Int)
func apply(toState state: inout TodoWorkflow.State) -> TodoWorkflow.Output? {
guard case .edit = state.step else {
@@ -102,13 +104,14 @@ extension TodoWorkflow {
switch self {
case .discardChanges:
+ // When a discard action is received, return to the list.
state.step = .list
- case .saveChanges(index: let index, todo: let updatedTodo):
- state.todos[index] = updatedTodo
- }
- // Return to the list view for either a discard or save action.
+ case .saveChanges(todo: let todo, index: let index):
+ // When changes are saved, update the state of that `todo` item and return to the list.
+ state.todos[index] = todo
state.step = .list
+ }
return nil
}
@@ -137,10 +140,7 @@ extension TodoWorkflow {
typealias Rendering = [BackStackScreen<AnyScreen>.Item]
func render(state: TodoWorkflow.State, context: RenderContext<TodoWorkflow>) -> Rendering {
- let todoListItem = TodoListWorkflow(
- name: name,
- todos: state.todos
- )
+ let todoListItem = TodoListWorkflow(name: name, todos: state.todos)
.mapOutput { output -> ListAction in
switch output {
case .back:
@@ -157,25 +157,25 @@ extension TodoWorkflow {
switch state.step {
case .list:
- // Return only the list item.
+ // On the "list" step, return just the list screen.
return [todoListItem]
case .edit(index: let index):
-
- let todoEditItem = TodoEditWorkflow(
- initialTodo: state.todos[index])
+ // On the "edit" step, return both the list and edit screens.
+ let todoEditItem = TodoEditWorkflow(initialTodo: state.todos[index])
.mapOutput { output -> EditAction in
switch output {
case .discard:
+ // Send the discardChanges actions when the discard output is received.
return .discardChanges
- case .save(let updatedTodo):
- return .saveChanges(index: index, todo: updatedTodo)
+ case .save(let todo):
+ // Send the saveChanges action when the save output is received.
+ return .saveChanges(todo: todo, index: index)
}
}
.rendered(in: context)
- // Return both the list item and edit.
return [todoListItem, todoEditItem]
}
}

original_message: Update Tutorial 4


message: Tweak setup instructions

diff:
@@ -8,7 +8,7 @@ To follow this tutorial:
- Open your terminal and run `bundle exec pod install` in the `Samples/Tutorial` directory.
- Open `Tutorial.xcworkspace` and build the `Tutorial` Scheme.
-Start from implementation of `Tutorial1` if you're skipping ahead. You can run this by updating the `AppDelegate` to import `Tutorial1` instead of `TutorialBase`.
+Start from the implementation of `Tutorial1` if you're skipping ahead. You can do this by updating the `AppDelegate` to import `Tutorial1` instead of `TutorialBase`.
## Second Workflow

diff:
@@ -8,7 +8,7 @@ To follow this tutorial:
- Open your terminal and run `bundle exec pod install` in the `Samples/Tutorial` directory.
- Open `Tutorial.xcworkspace` and build the `Tutorial` Scheme.
-Start from the implementation of `Tutorial2` if you're skipping ahead. You can run this by updating the `AppDelegate` to import `Tutorial2` instead of `TutorialBase`.
+Start from the implementation of `Tutorial2` if you're skipping ahead. You can do this by updating the `AppDelegate` to import `Tutorial2` instead of `TutorialBase`.
## Editing todo items

diff:
@@ -8,7 +8,7 @@ To follow this tutorial:
- Open your terminal and run `bundle exec pod install` in the `Samples/Tutorial` directory.
- Open `Tutorial.xcworkspace` and build the `Tutorial` Scheme.
-Start from implementation of `Tutorial3` if you're skipping ahead. You can run this by updating the `AppDelegate` to import `Tutorial3` instead of `TutorialBase`.
+Start from the implementation of `Tutorial3` if you're skipping ahead. You can do this by updating the `AppDelegate` to import `Tutorial3` instead of `TutorialBase`.
## Adding new todo items

diff:
@@ -9,7 +9,7 @@ To follow this tutorial:
- Open `Tutorial.xcworkspace` and build the `Tutorial` Scheme.
- The unit tests will run from the default scheme when pressing `cmd+shift+u`.
-Start from implementation of `Tutorial4` if you're skipping ahead. You can run this by updating the `AppDelegate` to import `Tutorial4` instead of `TutorialBase`.
+Start from the implementation of `Tutorial4` if you're skipping ahead. You can do this by updating the `AppDelegate` to import `Tutorial4` instead of `TutorialBase`.
# Testing
@@ -19,7 +19,7 @@ The `WorkflowTesting` library is provided to allow easy unit and integration tes
## Unit Tests (Actions)
-A `WorkflowAction`'s `apply` function is effectively a reducer. Given a current state and action, it returns a new state (and optionally an output). Because `apply` function should almost always be a "pure" function, it is a great candidate for unit testing.
+A `WorkflowAction`'s `apply` function is effectively a reducer. Given a current state and action, it returns a new state (and optionally an output). Because an `apply` function should almost always be a "pure" function, it is a great candidate for unit testing.
The `WorkflowActionTester` is provided to facilitate writing unit tests against actions.
@@ -34,7 +34,7 @@ The `WorkflowActionTester` is an extension on `WorkflowAction` which provides an
/// .assert(state: .differentState)
```
-It's provided with an initial state, and drives the state forward by sending one action at a time. The `Output` can be validated after each action is sent, as well as the `State`.
+You provide an initial state, and drive the state forward by sending one action at a time. The `Output` can be validated after each action is sent; the `State` can be, as well.
### WelcomeWorkflow Tests

original_message: Tweak setup instructions


message: Update action and render test content

diff:
@@ -23,6 +23,7 @@ Pod::Spec.new do |s|
s.test_spec 'Tests' do |test_spec|
test_spec.source_files = 'Tests/**/*.swift'
+ test_spec.dependency 'WorkflowTesting'
test_spec.framework = 'XCTest'
end

diff:
@@ -40,7 +40,7 @@ You provide an initial state, and drive the state forward by sending one action
Start by creating a new Unit test file called `WelcomeWorkflowTests`. Import `WorkflowTesting` as well as a `@testable import` for the `Tutorial` pod you're testing:
-We'll use the `@testable import` to be able to test the our workflows which are not exposed publicly.
+We'll use the `@testable import` to be able to test our workflows which are not exposed publicly.
```swift
import XCTest
@@ -49,12 +49,10 @@ import WorkflowTesting
class WelcomeWorkflowTests: XCTestCase {
-
- func testExample() {
+ func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
-
}
```
@@ -67,8 +65,7 @@ import WorkflowTesting
class WelcomeWorkflowTests: XCTestCase {
-
- func testNameUpdates() {
+ func testNameUpdates() throws {
WelcomeWorkflow.Action
.tester(withState: WelcomeWorkflow.State(name: ""))
.send(action: .nameChanged(name: "myName"))
@@ -79,73 +76,70 @@ class WelcomeWorkflowTests: XCTestCase {
XCTAssertEqual("myName", state.name)
}
}
-
}
```
-The `Output` of an action can also be tested. Next, we'll add a test for the `.didLogin` action.
+The `Output` of an action can also be tested. Next, we'll add a test for the `.didLogIn` action.
```swift
- func testLogin() {
+ func testLogIn() throws {
WelcomeWorkflow.Action
.tester(withState: WelcomeWorkflow.State(name: ""))
- .send(action: .nameChanged(name: "myName"))
- // No output is expected when the name changes.
- .assertNoOutput()
- .verifyState { state in
- // The `name` has been updated from the action.
- XCTAssertEqual("myName", state.name)
+ .send(action: .didLogIn)
+ .verifyOutput { output in
+ // A `.didLogIn` output should be emitted with the name when the `.didLogIn` action was received.
+ switch output {
+ case .didLogIn(name: let name):
+ XCTAssertEqual("", name)
+ }
}
}
```
-We have now validated that an output is emitted when the `.didLogin` action is received. However, while writing this test, it probably doesn't make sense to allow someone to log in without providing a name. Let's update the test to ensure that login is only allowed when there is a name:
+We have now validated that an output is emitted when the `.didLogIn` action is received. However, while writing this test, it probably doesn't make sense to allow someone to log in without providing a name. Let's update the test to ensure that login is only allowed when there is a name:
```swift
- func testLogin() {
+ func testLogIn() throws {
WelcomeWorkflow.Action
.tester(withState: WelcomeWorkflow.State(name: ""))
- .send(action: .didLogin)
- // Since the name is empty, `.didLogin` will not emit an output.
+ .send(action: .didLogIn)
+ // Since the name is empty, `.didLogIn` will not emit an output.
.assertNoOutput()
.verifyState { state in
// The name is empty, as was specified in the initial state.
XCTAssertEqual("", state.name)
}
- .send(action: .nameChanged(name: "MyName"))
+ .send(action: .nameChanged(name: "Ada"))
// Update the name, no output expected.
.assertNoOutput()
.verifyState { state in
// Validate the name was updated.
- XCTAssertEqual("MyName", state.name)
+ XCTAssertEqual("Ada", state.name)
}
- .send(action: .didLogin)
+ .send(action: .didLogIn)
.verifyOutput { output in
- // Now a `.didLogin` output should be emitted when the `.didLogin` action was received.
+ // Now a `.didLogIn` output should be emitted when the `.didLogIn` action was received.
switch output {
- case .didLogin(let name):
- XCTAssertEqual("MyName", name)
+ case .didLogIn(name: let name):
+ XCTAssertEqual("Ada", name)
}
}
}
```
-The test will now fail, as a `.didLogin` action will still cause `.didLogin` output when the name is blank. Update the `WelcomeWorkflow` logic to reflect the new behavior we want:
+The test will now fail, as a `.didLogIn` action will still cause `.didLogIn` output when the name is blank. Update the `WelcomeWorkflow` logic to reflect the new behavior we want:
```swift
// MARK: Actions
extension WelcomeWorkflow {
-
enum Action: WorkflowAction {
-
typealias WorkflowType = WelcomeWorkflow
case nameChanged(name: String)
- case didLogin
+ case didLogIn
func apply(toState state: inout WelcomeWorkflow.State) -> WelcomeWorkflow.Output? {
-
switch self {
case .nameChanged(name: let name):
// Update our state with the updated name.
@@ -153,13 +147,13 @@ extension WelcomeWorkflow {
// Return `nil` for the output, we want to handle this action only at the level of this workflow.
return nil
- case .didLogin:
- if state.name.count != 0 {
- // Return an output of `didLogin` with the name if it's not empty.
- return .didLogin(name: state.name)
- } else {
+ case .didLogIn:
+ if state.name.isEmpty {
// Don't log in if the name isn't filled in.
return nil
+ } else {
+ // Return an output of `didLogIn` with the name.
+ return .didLogIn(name: state.name)
}
}
}
@@ -167,7 +161,7 @@ extension WelcomeWorkflow {
}
```
-Run the test again and ensure that it passes. Additionally, try the app to see that it also reflects the updated behavior.
+Run the test again and ensure that it passes. Additionally, run the app to see that it also reflects the updated behavior.
### TodoListWorkflow
@@ -178,12 +172,9 @@ import XCTest
@testable import TutorialBase
import WorkflowTesting
-
class TodoListWorkflowTests: XCTestCase {
-
- func testActions() {
- TodoListWorkflow
- .Action
+ func testActions() throws {
+ TodoListWorkflow.Action
.tester(withState: TodoListWorkflow.State())
.send(action: .onBack)
.verifyOutput { output in
@@ -199,7 +190,7 @@ class TodoListWorkflowTests: XCTestCase {
.verifyOutput { output in
// The `.selectTodo` action should emit a `.selectTodo` output.
switch output {
- case .selectTodo(let index):
+ case .selectTodo(index: let index):
XCTAssertEqual(7, index)
default:
XCTFail("Expected an output of `.selectTodo`")
@@ -207,7 +198,7 @@ class TodoListWorkflowTests: XCTestCase {
}
.send(action: .new)
.verifyOutput { output in
- // The`.new` action should emit a `.newTodo` output.
+ // THe `.new` action should emit a `.newTodo` output.
switch output {
case .newTodo:
break // Expected
@@ -216,7 +207,6 @@ class TodoListWorkflowTests: XCTestCase {
}
}
}
-
}
```
@@ -229,16 +219,11 @@ import XCTest
@testable import TutorialBase
import WorkflowTesting
-
class TodoEditWorkflowTests: XCTestCase {
-
- func testAction() {
- TodoEditWorkflow
- .Action
+ func testAction() throws {
+ TodoEditWorkflow.Action
// Start with a todo of "Title" "Note"
- .tester(
- withState: TodoEditWorkflow.State(
- todo: TodoModel(title: "Title", note: "Note")))
+ .tester(withState: TodoEditWorkflow.State(todo: TodoModel(title: "Title", note: "Note")))
.verifyState { state in
XCTAssertEqual("Title", state.todo.title)
XCTAssertEqual("Note", state.todo.note)
@@ -254,7 +239,7 @@ class TodoEditWorkflowTests: XCTestCase {
// Update the note.
.send(action: .noteChanged("Updated Note"))
.assertNoOutput()
- // Validate that the note updated.
+ // Validate that the note was updated.
.verifyState { state in
XCTAssertEqual("Updated Title", state.todo.title)
XCTAssertEqual("Updated Note", state.todo.note)
@@ -281,16 +266,16 @@ class TodoEditWorkflowTests: XCTestCase {
}
}
}
-
}
```
The `TodoEditWorkflow` also uses the `workflowDidChange` to update the internal state if its parent provides it with a different `todo`. Validate that this works as expected:
```swift
- func testChangedPropertyUpdatesLocalState() {
+ func testChangedPropertyUpdatesLocalState() throws {
let initialWorkflow = TodoEditWorkflow(initialTodo: TodoModel(title: "Title", note: "Note"))
var state = initialWorkflow.makeInitialState()
+
// The initial state is a copy of the provided todo:
XCTAssertEqual("Title", state.todo.title)
XCTAssertEqual("Note", state.todo.note)
@@ -303,10 +288,9 @@ The `TodoEditWorkflow` also uses the `workflowDidChange` to update the internal
XCTAssertEqual("Updated Title", state.todo.title)
XCTAssertEqual("Note", state.todo.note)
- // The parent provided different properties. The internal state should be updated with the newly provided properties.
+ // The parent provided different properties. The internal state should be updated with the newly-provided properties.
let updatedWorkflow = TodoEditWorkflow(initialTodo: TodoModel(title: "New Title", note: "New Note"))
updatedWorkflow.workflowDidChange(from: initialWorkflow, state: &state)
-
XCTAssertEqual("New Title", state.todo.title)
XCTAssertEqual("New Note", state.todo.note)
}
@@ -314,11 +298,11 @@ The `TodoEditWorkflow` also uses the `workflowDidChange` to update the internal
## Testing Rendering
-Testing actions is very useful for validating all of the state transitions of a workflow, but it is beneficial to validate that the screens from `render` are expected. Since the `render` method uses a private implementation of a `RenderContext`, there is a `RenderTester` to facilitate testing.
+Testing actions is very useful for validating all of the state transitions of a workflow, but it is also beneficial to verify the logic in `render`. Since the `render` method uses a private implementation of a `RenderContext`, there is a `RenderTester` to facilitate testing.
## RenderTester
-The `renderTester` extension on `Workflow` provides an easy way to test the rendering from a workflow. The simple usage of validating a rendering is shown in the doc comments:
+The `renderTester` extension on `Workflow` provides an easy way to test the rendering of a workflow. The simple usage of validating a rendering is shown in the doc comments:
```swift
workflow
.renderTester()
@@ -336,10 +320,12 @@ workflow
XCTAssertEqual("expected text on rendering", rendering.text)
rendering.updateText("updated")
}
- .assert(state: TestWorkflow.State(text: "updated"))
+ .assert(
+ state: TestWorkflow.State(text: "updated")
+ )
```
-The full API allows for expected workers and (child) workflows, and verification of resulting state and output:
+The full API allows for expected workers and (child) workflows, as well as verification of resulting state and output:
```swift
workflow
.renderTester(initialState: State())
@@ -366,44 +352,45 @@ Add tests for the rendering of the `WelcomeWorkflow`:
```swift
// WelcomeWorkflowTests.swift
- func testRenderingInitial() {
+ func testRenderingInitial() throws {
WelcomeWorkflow()
- // Use the initial state provided by the welcome workflow
+ // Use the initial state provided by the welcome workflow.
.renderTester()
.render { screen in
XCTAssertEqual("", screen.name)
- // Simulate tapping the login button. No output will be emitted, as the name is empty:
+
+ // Simulate tapping the log in button. No output will be emitted, as the name is empty.
screen.onLoginTapped()
}
.assertNoOutput()
}
- func testRenderingNameChange() {
+ func testRenderingNameChange() throws {
WelcomeWorkflow()
- // Use the initial state provided by the welcome workflow
+ // Use the initial state provided by the welcome workflow.
.renderTester()
- // Next, simulate the name updating, expecting the state to be changed to reflect the updated name:
+ // Next, simulate the name updating, expecting the state to be changed to reflect the updated name.
.render { screen in
- screen.onNameChanged("myName")
+ screen.onNameChanged("Ada")
}
.verifyState { state in
- XCTAssertEqual(state.name, "myName")
+ XCTAssertEqual("Ada", state.name)
}
}
- func testRenderingLogin() {
+ func testRenderingLogIn() throws {
WelcomeWorkflow()
- // Start with a name entered
- .renderTester(initialState: WelcomeWorkflow.State(name: "myName"))
- // Simulate the name updating
+ // Start with a name already entered.
+ .renderTester(initialState: WelcomeWorkflow.State(name: "Ada"))
+ // Simulate a log in button tap.
.render { screen in
screen.onLoginTapped()
}
- // Finally, validate that `.didLogin` is sent
+ // Finally, validate that `.didLogIn` was sent.
.verifyOutput { output in
switch output {
- case .didLogin("myName"):
- break // pass
+ case .didLogIn(name: "Ada"):
+ break // Pass
default:
XCTFail("Unexpected output \(output)")
}
@@ -418,7 +405,7 @@ Since the `State` and `Output` on the `WelcomeWorkflow` aren't equatable, we had
struct WelcomeWorkflow: Workflow {
enum Output: Equatable {
- case didLogin(name: String)
+ case didLogIn(name: String)
}
}
@@ -426,7 +413,6 @@ struct WelcomeWorkflow: Workflow {
// MARK: State and Initialization
extension WelcomeWorkflow {
-
struct State: Equatable {
var name: String
}
@@ -436,29 +422,28 @@ extension WelcomeWorkflow {
Update the last two tests to take advantage of the `Equatable` conformance:
-
```swift
- func testRenderingNameChange() {
+ func testRenderingNameChange() throws {
WelcomeWorkflow()
- // Use the initial state provided by the welcome workflow
+ // Use the initial state provided by the welcome workflow.
.renderTester()
- // Next, simulate the name updating, expecting the state to be changed to reflect the updated name:
+ // Next, simulate the name updating, expecting the state to be changed to reflect the updated name.
.render { screen in
- screen.onNameChanged("myName")
+ screen.onNameChanged("Ada")
}
- .assert(state: WelcomeWorkflow.State(name: "myName"))
+ .assert(state: WelcomeWorkflow.State(name: "Ada"))
}
- func testRenderingLogin() {
+ func testRenderingLogIn() throws {
WelcomeWorkflow()
- // Start with a name entered
- .renderTester(initialState: WelcomeWorkflow.State(name: "myName"))
- // Simulate the name updating
+ // Start with a name already entered.
+ .renderTester(initialState: WelcomeWorkflow.State(name: "Ada"))
+ // Simulate a log in button tap.
.render { screen in
screen.onLoginTapped()
}
- // Finally, validate that `.didLogin` is sent
- .assert(output: .didLogin("myName"))
+ // Finally, validate that `.didLogIn` was sent.
+ .assert(output: .didLogIn(name: "Ada"))
}
```

original_message: Update action and render test content