Update the template screen to use the new(ish) Observation framework. (#4077)
* Update the template screen to use the new(ish) Observation framework. * Add a variant of deferFulfillment that supports observables. * Update snapshot fulfilment to work with either a publisher or a stream.
This commit is contained in:
@@ -16,6 +16,8 @@ enum TemplateScreenViewModelAction {
|
||||
struct TemplateScreenViewState: BindableState {
|
||||
var title: String
|
||||
var placeholder: String
|
||||
var counter = 0
|
||||
|
||||
var bindings: TemplateScreenViewStateBindings
|
||||
}
|
||||
|
||||
@@ -27,5 +29,8 @@ enum TemplateScreenViewAction {
|
||||
case done
|
||||
case textChanged
|
||||
|
||||
case incrementCounter
|
||||
case decrementCounter
|
||||
|
||||
// Consider adding CustomStringConvertible conformance if the actions contain PII
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
typealias TemplateScreenViewModelType = StateStoreViewModel<TemplateScreenViewState, TemplateScreenViewAction>
|
||||
typealias TemplateScreenViewModelType = StateStoreViewModelV2<TemplateScreenViewState, TemplateScreenViewAction>
|
||||
|
||||
class TemplateScreenViewModel: TemplateScreenViewModelType, TemplateScreenViewModelProtocol {
|
||||
private let actionsSubject: PassthroughSubject<TemplateScreenViewModelAction, Never> = .init()
|
||||
@@ -32,6 +32,16 @@ class TemplateScreenViewModel: TemplateScreenViewModelType, TemplateScreenViewMo
|
||||
actionsSubject.send(.done)
|
||||
case .textChanged:
|
||||
MXLog.info("View model: composer text changed to: \(state.bindings.composerText)")
|
||||
case .incrementCounter:
|
||||
Task {
|
||||
try await Task.sleep(for: .seconds(.random(in: 1.0...2.0)))
|
||||
state.counter += 1
|
||||
}
|
||||
case .decrementCounter:
|
||||
Task {
|
||||
try await Task.sleep(for: .seconds(.random(in: 1.0...2.0)))
|
||||
state.counter -= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct TemplateScreen: View {
|
||||
@ObservedObject var context: TemplateScreenViewModel.Context
|
||||
@Bindable var context: TemplateScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
@@ -18,9 +18,19 @@ struct TemplateScreen: View {
|
||||
kind: .textField(text: $context.composerText))
|
||||
|
||||
ListRow(label: .centeredAction(title: L10n.actionDone,
|
||||
systemIcon: .doorLeftHandClosed),
|
||||
icon: \.leave),
|
||||
kind: .button { context.send(viewAction: .done) })
|
||||
}
|
||||
|
||||
Section {
|
||||
ListRow(label: .default(title: "Counter", icon: \.chart),
|
||||
details: .counter(context.viewState.counter),
|
||||
kind: .label)
|
||||
ListRow(label: .default(title: "Increment", icon: \.plus),
|
||||
kind: .button { context.send(viewAction: .incrementCounter) })
|
||||
ListRow(label: .default(title: "Decrement", icon: \.minus),
|
||||
kind: .button { context.send(viewAction: .decrementCounter) })
|
||||
}
|
||||
}
|
||||
.compoundList()
|
||||
.navigationTitle(context.viewState.title)
|
||||
@@ -33,10 +43,29 @@ struct TemplateScreen: View {
|
||||
// MARK: - Previews
|
||||
|
||||
struct TemplateScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = TemplateScreenViewModel()
|
||||
static let viewModel = makeViewModel()
|
||||
static let incrementedViewModel = makeViewModel(counterValue: 1)
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
TemplateScreen(context: viewModel.context)
|
||||
}
|
||||
.previewDisplayName("Initial")
|
||||
|
||||
NavigationStack {
|
||||
TemplateScreen(context: incrementedViewModel.context)
|
||||
}
|
||||
.previewDisplayName("Incremented")
|
||||
.snapshotPreferences(expect: incrementedViewModel.context.observe(\.viewState.counter).map { $0 == 1 }.eraseToStream())
|
||||
}
|
||||
|
||||
static func makeViewModel(counterValue: Int = 0) -> TemplateScreenViewModel {
|
||||
let viewModel = TemplateScreenViewModel()
|
||||
|
||||
for _ in 0..<counterValue {
|
||||
viewModel.context.send(viewAction: .incrementCounter)
|
||||
}
|
||||
|
||||
return viewModel
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,13 +22,31 @@ class TemplateScreenViewModelTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testInitialState() {
|
||||
XCTAssertFalse(context.viewState.placeholder.isEmpty)
|
||||
XCTAssertFalse(context.composerText.isEmpty)
|
||||
XCTAssertEqual(context.viewState.counter, 0)
|
||||
}
|
||||
|
||||
func testCounter() async throws {
|
||||
func testTextField() async throws {
|
||||
context.composerText = "123"
|
||||
context.send(viewAction: .textChanged)
|
||||
XCTAssertEqual(context.composerText, "123")
|
||||
}
|
||||
|
||||
func testCounter() async throws {
|
||||
var deferred = deferFulfillment(context.observe(\.viewState.counter)) { $0 == 1 }
|
||||
context.send(viewAction: .incrementCounter)
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(context.viewState.counter, 1)
|
||||
|
||||
deferred = deferFulfillment(context.observe(\.viewState.counter)) { $0 == 3 }
|
||||
context.send(viewAction: .incrementCounter)
|
||||
context.send(viewAction: .incrementCounter)
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(context.viewState.counter, 3)
|
||||
|
||||
deferred = deferFulfillment(context.observe(\.viewState.counter)) { $0 == 2 }
|
||||
context.send(viewAction: .decrementCounter)
|
||||
try await deferred.fulfill()
|
||||
XCTAssertEqual(context.viewState.counter, 2)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user