Made StateStoreViewModel state mutable and removed the reducer for all the features using it.

This commit is contained in:
Stefan Ceriu
2022-01-28 12:58:31 +02:00
committed by Stefan Ceriu
parent fc9e95aee8
commit 313b05485a
42 changed files with 324 additions and 431 deletions
@@ -16,9 +16,6 @@
import Foundation
// The state is never modified so this is unnecessary.
enum AnalyticsPromptStateAction { }
enum AnalyticsPromptViewAction {
/// Enable analytics.
case enable
@@ -19,7 +19,7 @@ import Combine
@available(iOS 14, *)
typealias AnalyticsPromptViewModelType = StateStoreViewModel<AnalyticsPromptViewState,
AnalyticsPromptStateAction,
Never,
AnalyticsPromptViewAction>
@available(iOS 14, *)
class AnalyticsPromptViewModel: AnalyticsPromptViewModelType {
@@ -54,10 +54,6 @@ class AnalyticsPromptViewModel: AnalyticsPromptViewModelType {
openTermsURL()
}
}
override class func reducer(state: inout AnalyticsPromptViewState, action: AnalyticsPromptStateAction) {
// There is no mutable state to reduce :)
}
/// Enable analytics. The call to the Analytics class is made in the completion.
private func enable() {
@@ -92,9 +92,9 @@ class StateStoreViewModel<State: BindableState, StateAction, ViewAction> {
/// Constrained interface for passing to Views.
var context: Context
/// State can be read within the 'ViewModel' but not modified outside of the reducer.
var state: State {
context.viewState
get { context.viewState }
set { context.viewState = newValue }
}
// MARK: Setup
@@ -110,12 +110,14 @@ class StateStoreViewModel<State: BindableState, StateAction, ViewAction> {
/// Send state actions to modify the state within the reducer.
/// - Parameter action: The state action to send to the reducer.
@available(*, deprecated, message: "Mutate state directly instead")
func dispatch(action: StateAction) {
Self.reducer(state: &context.viewState, action: action)
}
/// Send state actions from a publisher to modify the state within the reducer.
/// - Parameter actionPublisher: The publisher that produces actions to be sent to the reducer
@available(*, deprecated, message: "Mutate state directly instead")
func dispatch(actionPublisher: AnyPublisher<StateAction, Never>) {
actionPublisher.sink { [weak self] action in
guard let self = self else { return }
@@ -30,10 +30,6 @@ struct OnboardingSplashScreenPageContent {
// MARK: View model
enum OnboardingSplashScreenStateAction {
case viewAction(OnboardingSplashScreenViewAction)
}
enum OnboardingSplashScreenViewModelResult {
case register
case login
@@ -19,7 +19,7 @@ import Combine
@available(iOS 14, *)
typealias OnboardingSplashScreenViewModelType = StateStoreViewModel<OnboardingSplashScreenViewState,
OnboardingSplashScreenStateAction,
Never,
OnboardingSplashScreenViewAction>
protocol OnboardingSplashScreenViewModelProtocol {
@@ -54,31 +54,18 @@ class OnboardingSplashScreenViewModel: OnboardingSplashScreenViewModelType, Onbo
register()
case .login:
login()
case .nextPage, .previousPage, .hiddenPage:
dispatch(action: .viewAction(viewAction))
case .nextPage:
// Wrap back round to the first page index when reaching the end.
state.bindings.pageIndex = (state.bindings.pageIndex + 1) % state.content.count
case .previousPage:
// Prevent the hidden page at index -1 from being shown.
state.bindings.pageIndex = max(0, (state.bindings.pageIndex - 1))
case .hiddenPage:
// Hidden page for a nicer animation when looping back to the start.
state.bindings.pageIndex = -1
}
}
override class func reducer(state: inout OnboardingSplashScreenViewState, action: OnboardingSplashScreenStateAction) {
switch action {
case .viewAction(let viewAction):
switch viewAction {
case .nextPage:
// Wrap back round to the first page index when reaching the end.
state.bindings.pageIndex = (state.bindings.pageIndex + 1) % state.content.count
case .previousPage:
// Prevent the hidden page at index -1 from being shown.
state.bindings.pageIndex = max(0, (state.bindings.pageIndex - 1))
case .hiddenPage:
// Hidden page for a nicer animation when looping back to the start.
state.bindings.pageIndex = -1
case .login, .register:
break
}
}
UILog.debug("[OnboardingSplashScreenViewModel] reducer with action \(action) produced state: \(state)")
}
private func register() {
completion?(.register)
}
@@ -21,5 +21,5 @@ import Combine
@available(iOS 14.0, *)
class OnboardingSplashScreenViewModelTests: XCTestCase {
// TODO: Check for any useful tests when finished
}
@@ -33,12 +33,7 @@ final class LocationSharingCoordinator: Coordinator, Presentable {
private let parameters: LocationSharingCoordinatorParameters
private let locationSharingHostingController: UIViewController
private var _locationSharingViewModel: Any? = nil
@available(iOS 14.0, *)
fileprivate var locationSharingViewModel: LocationSharingViewModel {
return _locationSharingViewModel as! LocationSharingViewModel
}
private var locationSharingViewModel: LocationSharingViewModelProtocol
// MARK: Public
@@ -58,7 +53,7 @@ final class LocationSharingCoordinator: Coordinator, Presentable {
let view = LocationSharingView(context: viewModel.context)
.addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager))
_locationSharingViewModel = viewModel
locationSharingViewModel = viewModel
locationSharingHostingController = VectorHostingController(rootView: view)
}
@@ -81,20 +76,18 @@ final class LocationSharingCoordinator: Coordinator, Presentable {
return
}
self.locationSharingViewModel.dispatch(action: .startLoading)
self.locationSharingViewModel.startLoading()
self.parameters.roomDataSource.sendLocation(withLatitude: latitude,
longitude: longitude,
description: nil) { [weak self] _ in
self.parameters.roomDataSource.sendLocation(withLatitude: latitude, longitude: longitude, description: nil) { [weak self] _ in
guard let self = self else { return }
self.locationSharingViewModel.dispatch(action: .stopLoading(nil))
self.locationSharingViewModel.stopLoading()
self.completion?()
} failure: { [weak self] error in
guard let self = self else { return }
MXLog.error("[LocationSharingCoordinator] Failed sharing location with error: \(String(describing: error))")
self.locationSharingViewModel.dispatch(action: .stopLoading(error))
self.locationSharingViewModel.stopLoading(error: .locationSharingError)
}
}
@@ -19,31 +19,23 @@ import SwiftUI
import Combine
import CoreLocation
enum LocationSharingViewError {
case failedLoadingMap
case failedLocatingUser
case invalidLocationAuthorization
case failedSharingLocation
}
enum LocationSharingStateAction {
case error(LocationSharingViewError, LocationSharingViewModelCallback?)
case startLoading
case stopLoading(Error?)
}
enum LocationSharingViewAction {
case cancel
case share
}
typealias LocationSharingViewModelCallback = ((LocationSharingViewModelResult) -> Void)
enum LocationSharingViewModelResult {
case cancel
case share(latitude: Double, longitude: Double)
}
enum LocationSharingViewError {
case failedLoadingMap
case failedLocatingUser
case invalidLocationAuthorization
case failedSharingLocation
}
@available(iOS 14, *)
struct LocationSharingViewState: BindableState {
let tileServerMapURL: URL
@@ -80,6 +72,7 @@ struct LocationSharingErrorAlertInfo: Identifiable {
let id: AlertType
let title: String
var subtitle: String? = nil
let primaryButton: (title: String, action: (() -> Void)?)
let secondaryButton: (title: String, action: (() -> Void)?)?
var secondaryButton: (title: String, action: (() -> Void)?)? = nil
}
@@ -19,11 +19,11 @@ import Combine
import CoreLocation
@available(iOS 14, *)
typealias LocationSharingViewModelType = StateStoreViewModel< LocationSharingViewState,
LocationSharingStateAction,
LocationSharingViewAction >
typealias LocationSharingViewModelType = StateStoreViewModel<LocationSharingViewState,
Never,
LocationSharingViewAction>
@available(iOS 14, *)
class LocationSharingViewModel: LocationSharingViewModelType {
class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingViewModelProtocol {
// MARK: - Properties
@@ -41,7 +41,7 @@ class LocationSharingViewModel: LocationSharingViewModelType {
state.errorSubject.sink { [weak self] error in
guard let self = self else { return }
self.dispatch(action: .error(error, self.completion))
self.processError(error)
}.store(in: &cancellables)
}
@@ -58,7 +58,7 @@ class LocationSharingViewModel: LocationSharingViewModelType {
}
guard let location = state.bindings.userLocation else {
dispatch(action: .error(.failedLocatingUser, completion))
processError(.failedLocatingUser)
return
}
@@ -66,45 +66,54 @@ class LocationSharingViewModel: LocationSharingViewModelType {
}
}
override class func reducer(state: inout LocationSharingViewState, action: LocationSharingStateAction) {
switch action {
case .error(let error, let completion):
switch error {
case .failedLoadingMap:
state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .mapLoadingError,
title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName) ,
primaryButton: (VectorL10n.ok, { completion?(.cancel) }),
secondaryButton: nil)
case .failedLocatingUser:
state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .userLocatingError,
title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName),
primaryButton: (VectorL10n.ok, { completion?(.cancel) }),
secondaryButton: nil)
case .invalidLocationAuthorization:
state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .authorizationError,
title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName),
primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, { completion?(.cancel) }),
secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, {
if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) {
UIApplication.shared.open(applicationSettingsURL)
}
}))
default:
break
}
case .startLoading:
state.showLoadingIndicator = true
case .stopLoading(let error):
state.showLoadingIndicator = false
if error != nil {
state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .locationSharingError,
title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName),
primaryButton: (VectorL10n.ok, nil),
secondaryButton: nil)
}
// MARK: - LocationSharingViewModelProtocol
public func startLoading() {
state.showLoadingIndicator = true
}
func stopLoading(error: LocationSharingErrorAlertInfo.AlertType?) {
state.showLoadingIndicator = false
if let error = error {
state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: error,
title: VectorL10n.locationSharingPostFailureTitle,
subtitle: VectorL10n.locationSharingPostFailureSubtitle(AppInfo.current.displayName),
primaryButton: (VectorL10n.ok, nil))
}
}
// MARK: - Private
private func processError(_ error: LocationSharingViewError) {
guard state.bindings.alertInfo == nil else {
return
}
let primaryButtonCompletion = { [weak self] () -> Void in
self?.completion?(.cancel)
}
switch error {
case .failedLoadingMap:
state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .mapLoadingError,
title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName),
primaryButton: (VectorL10n.ok, primaryButtonCompletion))
case .failedLocatingUser:
state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .userLocatingError,
title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName),
primaryButton: (VectorL10n.ok, primaryButtonCompletion))
case .invalidLocationAuthorization:
state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .authorizationError,
title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName),
primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, primaryButtonCompletion),
secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, {
if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) {
UIApplication.shared.open(applicationSettingsURL)
}
}))
default:
break
}
}
}
@@ -16,7 +16,15 @@
import Foundation
@available(iOS 14.0, *)
enum UserSuggestionStateAction {
case updateWithItems([UserSuggestionItemProtocol])
protocol LocationSharingViewModelProtocol {
var completion: ((LocationSharingViewModelResult) -> Void)? { get set }
func startLoading()
func stopLoading(error: LocationSharingErrorAlertInfo.AlertType?)
}
extension LocationSharingViewModelProtocol {
func stopLoading() {
stopLoading(error: nil)
}
}
@@ -41,7 +41,7 @@ class LocationSharingUITests: XCTestCase {
goToScreenWithIdentifier(MockLocationSharingScreenState.displayExistingLocation.title)
XCTAssertTrue(app.buttons["Cancel"].exists)
XCTAssertTrue(app.buttons["location share icon"].exists)
XCTAssertTrue(app.buttons["LocationSharingView.shareButton"].exists)
XCTAssertTrue(app.otherElements["Map"].exists)
}
@@ -100,12 +100,12 @@ class LocationSharingViewModelTests: XCTestCase {
func testLoading() {
let viewModel = buildViewModel(withLocation: false)
viewModel.dispatch(action: .startLoading)
viewModel.startLoading()
XCTAssertFalse(viewModel.context.viewState.shareButtonEnabled)
XCTAssertTrue(viewModel.context.viewState.showLoadingIndicator)
viewModel.dispatch(action: .stopLoading(nil))
viewModel.stopLoading()
XCTAssertTrue(viewModel.context.viewState.shareButtonEnabled)
XCTAssertFalse(viewModel.context.viewState.showLoadingIndicator)
@@ -68,7 +68,7 @@ class LocationSharingMapViewCoordinator: NSObject, MGLMapViewDelegate {
private let avatarData: AvatarInputProtocol
private let errorSubject: PassthroughSubject<LocationSharingViewError, Never>
@Binding var userLocation: CLLocationCoordinate2D?
@Binding private var userLocation: CLLocationCoordinate2D?
init(avatarData: AvatarInputProtocol,
errorSubject: PassthroughSubject<LocationSharingViewError, Never>,
@@ -89,6 +89,10 @@ class LocationSharingMapViewCoordinator: NSObject, MGLMapViewDelegate {
}
func mapView(_ mapView: MGLMapView, didFailToLocateUserWithError error: Error) {
guard mapView.showsUserLocation else {
return
}
errorSubject.send(.failedLocatingUser)
}
@@ -97,11 +101,15 @@ class LocationSharingMapViewCoordinator: NSObject, MGLMapViewDelegate {
}
func mapView(_ mapView: MGLMapView, didChangeLocationManagerAuthorization manager: MGLLocationManager) {
guard mapView.showsUserLocation else {
return
}
switch manager.authorizationStatus {
case .restricted:
fallthrough
case .denied:
errorSubject.send(.failedLocatingUser)
errorSubject.send(.invalidLocationAuthorization)
default:
break
}
@@ -54,6 +54,7 @@ struct LocationSharingView: View {
context.send(viewAction: .share)
} label: {
Image(uiImage: Asset.Images.locationShareIcon.image)
.accessibilityIdentifier("LocationSharingView.shareButton")
}
.disabled(!context.viewState.shareButtonEnabled)
} else {
@@ -69,6 +70,7 @@ struct LocationSharingView: View {
.alert(item: $context.alertInfo) { info in
if let secondaryButton = info.secondaryButton {
return Alert(title: Text(info.title),
message: subtitleTextForAlertInfo(info),
primaryButton: .default(Text(info.primaryButton.title)) {
info.primaryButton.action?()
},
@@ -77,6 +79,7 @@ struct LocationSharingView: View {
})
} else {
return Alert(title: Text(info.title),
message: subtitleTextForAlertInfo(info),
dismissButton: .default(Text(info.primaryButton.title)) {
info.primaryButton.action?()
})
@@ -93,6 +96,14 @@ struct LocationSharingView: View {
ActivityIndicator()
}
}
private func subtitleTextForAlertInfo(_ alertInfo: LocationSharingErrorAlertInfo) -> Text? {
guard let subtitle = alertInfo.subtitle else {
return nil
}
return Text(subtitle)
}
}
// MARK: - Previews
@@ -31,12 +31,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable {
private let parameters: PollEditFormCoordinatorParameters
private let pollEditFormHostingController: UIViewController
private var _pollEditFormViewModel: Any? = nil
@available(iOS 14.0, *)
fileprivate var pollEditFormViewModel: PollEditFormViewModel {
return _pollEditFormViewModel as! PollEditFormViewModel
}
private var pollEditFormViewModel: PollEditFormViewModelProtocol
// MARK: Public
@@ -64,7 +59,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable {
let view = PollEditForm(viewModel: viewModel.context)
_pollEditFormViewModel = viewModel
pollEditFormViewModel = viewModel
pollEditFormHostingController = VectorHostingController(rootView: view)
}
@@ -84,18 +79,18 @@ final class PollEditFormCoordinator: Coordinator, Presentable {
let pollStartContent = self.buildPollContentWithDetails(details)
self.pollEditFormViewModel.dispatch(action: .startLoading)
self.pollEditFormViewModel.startLoading()
self.parameters.room.sendPollStart(withContent: pollStartContent, threadId: nil, localEcho: nil) { [weak self] result in
guard let self = self else { return }
self.pollEditFormViewModel.dispatch(action: .stopLoading(nil))
self.pollEditFormViewModel.stopLoading()
self.completion?()
} failure: { [weak self] error in
guard let self = self else { return }
MXLog.error("Failed creating poll with error: \(String(describing: error))")
self.pollEditFormViewModel.dispatch(action: .stopLoading(.failedCreatingPoll))
self.pollEditFormViewModel.stopLoading(errorAlertType: .failedCreatingPoll)
}
case .update(let details):
@@ -103,10 +98,10 @@ final class PollEditFormCoordinator: Coordinator, Presentable {
fatalError()
}
self.pollEditFormViewModel.dispatch(action: .startLoading)
self.pollEditFormViewModel.startLoading()
guard let oldPollContent = MXEventContentPollStart(fromJSON: pollStartEvent.content) else {
self.pollEditFormViewModel.dispatch(action: .stopLoading(.failedUpdatingPoll))
self.pollEditFormViewModel.stopLoading(errorAlertType: .failedUpdatingPoll)
return
}
@@ -117,13 +112,13 @@ final class PollEditFormCoordinator: Coordinator, Presentable {
newContent: newPollContent, localEcho: nil) { [weak self] result in
guard let self = self else { return }
self.pollEditFormViewModel.dispatch(action: .stopLoading(nil))
self.pollEditFormViewModel.stopLoading()
self.completion?()
} failure: { [weak self] error in
guard let self = self else { return }
MXLog.error("Failed updating poll with error: \(String(describing: error))")
self.pollEditFormViewModel.dispatch(action: .stopLoading(.failedUpdatingPoll))
self.pollEditFormViewModel.stopLoading(errorAlertType: .failedUpdatingPoll)
}
}
}
@@ -38,12 +38,6 @@ enum PollEditFormMode {
case editing
}
enum PollEditFormStateAction {
case viewAction(PollEditFormViewAction)
case startLoading
case stopLoading(PollEditFormErrorAlertInfo.AlertType?)
}
enum PollEditFormViewAction {
case addAnswerOption
case deleteAnswerOption(PollEditFormAnswerOption)
@@ -23,11 +23,11 @@ struct PollEditFormViewModelParameters {
}
@available(iOS 14, *)
typealias PollEditFormViewModelType = StateStoreViewModel< PollEditFormViewState,
PollEditFormStateAction,
PollEditFormViewAction >
typealias PollEditFormViewModelType = StateStoreViewModel <PollEditFormViewState,
Never,
PollEditFormViewAction>
@available(iOS 14, *)
class PollEditFormViewModel: PollEditFormViewModelType {
class PollEditFormViewModel: PollEditFormViewModelType, PollEditFormViewModelProtocol {
private struct Constants {
static let minAnswerOptionsCount = 2
@@ -71,40 +71,32 @@ class PollEditFormViewModel: PollEditFormViewModelType {
completion?(.create(buildPollDetails()))
case .update:
completion?(.update(buildPollDetails()))
default:
dispatch(action: .viewAction(viewAction))
case .addAnswerOption:
state.bindings.answerOptions.append(PollEditFormAnswerOption(text: "", maxLength: Constants.maxAnswerOptionLength))
case .deleteAnswerOption(let answerOption):
state.bindings.answerOptions.removeAll { $0 == answerOption }
}
}
override class func reducer(state: inout PollEditFormViewState, action: PollEditFormStateAction) {
switch action {
case .viewAction(let viewAction):
switch viewAction {
case .deleteAnswerOption(let answerOption):
state.bindings.answerOptions.removeAll { $0 == answerOption }
case .addAnswerOption:
state.bindings.answerOptions.append(PollEditFormAnswerOption(text: "", maxLength: Constants.maxAnswerOptionLength))
default:
break
}
case .startLoading:
state.showLoadingIndicator = true
break
case .stopLoading(let error):
state.showLoadingIndicator = false
switch error {
case .failedCreatingPoll:
state.bindings.alertInfo = PollEditFormErrorAlertInfo(id: .failedCreatingPoll,
title: VectorL10n.pollEditFormPostFailureTitle,
subtitle: VectorL10n.pollEditFormPostFailureSubtitle)
case .failedUpdatingPoll:
state.bindings.alertInfo = PollEditFormErrorAlertInfo(id: .failedUpdatingPoll,
title: VectorL10n.pollEditFormUpdateFailureTitle,
subtitle: VectorL10n.pollEditFormUpdateFailureSubtitle)
case .none:
break
}
// MARK: - PollEditFormViewModelProtocol
func startLoading() {
state.showLoadingIndicator = true
}
func stopLoading(errorAlertType: PollEditFormErrorAlertInfo.AlertType?) {
state.showLoadingIndicator = false
switch errorAlertType {
case .failedCreatingPoll:
state.bindings.alertInfo = PollEditFormErrorAlertInfo(id: .failedCreatingPoll,
title: VectorL10n.pollEditFormPostFailureTitle,
subtitle: VectorL10n.pollEditFormPostFailureSubtitle)
case .failedUpdatingPoll:
state.bindings.alertInfo = PollEditFormErrorAlertInfo(id: .failedUpdatingPoll,
title: VectorL10n.pollEditFormUpdateFailureTitle,
subtitle: VectorL10n.pollEditFormUpdateFailureSubtitle)
case .none:
break
}
}
@@ -115,8 +107,8 @@ class PollEditFormViewModel: PollEditFormViewModelType {
return EditFormPollDetails(type: state.bindings.type,
question: state.bindings.question.text.trimmingCharacters(in: .whitespacesAndNewlines),
answerOptions: state.bindings.answerOptions.compactMap({ answerOption in
let text = answerOption.text.trimmingCharacters(in: .whitespacesAndNewlines)
return text.isEmpty ? nil : text
}))
let text = answerOption.text.trimmingCharacters(in: .whitespacesAndNewlines)
return text.isEmpty ? nil : text
}))
}
}
@@ -16,7 +16,15 @@
import Foundation
@available(iOS 14, *)
enum UserSuggestionViewAction {
case selectedItem(UserSuggestionViewStateItem)
protocol PollEditFormViewModelProtocol {
var completion: ((PollEditFormViewModelResult) -> Void)? { get set }
func startLoading()
func stopLoading(errorAlertType: PollEditFormErrorAlertInfo.AlertType?)
}
extension PollEditFormViewModelProtocol {
func stopLoading() {
stopLoading(errorAlertType: nil)
}
}
@@ -35,7 +35,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel
private let selectedAnswerIdentifiersSubject = PassthroughSubject<[String], Never>()
private var pollAggregator: PollAggregator
private var viewModel: TimelinePollViewModel!
private var viewModel: TimelinePollViewModelProtocol!
private var cancellables = Set<AnyCancellable>()
// MARK: Public
@@ -53,7 +53,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel
pollAggregator.delegate = self
viewModel = TimelinePollViewModel(timelinePollDetails: buildTimelinePollFrom(pollAggregator.poll))
viewModel.callback = { [weak self] result in
viewModel.completion = { [weak self] result in
guard let self = self else { return }
switch result {
@@ -76,7 +76,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel
MXLog.error("[TimelinePollCoordinator]] Failed submitting response with error \(String(describing: error))")
self.viewModel.dispatch(action: .showAnsweringFailure)
self.viewModel.showAnsweringFailure()
}
}
.store(in: &cancellables)
@@ -102,14 +102,14 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel
func endPoll() {
parameters.room.sendPollEnd(for: parameters.pollStartEvent, threadId: nil, localEcho: nil, success: nil) { [weak self] error in
self?.viewModel.dispatch(action: .showClosingFailure)
self?.viewModel.showClosingFailure()
}
}
// MARK: - PollAggregatorDelegate
func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) {
viewModel.dispatch(action: .updateWithPoll(buildTimelinePollFrom(aggregator.poll)))
viewModel.updateWithPollDetails(buildTimelinePollFrom(aggregator.poll))
}
func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) {
@@ -85,7 +85,7 @@ class TimelinePollViewModelTests: XCTestCase {
}
func testClosedSelection() {
context.viewState.poll.closed = true
viewModel.state.poll.closed = true
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
context.send(viewAction: .selectAnswerOptionWithIdentifier("3"))
@@ -96,7 +96,7 @@ class TimelinePollViewModelTests: XCTestCase {
}
func testSingleSelectionOnMax2Allowed() {
context.viewState.poll.maxAllowedSelections = 2
viewModel.state.poll.maxAllowedSelections = 2
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
@@ -106,7 +106,7 @@ class TimelinePollViewModelTests: XCTestCase {
}
func testSingleReselectionOnMax2Allowed() {
context.viewState.poll.maxAllowedSelections = 2
viewModel.state.poll.maxAllowedSelections = 2
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
@@ -117,7 +117,7 @@ class TimelinePollViewModelTests: XCTestCase {
}
func testMultipleSelectionOnMax2Allowed() {
context.viewState.poll.maxAllowedSelections = 2
viewModel.state.poll.maxAllowedSelections = 2
context.send(viewAction: .selectAnswerOptionWithIdentifier("1"))
context.send(viewAction: .selectAnswerOptionWithIdentifier("3"))
@@ -19,13 +19,6 @@ import SwiftUI
typealias TimelinePollViewModelCallback = ((TimelinePollViewModelResult) -> Void)
enum TimelinePollStateAction {
case viewAction(TimelinePollViewAction, TimelinePollViewModelCallback?)
case updateWithPoll(TimelinePollDetails)
case showAnsweringFailure
case showClosingFailure
}
enum TimelinePollViewAction {
case selectAnswerOptionWithIdentifier(String)
}
@@ -39,7 +32,7 @@ enum TimelinePollType {
case undisclosed
}
class TimelinePollAnswerOption: Identifiable {
struct TimelinePollAnswerOption: Identifiable {
var id: String
var text: String
var count: UInt
@@ -55,7 +48,15 @@ class TimelinePollAnswerOption: Identifiable {
}
}
class TimelinePollDetails {
extension MutableCollection where Element == TimelinePollAnswerOption {
mutating func updateEach(_ update: (inout Element) -> Void) {
for index in indices {
update(&self[index])
}
}
}
struct TimelinePollDetails {
var question: String
var answerOptions: [TimelinePollAnswerOption]
var closed: Bool
@@ -19,10 +19,10 @@ import Combine
@available(iOS 14, *)
typealias TimelinePollViewModelType = StateStoreViewModel<TimelinePollViewState,
TimelinePollStateAction,
Never,
TimelinePollViewAction>
@available(iOS 14, *)
class TimelinePollViewModel: TimelinePollViewModelType {
class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelProtocol {
// MARK: - Properties
@@ -30,7 +30,7 @@ class TimelinePollViewModel: TimelinePollViewModelType {
// MARK: Public
var callback: TimelinePollViewModelCallback?
var completion: TimelinePollViewModelCallback?
// MARK: - Setup
@@ -42,49 +42,47 @@ class TimelinePollViewModel: TimelinePollViewModelType {
override func process(viewAction: TimelinePollViewAction) {
switch viewAction {
case .selectAnswerOptionWithIdentifier(_):
dispatch(action: .viewAction(viewAction, callback))
}
}
override class func reducer(state: inout TimelinePollViewState, action: TimelinePollStateAction) {
switch action {
case .viewAction(let viewAction, let callback):
switch viewAction {
// Update local state. An update will be pushed from the coordinator once sent.
case .selectAnswerOptionWithIdentifier(let identifier):
guard !state.poll.closed else {
return
}
if (state.poll.maxAllowedSelections == 1) {
updateSingleSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: callback)
} else {
updateMultiSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: callback)
}
// Update local state. An update will be pushed from the coordinator once sent.
case .selectAnswerOptionWithIdentifier(let identifier):
guard !state.poll.closed else {
return
}
if (state.poll.maxAllowedSelections == 1) {
updateSingleSelectPollLocalState(selectedAnswerIdentifier: identifier, callback: completion)
} else {
updateMultiSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: completion)
}
case .updateWithPoll(let poll):
state.poll = poll
case .showAnsweringFailure:
state.bindings.alertInfo = TimelinePollErrorAlertInfo(id: .failedSubmittingAnswer,
title: VectorL10n.pollTimelineVoteNotRegisteredTitle,
subtitle: VectorL10n.pollTimelineVoteNotRegisteredSubtitle)
case .showClosingFailure:
state.bindings.alertInfo = TimelinePollErrorAlertInfo(id: .failedClosingPoll,
title: VectorL10n.pollTimelineNotClosedTitle,
subtitle: VectorL10n.pollTimelineNotClosedSubtitle)
}
}
// MARK: - TimelinePollViewModelProtocol
func updateWithPollDetails(_ pollDetails: TimelinePollDetails) {
state.poll = pollDetails
}
func showAnsweringFailure() {
state.bindings.alertInfo = TimelinePollErrorAlertInfo(id: .failedSubmittingAnswer,
title: VectorL10n.pollTimelineVoteNotRegisteredTitle,
subtitle: VectorL10n.pollTimelineVoteNotRegisteredSubtitle)
}
func showClosingFailure() {
state.bindings.alertInfo = TimelinePollErrorAlertInfo(id: .failedClosingPoll,
title: VectorL10n.pollTimelineNotClosedTitle,
subtitle: VectorL10n.pollTimelineNotClosedSubtitle)
}
// MARK: - Private
static func updateSingleSelectPollLocalState(_ state: inout TimelinePollViewState, selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) {
for answerOption in state.poll.answerOptions {
func updateSingleSelectPollLocalState(selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) {
state.poll.answerOptions.updateEach { answerOption in
if answerOption.selected {
answerOption.selected = false
if(answerOption.count > 0) {
if(state.poll.answerOptions.count > 0) {
answerOption.count = answerOption.count - 1
state.poll.totalAnswerCount -= 1
}
@@ -100,7 +98,7 @@ class TimelinePollViewModel: TimelinePollViewModelType {
informCoordinatorOfSelectionUpdate(state: state, callback: callback)
}
static func updateMultiSelectPollLocalState(_ state: inout TimelinePollViewState, selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) {
func updateMultiSelectPollLocalState(_ state: inout TimelinePollViewState, selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) {
let selectedAnswerOptions = state.poll.answerOptions.filter { $0.selected == true }
let isDeselecting = selectedAnswerOptions.filter { $0.id == selectedAnswerIdentifier }.count > 0
@@ -109,7 +107,11 @@ class TimelinePollViewModel: TimelinePollViewModelType {
return
}
for answerOption in state.poll.answerOptions where answerOption.id == selectedAnswerIdentifier {
state.poll.answerOptions.updateEach { answerOption in
if (answerOption.id != selectedAnswerIdentifier) {
return
}
if answerOption.selected {
answerOption.selected = false
answerOption.count -= 1
@@ -124,7 +126,7 @@ class TimelinePollViewModel: TimelinePollViewModelType {
informCoordinatorOfSelectionUpdate(state: state, callback: callback)
}
static func informCoordinatorOfSelectionUpdate(state: TimelinePollViewState, callback: TimelinePollViewModelCallback?) {
func informCoordinatorOfSelectionUpdate(state: TimelinePollViewState, callback: TimelinePollViewModelCallback?) {
let selectedIdentifiers = state.poll.answerOptions.compactMap { answerOption in
answerOption.selected ? answerOption.id : nil
}
@@ -16,7 +16,12 @@
import Foundation
struct UserSuggestionCoordinatorParameters {
let mediaManager: MXMediaManager
let room: MXRoom
protocol TimelinePollViewModelProtocol {
@available(iOS 14, *)
var context: TimelinePollViewModelType.Context { get }
var completion: ((TimelinePollViewModelResult) -> Void)? { get set }
func updateWithPollDetails(_ pollDetails: TimelinePollDetails)
func showAnsweringFailure()
func showClosingFailure()
}
@@ -23,6 +23,11 @@ protocol UserSuggestionCoordinatorDelegate: AnyObject {
func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?)
}
struct UserSuggestionCoordinatorParameters {
let mediaManager: MXMediaManager
let room: MXRoom
}
@available(iOS 14.0, *)
final class UserSuggestionCoordinator: Coordinator, Presentable {
@@ -53,11 +58,12 @@ final class UserSuggestionCoordinator: Coordinator, Presentable {
roomMemberProvider = UserSuggestionCoordinatorRoomMemberProvider(room: parameters.room)
userSuggestionService = UserSuggestionService(roomMemberProvider: roomMemberProvider)
userSuggestionViewModel = UserSuggestionViewModel.makeUserSuggestionViewModel(userSuggestionService: userSuggestionService)
let view = UserSuggestionList(viewModel: userSuggestionViewModel.context)
let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService)
let view = UserSuggestionList(viewModel: viewModel.context)
.addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager))
userSuggestionViewModel = viewModel
userSuggestionHostingController = VectorHostingController(rootView: view)
userSuggestionViewModel.completion = { [weak self] result in
@@ -90,7 +96,6 @@ final class UserSuggestionCoordinator: Coordinator, Presentable {
}
}
@available(iOS 14.0, *)
private class UserSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderProtocol {
private let room: MXRoom
@@ -1,22 +0,0 @@
//
// Copyright 2021 New Vector Ltd
//
// 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
@available(iOS 14, *)
enum UserSuggestionViewModelResult {
case selectedItemWithIdentifier(String)
}
@@ -17,19 +17,16 @@
import Foundation
import Combine
@available(iOS 14.0, *)
struct RoomMembersProviderMember {
var userId: String
var displayName: String
var avatarUrl: String
}
@available(iOS 14.0, *)
protocol RoomMembersProviderProtocol {
func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void)
}
@available(iOS 14.0, *)
struct UserSuggestionServiceItem: UserSuggestionItemProtocol {
let userId: String
let displayName: String?
@@ -17,7 +17,6 @@
import Foundation
import Combine
@available(iOS 14.0, *)
protocol UserSuggestionItemProtocol: Avatarable {
var userId: String { get }
var displayName: String? { get }
@@ -36,7 +35,6 @@ protocol UserSuggestionServiceProtocol {
// MARK: Avatarable
@available(iOS 14.0, *)
extension UserSuggestionItemProtocol {
var mxContentUri: String? {
avatarUrl
@@ -15,16 +15,21 @@
//
import Foundation
import Combine
@available(iOS 14.0, *)
enum UserSuggestionViewAction {
case selectedItem(UserSuggestionViewStateItem)
}
enum UserSuggestionViewModelResult {
case selectedItemWithIdentifier(String)
}
struct UserSuggestionViewStateItem: Identifiable {
let id: String
let avatar: AvatarInputProtocol?
let displayName: String?
}
@available(iOS 14.0, *)
struct UserSuggestionViewState: BindableState {
var items: [UserSuggestionViewStateItem]
}
@@ -29,7 +29,7 @@ enum MockUserSuggestionScreenState: MockScreenState, CaseIterable {
var screenView: ([Any], AnyView) {
let service = UserSuggestionService(roomMemberProvider: self)
let listViewModel = UserSuggestionViewModel.makeUserSuggestionViewModel(userSuggestionService: service)
let listViewModel = UserSuggestionViewModel(userSuggestionService: service)
let viewModel = UserSuggestionListWithInputViewModel(listViewModel: listViewModel) { textMessage in
service.processTextMessage(textMessage)
@@ -17,11 +17,12 @@
import SwiftUI
import Combine
@available(iOS 14, *)
@available(iOS 14.0, *)
typealias UserSuggestionViewModelType = StateStoreViewModel <UserSuggestionViewState,
UserSuggestionStateAction,
Never,
UserSuggestionViewAction>
@available(iOS 14, *)
@available(iOS 14.0, *)
class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewModelProtocol {
// MARK: - Properties
@@ -35,30 +36,21 @@ class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewMo
var completion: ((UserSuggestionViewModelResult) -> Void)?
// MARK: - Setup
static func makeUserSuggestionViewModel(userSuggestionService: UserSuggestionServiceProtocol) -> UserSuggestionViewModelProtocol {
return UserSuggestionViewModel(userSuggestionService: userSuggestionService)
}
private init(userSuggestionService: UserSuggestionServiceProtocol) {
init(userSuggestionService: UserSuggestionServiceProtocol) {
self.userSuggestionService = userSuggestionService
super.init(initialViewState: Self.defaultState(userSuggestionService: userSuggestionService))
setupItemsObserving()
}
private func setupItemsObserving() {
let updatePublisher = userSuggestionService.items
.map(UserSuggestionStateAction.updateWithItems)
.eraseToAnyPublisher()
dispatch(actionPublisher: updatePublisher)
}
private static func defaultState(userSuggestionService: UserSuggestionServiceProtocol) -> UserSuggestionViewState {
let viewStateItems = userSuggestionService.items.value.map { suggestionItem in
let items = userSuggestionService.items.value.map { suggestionItem in
return UserSuggestionViewStateItem(id: suggestionItem.userId, avatar: suggestionItem, displayName: suggestionItem.displayName)
}
return UserSuggestionViewState(items: viewStateItems)
super.init(initialViewState: UserSuggestionViewState(items: items))
userSuggestionService.items.sink { items in
self.state.items = items.map({ item in
UserSuggestionViewStateItem(id: item.userId, avatar: item, displayName: item.displayName)
})
}.store(in: &cancellables)
}
// MARK: - Public
@@ -69,13 +61,4 @@ class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewMo
completion?(.selectedItemWithIdentifier(item.id))
}
}
override class func reducer(state: inout UserSuggestionViewState, action: UserSuggestionStateAction) {
switch action {
case .updateWithItems(let items):
state.items = items.map({ item in
UserSuggestionViewStateItem(id: item.userId, avatar: item, displayName: item.displayName)
})
}
}
}
@@ -16,12 +16,6 @@
import Foundation
@available(iOS 14, *)
protocol UserSuggestionViewModelProtocol {
static func makeUserSuggestionViewModel(userSuggestionService: UserSuggestionServiceProtocol) -> UserSuggestionViewModelProtocol
var context: UserSuggestionViewModelType.Context { get }
var completion: ((UserSuggestionViewModelResult) -> Void)? { get set }
}
@@ -18,7 +18,7 @@ import SwiftUI
@available(iOS 14.0, *)
struct UserSuggestionListWithInputViewModel {
let listViewModel: UserSuggestionViewModelProtocol
let listViewModel: UserSuggestionViewModel
let callback: (String)->()
}
@@ -47,10 +47,6 @@ extension TemplateSimpleScreenPromptType: Identifiable, CaseIterable {
// MARK: View model
enum TemplateSimpleScreenStateAction {
case viewAction(TemplateSimpleScreenViewAction)
}
enum TemplateSimpleScreenViewModelResult {
case accept
case cancel
@@ -18,7 +18,7 @@ import SwiftUI
@available(iOS 14, *)
typealias TemplateSimpleScreenViewModelType = StateStoreViewModel<TemplateSimpleScreenViewState,
TemplateSimpleScreenStateAction,
Never,
TemplateSimpleScreenViewAction>
@available(iOS 14, *)
class TemplateSimpleScreenViewModel: TemplateSimpleScreenViewModelType, TemplateSimpleScreenViewModelProtocol {
@@ -45,23 +45,10 @@ class TemplateSimpleScreenViewModel: TemplateSimpleScreenViewModelType, Template
completion?(.accept)
case .cancel:
completion?(.cancel)
case .incrementCount, .decrementCount:
dispatch(action: .viewAction(viewAction))
case .incrementCount:
state.count += 1
case .decrementCount:
state.count -= 1
}
}
override class func reducer(state: inout TemplateSimpleScreenViewState, action: TemplateSimpleScreenStateAction) {
switch action {
case .viewAction(let viewAction):
switch viewAction {
case .incrementCount:
state.count += 1
case .decrementCount:
state.count -= 1
case .accept, .cancel:
break
}
}
UILog.debug("[TemplateSimpleScreenViewModel] reducer with action \(action) produced state: \(state)")
}
}
@@ -41,11 +41,6 @@ extension TemplateUserProfilePresence: Identifiable, CaseIterable {
// MARK: View model
enum TemplateUserProfileStateAction {
case viewAction(TemplateUserProfileViewAction)
case updatePresence(TemplateUserProfilePresence)
}
enum TemplateUserProfileViewModelResult {
case cancel
case done
@@ -19,7 +19,7 @@ import Combine
@available(iOS 14, *)
typealias TemplateUserProfileViewModelType = StateStoreViewModel<TemplateUserProfileViewState,
TemplateUserProfileStateAction,
Never,
TemplateUserProfileViewAction>
@available(iOS 14, *)
class TemplateUserProfileViewModel: TemplateUserProfileViewModelType, TemplateUserProfileViewModelProtocol {
@@ -54,49 +54,28 @@ class TemplateUserProfileViewModel: TemplateUserProfileViewModelType, TemplateUs
count: 0
)
}
private func setupPresenceObserving() {
let presenceUpdatePublisher = templateUserProfileService.presenceSubject
.map(TemplateUserProfileStateAction.updatePresence)
.eraseToAnyPublisher()
dispatch(actionPublisher: presenceUpdatePublisher)
templateUserProfileService
.presenceSubject
.sink(receiveValue: { [weak self] presence in
self?.state.presence = presence
})
.store(in: &cancellables)
}
// MARK: - Public
override func process(viewAction: TemplateUserProfileViewAction) {
switch viewAction {
case .cancel:
cancel()
completion?(.cancel)
case .done:
done()
case .incrementCount, .decrementCount:
dispatch(action: .viewAction(viewAction))
completion?(.done)
case .incrementCount:
state.count += 1
case .decrementCount:
state.count -= 1
}
}
override class func reducer(state: inout TemplateUserProfileViewState, action: TemplateUserProfileStateAction) {
switch action {
case .updatePresence(let presence):
state.presence = presence
case .viewAction(let viewAction):
switch viewAction {
case .incrementCount:
state.count += 1
case .decrementCount:
state.count -= 1
case .cancel, .done:
break
}
}
UILog.debug("[TemplateUserProfileViewModel] reducer with action \(action) produced state: \(state)")
}
private func done() {
completion?(.done)
}
private func cancel() {
completion?(.cancel)
}
}
@@ -83,13 +83,6 @@ enum TemplateRoomChatRoomInitializationStatus {
case failedToInitialize
}
/// Actions to be performed on the `ViewModel` State
enum TemplateRoomChatStateAction {
case updateRoomInitializationStatus(TemplateRoomChatRoomInitializationStatus)
case updateBubbles([TemplateRoomChatBubble])
case clearMessageInput
}
/// Actions sent by the `ViewModel` to the `Coordinator`
enum TemplateRoomChatViewModelAction {
case done
@@ -19,7 +19,7 @@ import Combine
@available(iOS 14, *)
typealias TemplateRoomChatViewModelType = StateStoreViewModel<TemplateRoomChatViewState,
TemplateRoomChatStateAction,
Never,
TemplateRoomChatViewAction>
@available(iOS 14, *)
@@ -48,21 +48,22 @@ class TemplateRoomChatViewModel: TemplateRoomChatViewModelType, TemplateRoomChat
}
private func setupRoomInitializationObserving() {
let initializationPublisher = templateRoomChatService
templateRoomChatService
.roomInitializationStatus
.map(TemplateRoomChatStateAction.updateRoomInitializationStatus)
.eraseToAnyPublisher()
dispatch(actionPublisher: initializationPublisher)
.sink { [weak self] status in
self?.state.roomInitializationStatus = status
}
.store(in: &cancellables)
}
private func setupMessageObserving() {
let messageActionPublisher = templateRoomChatService
templateRoomChatService
.chatMessagesSubject
.map(Self.makeBubbles(messages:))
.map(TemplateRoomChatStateAction.updateBubbles)
.eraseToAnyPublisher()
dispatch(actionPublisher: messageActionPublisher)
.sink { [weak self] bubbles in
self?.state.bubbles = bubbles
}
.store(in: &cancellables)
}
private static func defaultState(templateRoomChatService: TemplateRoomChatServiceProtocol) -> TemplateRoomChatViewState {
@@ -117,27 +118,10 @@ class TemplateRoomChatViewModel: TemplateRoomChatViewModelType, TemplateRoomChat
override func process(viewAction: TemplateRoomChatViewAction) {
switch viewAction {
case .done:
done()
callback?(.done)
case .sendMessage:
templateRoomChatService.send(textMessage: state.bindings.messageInput)
dispatch(action: .clearMessageInput)
}
}
override class func reducer(state: inout TemplateRoomChatViewState, action: TemplateRoomChatStateAction) {
switch action {
case .updateRoomInitializationStatus(let status):
state.roomInitializationStatus = status
case .clearMessageInput:
state.bindings.messageInput = ""
case .updateBubbles(let bubbles):
state.bubbles = bubbles
}
}
// MARK: - Private
private func done() {
callback?(.done)
}
}
@@ -32,11 +32,6 @@ enum TemplateRoomListCoordinatorAction {
// MARK: - View model
/// Actions to be performed on the `ViewModel` State
enum TemplateRoomListStateAction {
case updateRooms([TemplateRoomListRoom])
}
/// Actions sent by the`ViewModel` to the `Coordinator`.
enum TemplateRoomListViewModelAction {
case didSelectRoom(String)
@@ -19,7 +19,7 @@ import Combine
@available(iOS 14, *)
typealias TemplateRoomListViewModelType = StateStoreViewModel<TemplateRoomListViewState,
TemplateRoomListStateAction,
Never,
TemplateRoomListViewAction>
@available(iOS 14.0, *)
class TemplateRoomListViewModel: TemplateRoomListViewModelType, TemplateRoomListViewModelProtocol {
@@ -47,10 +47,12 @@ class TemplateRoomListViewModel: TemplateRoomListViewModelType, TemplateRoomList
}
private func startObservingRooms() {
let roomsUpdatePublisher = templateRoomListService.roomsSubject
.map(TemplateRoomListStateAction.updateRooms)
.eraseToAnyPublisher()
dispatch(actionPublisher: roomsUpdatePublisher)
templateRoomListService
.roomsSubject
.sink { [weak self] rooms in
self?.state.rooms = rooms
}
.store(in: &cancellables)
}
// MARK: - Public
@@ -64,13 +66,6 @@ class TemplateRoomListViewModel: TemplateRoomListViewModelType, TemplateRoomList
}
}
override class func reducer(state: inout TemplateRoomListViewState, action: TemplateRoomListStateAction) {
switch action {
case .updateRooms(let rooms):
state.rooms = rooms
}
}
// MARK: - Private
private func done() {