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:
Doug
2025-04-30 16:49:23 +01:00
committed by GitHub
parent b4ee531a13
commit a67559299a
12 changed files with 297 additions and 25 deletions

View File

@@ -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
}

View File

@@ -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
}
}
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}