Update the completed Tutorial 5 code
MODIFY
Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/RootWorkflow.swift
Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/RootWorkflow.swift
@@ -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
}
}
@@ -90,17 +89,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 +113,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)
}

MODIFY
Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/Edit/TodoEditScreen.swift
Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/Edit/TodoEditScreen.swift
@@ -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

MODIFY
Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/Edit/TodoEditWorkflow.swift
Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/Edit/TodoEditWorkflow.swift
@@ -140,6 +140,7 @@ extension TodoEditWorkflow {
))
)
)
+
return backStackItem
}
}

MODIFY
Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/List/TodoListScreen.swift
Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/List/TodoListScreen.swift
@@ -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

MODIFY
Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/List/TodoListWorkflow.swift
Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/List/TodoListWorkflow.swift
@@ -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)

MODIFY
Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/TodoWorkflow.swift
Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/TodoWorkflow.swift
@@ -36,9 +36,11 @@ extension TodoWorkflow {
struct State: Equatable {
var todos: [TodoModel]
var step: Step
+
enum Step: Equatable {
// 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,20 +157,21 @@ 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)

MODIFY
Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Welcome/WelcomeScreen.swift
Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Welcome/WelcomeScreen.swift
@@ -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

MODIFY
Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Welcome/WelcomeWorkflow.swift
Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Welcome/WelcomeWorkflow.swift
@@ -23,7 +23,7 @@ import WorkflowUI
struct WelcomeWorkflow: Workflow {
enum Output: Equatable {
- 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,13 +58,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)
}
}
}
@@ -102,8 +102,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)
}
)
}

MODIFY
Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/RootWorkflowTests.swift
Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/RootWorkflowTests.swift
@@ -24,16 +24,15 @@ import XCTest
@testable import WorkflowUI
class RootWorkflowTests: XCTestCase {
- func testWelcomeRendering() {
+ func testWelcomeRendering() throws {
RootWorkflow()
// Start in the `.welcome` state
- .renderTester(initialState: RootWorkflow.State.welcome)
+ .renderTester(initialState: .welcome)
// The `WelcomeWorkflow` is expected to be started in this render.
.expectWorkflow(
type: WelcomeWorkflow.self,
- // Simulate this as the `WelcomeScreen` returned by the `WelcomeWorkflow`. The callback can be stubbed out, as they won't be used.
producingRendering: WelcomeScreen(
- name: "MyName",
+ name: "Ada",
onNameChanged: { _ in },
onLoginTapped: {}
)
@@ -45,28 +44,27 @@ class RootWorkflowTests: XCTestCase {
XCTFail("Expected first screen to be a `WelcomeScreen`")
return
}
- XCTAssertEqual("MyName", welcomeScreen.name)
+
+ XCTAssertEqual("Ada", welcomeScreen.name)
}
- // No output should be produced from the root workflow.
+ // Assert that no action was produced during this render, meaning our state remains unchanged
.assertNoOutput()
- // Assert that the state to stays as `.welcome`.
- .assert(state: .welcome)
}
- func testLogin() {
+ func testLogIn() throws {
RootWorkflow()
// Start in the `.welcome` state
- .renderTester(initialState: RootWorkflow.State.welcome)
+ .renderTester(initialState: .welcome)
// The `WelcomeWorkflow` is expected to be started in this render.
.expectWorkflow(
type: WelcomeWorkflow.self,
producingRendering: WelcomeScreen(
- name: "MyName",
+ name: "Ada",
onNameChanged: { _ in },
onLoginTapped: {}
),
- // Simulate the `WelcomeWorkflow` sending an output of `.didLogin` as if the login button was tapped.
- producingOutput: .didLogin(name: "MyName")
+ // Simulate the `WelcomeWorkflow` sending an out0put of `.didLogIn` as if the "log in" button was tapped.
+ producingOutput: .didLogIn(name: "Ada")
)
// Now, validate that there is a single item in the BackStackScreen, which is our welcome screen (prior to the output).
.render { rendering in
@@ -75,14 +73,14 @@ class RootWorkflowTests: XCTestCase {
XCTFail("Expected first screen to be a `WelcomeScreen`")
return
}
- XCTAssertEqual("MyName", welcomeScreen.name)
+ XCTAssertEqual("Ada", welcomeScreen.name)
}
- // No output should be produced from the root workflow.
- .assertNoOutput()
- .assert(state: .todo(name: "MyName"))
+ // Assert that the state transitioned to `.todo`
+ .assert(state: .todo(name: "Ada"))
}
- func testAppFlow() {
+ func testAppFlow() throws {
+ // Note: You'll need to `import Workflow` in order to use `WorkflowHost`
let workflowHost = WorkflowHost(workflow: RootWorkflow())
// First rendering is just the welcome screen. Update the name.
@@ -95,10 +93,10 @@ class RootWorkflowTests: XCTestCase {
return
}
- welcomeScreen.onNameChanged("MyName")
+ welcomeScreen.onNameChanged("Ada")
}
- // Log in and go to the welcome list
+ // Log in and go to the todo list.
do {
let backStack = workflowHost.rendering.value
XCTAssertEqual(1, backStack.items.count)
@@ -111,7 +109,7 @@ class RootWorkflowTests: XCTestCase {
welcomeScreen.onLoginTapped()
}
- // Expect the todo list. Edit the first todo.
+ // Expect the todo list to be rendered. Edit the first todo.
do {
let backStack = workflowHost.rendering.value
XCTAssertEqual(2, backStack.items.count)
@@ -125,8 +123,10 @@ class RootWorkflowTests: XCTestCase {
XCTFail("Expected second screen of `TodoListScreen`")
return
}
+
XCTAssertEqual(1, todoScreen.todoTitles.count)
- // Select the first todo:
+
+ // Select the first todo.
todoScreen.onTodoSelected(0)
}
@@ -150,7 +150,7 @@ class RootWorkflowTests: XCTestCase {
return
}
- // Update the title:
+ // Update the title.
editScreen.onTitleChanged("New Title")
}
@@ -177,17 +177,20 @@ class RootWorkflowTests: XCTestCase {
// Save the changes by tapping the right bar button.
// This also validates that the navigation bar was described as expected.
switch backStack.items[2].barVisibility {
+
case .hidden:
XCTFail("Expected a visible navigation bar")
case .visible(let barContent):
switch barContent.rightItem {
+
case .none:
XCTFail("Expected a right bar button")
case .button(let button):
switch button.content {
+
case .text(let text):
XCTAssertEqual("Save", text)
@@ -214,6 +217,7 @@ class RootWorkflowTests: XCTestCase {
XCTFail("Expected second screen of `TodoListScreen`")
return
}
+
XCTAssertEqual(1, todoScreen.todoTitles.count)
XCTAssertEqual("New Title", todoScreen.todoTitles[0])
}

MODIFY
Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/TodoEditWorkflowTests.swift
Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/TodoEditWorkflowTests.swift
@@ -19,13 +19,10 @@ import XCTest
@testable import Tutorial5
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)
@@ -41,7 +38,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)
@@ -69,9 +66,10 @@ class TodoEditWorkflowTests: XCTestCase {
}
}
- 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)
@@ -84,10 +82,9 @@ class TodoEditWorkflowTests: XCTestCase {
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)
}

MODIFY
Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/TodoListWorkflowTests.swift
Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/TodoListWorkflowTests.swift
@@ -19,9 +19,8 @@ import XCTest
@testable import Tutorial5
class TodoListWorkflowTests: XCTestCase {
- func testActions() {
- TodoListWorkflow
- .Action
+ func testActions() throws {
+ TodoListWorkflow.Action
.tester(withState: TodoListWorkflow.State())
.send(action: .onBack)
.verifyOutput { output in
@@ -37,7 +36,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`")

MODIFY
Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/TodoWorkflowTests.swift
Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/TodoWorkflowTests.swift
import BackStackContainer
import Workflow
import WorkflowTesting
+import WorkflowUI
import XCTest
@testable import Tutorial5
class TodoWorkflowTests: XCTestCase {
- func testSelectingTodo() {
+ func testSelectingTodo() throws {
let todos: [TodoModel] = [TodoModel(title: "Title", note: "Note")]
- TodoWorkflow(name: "MyName")
- // Start from the list step to validate selecting a todo:
+ TodoWorkflow(name: "Ada")
+ // Start from the list step to validate selecting a todo.
.renderTester(initialState: TodoWorkflow.State(
todos: todos,
step: .list
))
- // We only expect the TodoListWorkflow
+ // We only expect the TodoListWorkflow to be rendered.
.expectWorkflow(
type: TodoListWorkflow.self,
- producingRendering: BackStackScreen.Item(
- screen: TodoListScreen(
- todoTitles: ["Title"],
- onTodoSelected: { _ in }
- ).asAnyScreen()),
- // Simulate selecting the first todo:
- producingOutput: TodoListWorkflow.Output.selectTodo(index: 0)
+ producingRendering: BackStackScreen<AnyScreen>.Item(
+ screen: TodoListScreen(todoTitles: ["Title"], onTodoSelected: { _ in }).asAnyScreen()
+ ),
+ // Simulate selecting the first todo.
+ producingOutput: .selectTodo(index: 0)
)
.render { items in
// Just validate that there is one item in the back stack.
- // Additional validation could be done on the screens returned if so desired.
+ // Additional validation could be done on the screens returned, if desired.
XCTAssertEqual(1, items.count)
}
- // Validate that the state was updated after the last render pass with the output from the TodoEditWorkflow.
- .assert(
- state: TodoWorkflow.State(
+ // Assert that the state was updated after the render pass with the output from the TodoListWorkflow.
+ .assert(state: TodoWorkflow.State(
todos: [TodoModel(title: "Title", note: "Note")],
step: .edit(index: 0)
- )
- )
+ ))
}
- func testSavingTodo() {
+ func testSavingTodo() throws {
let todos: [TodoModel] = [TodoModel(title: "Title", note: "Note")]
- TodoWorkflow(name: "MyName")
- // Start from the edit step so we can simulate saving:
+ TodoWorkflow(name: "Ada")
+ // Start from the edit step so we can simulate saving.
.renderTester(initialState: TodoWorkflow.State(
todos: todos,
step: .edit(index: 0)
))
- // We always expect the TodoListWorkflow
+ // We always expect the TodoListWorkflow to be rendered.
.expectWorkflow(
type: TodoListWorkflow.self,
- producingRendering: BackStackScreen.Item(
+ producingRendering: BackStackScreen<AnyScreen>.Item(
screen: TodoListScreen(
todoTitles: ["Title"],
onTodoSelected: { _ in }
).asAnyScreen()
)
)
- // Expect the TodoEditWorkflow. Additionally, simulate it emitting an output of ".save" to update the state.
+ // Expect the TodoEditWorkflow to be rendered as well (as we're on the edit step).
.expectWorkflow(
type: TodoEditWorkflow.self,
- producingRendering: BackStackScreen.Item(
+ producingRendering: BackStackScreen<AnyScreen>.Item(
screen: TodoEditScreen(
title: "Title",
note: "Note",
@@ -85,22 +82,21 @@ class TodoWorkflowTests: XCTestCase {
onNoteChanged: { _ in }
).asAnyScreen()
),
- producingOutput: TodoEditWorkflow.Output.save(TodoModel(
+ // Simulate it emitting an output of `.save` to update the state.
+ producingOutput: .save(TodoModel(
title: "Updated Title",
note: "Updated Note"
))
)
.render { items in
// Just validate that there are two items in the back stack.
- // Additional validation could be done on the screens returned if so desired.
+ // Additional validation could be done on the screens returned, if desired.
XCTAssertEqual(2, items.count)
}
- // Validate that the state was updated after the last render pass with the output from the TodoEditWorkflow.
- .assert(
- state: TodoWorkflow.State(
+ // Validate that the state was updated after the render pass with the output from the TodoEditWorkflow.
+ .assert(state: TodoWorkflow.State(
todos: [TodoModel(title: "Updated Title", note: "Updated Note")],
step: .list
- )
- )
+ ))
}
}

MODIFY
Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/WelcomeWorkflowTests.swift
Samples/Tutorial/Frameworks/Tutorial5Complete/Tests/WelcomeWorkflowTests.swift
@@ -19,13 +19,9 @@ import XCTest
@testable import Tutorial5
class WelcomeWorkflowTests: XCTestCase {
- func testNameUpdates() {
+ func testNameUpdates() throws {
WelcomeWorkflow.Action
.tester(withState: WelcomeWorkflow.State(name: ""))
- .verifyState { state in
- // The initial state provided was an empty name.
- XCTAssertEqual("", state.name)
- }
.send(action: .nameChanged(name: "myName"))
// No output is expected when the name changes.
.assertNoOutput()
@@ -35,63 +31,66 @@ class WelcomeWorkflowTests: XCTestCase {
}
}
- 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)
}
}
}
- func testRendering() {
+ 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()
- .assert(state: WelcomeWorkflow.State(name: ""))
+ }
- // Next, simulate the name updating, expecting the state to be changed to reflect the updated name:
+ func testRenderingNameChange() throws {
WelcomeWorkflow()
- .renderTester(initialState: WelcomeWorkflow.State(name: ""))
+ // 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.
.render { screen in
- screen.onNameChanged("myName")
+ screen.onNameChanged("Ada")
+ }
+ .assert(state: WelcomeWorkflow.State(name: "Ada"))
}
- .assert(state: WelcomeWorkflow.State(name: "myName"))
- .assertNoOutput()
- // Finally, validate that `.didLogin` is sent when login is tapped with a non-empty name:
+ func testRenderingLogIn() throws {
WelcomeWorkflow()
- .renderTester(initialState: WelcomeWorkflow.State(name: "myName"))
+ // Start with a name already entered.
+ .renderTester(initialState: WelcomeWorkflow.State(name: "Ada"))
+ // Simulate a log in button tap.
.render { screen in
- XCTAssertEqual("myName", screen.name)
- // Simulate tapping the login button. No output will be emitted, as the name is empty:
screen.onLoginTapped()
}
- .assert(output: .didLogin(name: "myName"))
- .assert(state: WelcomeWorkflow.State(name: "myName"))
+ // Finally, validate that `.didLogIn` was sent.
+ .assert(output: .didLogIn(name: "Ada"))
}
}

Update the completed Tutorial 5 code