mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-26 19:34:25 +02:00
Add StateStoreViewModel and publisher extensions for convenienec.
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension Publisher {
|
||||
|
||||
func sinkDispatchTo<S, SA, IA>(_ store: StateStoreViewModel<S, SA, IA>) where SA == Output, Failure == Never {
|
||||
return self
|
||||
.subscribe(on: DispatchQueue.main)
|
||||
.sink { [weak store] (output) in
|
||||
guard let store = store else { return }
|
||||
store.dispatch(action: output)
|
||||
}
|
||||
.store(in: &store.cancellables)
|
||||
}
|
||||
|
||||
|
||||
func dispatchTo<S, SA, IA>(_ store: StateStoreViewModel<S, SA, IA>) -> Publishers.HandleEvents<Publishers.SubscribeOn<Self, DispatchQueue>> where SA == Output, Failure == Never {
|
||||
return self
|
||||
.subscribe(on: DispatchQueue.main)
|
||||
.handleEvents(receiveOutput: { [weak store] action in
|
||||
guard let store = store else { return }
|
||||
store.dispatch(action: action)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// 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
|
||||
import Combine
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
protocol BindableState {
|
||||
associatedtype BindStateType = Void
|
||||
var bindings: BindStateType { get set }
|
||||
}
|
||||
|
||||
extension BindableState where BindStateType == Void {
|
||||
var bindings: Void {
|
||||
get {
|
||||
()
|
||||
}
|
||||
set {
|
||||
fatalError("Can't bind to the default Void binding.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
class ViewModelContext<ViewState:BindableState, ViewAction>: ObservableObject {
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
let inputActions: PassthroughSubject<ViewAction, Never>
|
||||
@Published var inputState: ViewState.BindStateType
|
||||
@Published fileprivate(set) var viewState: ViewState
|
||||
|
||||
init(initialViewState: ViewState) {
|
||||
self.inputState = initialViewState.bindings
|
||||
self.inputActions = PassthroughSubject()
|
||||
self.viewState = initialViewState
|
||||
if !(initialViewState.bindings is Void) {
|
||||
self.$inputState
|
||||
.weakAssign(to: \.viewState.bindings, on: self)
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
class StateStoreViewModel<State:BindableState, StateAction, ViewAction> {
|
||||
|
||||
typealias Context = ViewModelContext<State, ViewAction>
|
||||
|
||||
let state: CurrentValueSubject<State, Never>
|
||||
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
var context: Context
|
||||
|
||||
init(initialViewState: State) {
|
||||
|
||||
self.context = Context(initialViewState: initialViewState)
|
||||
self.state = CurrentValueSubject(initialViewState)
|
||||
self.state.weakAssign(to: \.context.viewState, on: self)
|
||||
.store(in: &cancellables)
|
||||
self.context.inputActions.sink { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
self.process(viewAction: action)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
func dispatch(action: StateAction) {
|
||||
reducer(state: &state.value, action: action)
|
||||
}
|
||||
|
||||
func reducer(state: inout State, action: StateAction) {
|
||||
|
||||
}
|
||||
|
||||
func process(viewAction: ViewAction) {
|
||||
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -39,8 +39,8 @@ final class TemplateUserProfileCoordinator: Coordinator {
|
||||
@available(iOS 14.0, *)
|
||||
init(parameters: TemplateUserProfileCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
let viewModel = TemplateUserProfileViewModel(templateUserProfileService: TemplateUserProfileService(session: parameters.session))
|
||||
let view = TemplateUserProfile(viewModel: viewModel)
|
||||
let viewModel = TemplateUserProfileViewModel.makeTemplateUserProfileViewModel(templateUserProfileService: TemplateUserProfileService(session: parameters.session))
|
||||
let view = TemplateUserProfile(viewModel: viewModel.context)
|
||||
.addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
|
||||
templateUserProfileViewModel = viewModel
|
||||
templateUserProfileHostingController = VectorHostingController(rootView: view)
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TemplateUserProfileViewState {
|
||||
struct TemplateUserProfileViewState: BindableState {
|
||||
let avatar: AvatarInputProtocol?
|
||||
let displayName: String?
|
||||
var presence: TemplateUserProfilePresence
|
||||
|
||||
+2
-2
@@ -50,11 +50,11 @@ enum MockTemplateUserProfileScreenState: MockScreenState, CaseIterable {
|
||||
case .longDisplayName(let displayName):
|
||||
service = MockTemplateUserProfileService(displayName: displayName)
|
||||
}
|
||||
let viewModel = TemplateUserProfileViewModel(templateUserProfileService: service)
|
||||
let viewModel = TemplateUserProfileViewModel.makeTemplateUserProfileViewModel(templateUserProfileService: service)
|
||||
|
||||
// can simulate service and viewModel actions here if needs be.
|
||||
|
||||
return AnyView(TemplateUserProfile(viewModel: viewModel)
|
||||
return AnyView(TemplateUserProfile(viewModel: viewModel.context)
|
||||
.addDependency(MockAvatarService.example))
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -27,9 +27,10 @@ struct TemplateUserProfile: View {
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var viewModel: TemplateUserProfileViewModel
|
||||
@ObservedObject var viewModel: TemplateUserProfileViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
EmptyView()
|
||||
VStack {
|
||||
TemplateUserProfileHeader(
|
||||
avatar: viewModel.viewState.avatar,
|
||||
@@ -50,12 +51,12 @@ struct TemplateUserProfile: View {
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button(VectorL10n.done) {
|
||||
viewModel.process(viewAction: .cancel)
|
||||
viewModel.inputActions.send(.done)
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(VectorL10n.cancel) {
|
||||
viewModel.process(viewAction: .cancel)
|
||||
viewModel.inputActions.send(.cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+26
-23
@@ -16,33 +16,36 @@
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class TemplateUserProfileViewModel: ObservableObject, TemplateUserProfileViewModelProtocol {
|
||||
|
||||
|
||||
|
||||
@available(iOS 14, *)
|
||||
typealias TemplateUserProfileViewModelType = StateStoreViewModel<TemplateUserProfileViewState,
|
||||
TemplateUserProfileStateAction,
|
||||
TemplateUserProfileViewAction>
|
||||
@available(iOS 14, *)
|
||||
class TemplateUserProfileViewModel: TemplateUserProfileViewModelType, TemplateUserProfileViewModelProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let templateUserProfileService: TemplateUserProfileServiceProtocol
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// MARK: Public
|
||||
@Published private(set) var viewState: TemplateUserProfileViewState
|
||||
|
||||
var completion: ((TemplateUserProfileViewModelResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
init(templateUserProfileService: TemplateUserProfileServiceProtocol, initialState: TemplateUserProfileViewState? = nil) {
|
||||
|
||||
static func makeTemplateUserProfileViewModel(templateUserProfileService: TemplateUserProfileServiceProtocol) -> TemplateUserProfileViewModelProtocol {
|
||||
return TemplateUserProfileViewModel(templateUserProfileService: templateUserProfileService)
|
||||
}
|
||||
|
||||
fileprivate init(templateUserProfileService: TemplateUserProfileServiceProtocol) {
|
||||
self.templateUserProfileService = templateUserProfileService
|
||||
self.viewState = initialState ?? Self.defaultState(templateUserProfileService: templateUserProfileService)
|
||||
|
||||
templateUserProfileService.presenceSubject
|
||||
.map(TemplateUserProfileStateAction.updatePresence)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] action in
|
||||
self?.dispatch(action:action)
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
super.init(initialViewState: Self.defaultState(templateUserProfileService: templateUserProfileService))
|
||||
setupPresenceObserving()
|
||||
}
|
||||
|
||||
private static func defaultState(templateUserProfileService: TemplateUserProfileServiceProtocol) -> TemplateUserProfileViewState {
|
||||
@@ -53,8 +56,15 @@ class TemplateUserProfileViewModel: ObservableObject, TemplateUserProfileViewMod
|
||||
)
|
||||
}
|
||||
|
||||
private func setupPresenceObserving() {
|
||||
templateUserProfileService.presenceSubject
|
||||
.map(TemplateUserProfileStateAction.updatePresence)
|
||||
.sinkDispatchTo(self)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
func process(viewAction: TemplateUserProfileViewAction) {
|
||||
|
||||
override func process(viewAction: TemplateUserProfileViewAction) {
|
||||
switch viewAction {
|
||||
case .cancel:
|
||||
cancel()
|
||||
@@ -62,13 +72,6 @@ class TemplateUserProfileViewModel: ObservableObject, TemplateUserProfileViewMod
|
||||
done()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
/// Send state actions to mutate the state.
|
||||
/// - Parameter action: The `TemplateUserProfileStateAction` to trigger the state change.
|
||||
private func dispatch(action: TemplateUserProfileStateAction) {
|
||||
Self.reducer(state: &self.viewState, action: action)
|
||||
}
|
||||
|
||||
/// A redux style reducer
|
||||
///
|
||||
|
||||
+5
@@ -17,5 +17,10 @@
|
||||
import Foundation
|
||||
|
||||
protocol TemplateUserProfileViewModelProtocol {
|
||||
|
||||
var completion: ((TemplateUserProfileViewModelResult) -> Void)? { get set }
|
||||
@available(iOS 14, *)
|
||||
static func makeTemplateUserProfileViewModel(templateUserProfileService: TemplateUserProfileServiceProtocol) -> TemplateUserProfileViewModelProtocol
|
||||
@available(iOS 14, *)
|
||||
var context: TemplateUserProfileViewModelType.Context { get }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user