-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Synthesize update(state:actions:environment) (#16)
This PR introduces an alternative approach to composing multiple update functions. Any type that conforms to `ModelProtocol` has a `update(state:actions:environment)` static function synthesized for it. This function can be used to simulate the effect of sending multiple actions in sequence *immediately*, in effect, composing the actions. ``` update( state: state, actions: [ .setEditor( text: detail.entry.body, saveState: detail.saveState ), .presentDetail(true), .requestEditorFocus(false) ], environment: environment ) ``` State is updated immediately, fx are merged, and last transaction wins. Now that we have a way to immediately sequence actions in same state update, we no longer need to run fx on same tick. Joining on main is my preference from an API perspective because it has fewer footguns in implementation and use. #15 caused fx to be run immediately instead of joined on main. The intent was to allow for composing multiple actions by sending up many `Just(.action)` fx. However, - This is verbose to write, and rather "chatty". - It also makes the store implementation less straightforward, since without joining on main, we must check if fx was completed immediately before adding to fx dictionary. Joining on main solves this problem by running the fx on next tick, after the fx has been added to dictionary. - Additionally, it means that off-main-thread fx are required to be joined manually on main to prevent SwiftUI from complaining. ## Breaking changes - Remove Update.pipe. Redundant now. Was never happy with it anyway. It was an inelegant way to accomplish the same thing as `update(state:actions:environment:)`. - Revert fx to be joined on main thread. We join on main with a .default QoS, because fx should be async/never block user interaction.
- Loading branch information
1 parent
b773ae5
commit 5ea1c8b
Showing
2 changed files
with
159 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
// | ||
// UpdateActionsTests.swift | ||
// | ||
// Created by Gordon Brander on 9/14/22. | ||
// | ||
|
||
import XCTest | ||
import ObservableStore | ||
import Combine | ||
|
||
class UpdateActionsTests: XCTestCase { | ||
enum TestAction { | ||
case increment | ||
case setText(String) | ||
case delayedText(text: String, delay: Double) | ||
case delayedIncrement(delay: Double) | ||
case combo | ||
} | ||
|
||
struct TestModel: ModelProtocol { | ||
typealias Action = TestAction | ||
typealias Environment = Void | ||
|
||
var count = 0 | ||
var text = "" | ||
|
||
static func update( | ||
state: TestModel, | ||
action: TestAction, | ||
environment: Void | ||
) -> Update<TestModel> { | ||
switch action { | ||
case .increment: | ||
var model = state | ||
model.count = model.count + 1 | ||
return Update(state: model) | ||
.animation(.default) | ||
case .setText(let text): | ||
var model = state | ||
model.text = text | ||
return Update(state: model) | ||
case let .delayedText(text, delay): | ||
let fx: Fx<Action> = Just( | ||
Action.setText(text) | ||
) | ||
.delay(for: .seconds(delay), scheduler: DispatchQueue.main) | ||
.eraseToAnyPublisher() | ||
return Update(state: state, fx: fx) | ||
case let .delayedIncrement(delay): | ||
let fx: Fx<Action> = Just( | ||
Action.increment | ||
) | ||
.delay(for: .seconds(delay), scheduler: DispatchQueue.main) | ||
.eraseToAnyPublisher() | ||
return Update(state: state, fx: fx) | ||
case .combo: | ||
return update( | ||
state: state, | ||
actions: [ | ||
.increment, | ||
.increment, | ||
.delayedIncrement(delay: 0.02), | ||
.delayedText(text: "Test", delay: 0.01), | ||
.increment | ||
], | ||
environment: environment | ||
) | ||
} | ||
} | ||
} | ||
|
||
func testUpdateActions() throws { | ||
let store = Store( | ||
state: TestModel(), | ||
environment: () | ||
) | ||
store.send(.combo) | ||
let expectation = XCTestExpectation( | ||
description: "Autofocus sets editor focus" | ||
) | ||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { | ||
XCTAssertEqual( | ||
store.state.count, | ||
4, | ||
"All increments run. Fx merged." | ||
) | ||
XCTAssertEqual( | ||
store.state.text, | ||
"Test", | ||
"Text set" | ||
) | ||
expectation.fulfill() | ||
} | ||
wait(for: [expectation], timeout: 0.2) | ||
} | ||
|
||
func testUpdateActionsTransaction() throws { | ||
let next = TestModel.update( | ||
state: TestModel(), | ||
actions: [ | ||
.increment, | ||
.increment, | ||
.setText("Foo"), | ||
.increment, | ||
], | ||
environment: () | ||
) | ||
XCTAssertNotNil(next.transaction, "Last transaction wins") | ||
} | ||
} |