ReMVVM is an application architecture concept, marriage of Unidirectional Data Flow (Redux) with MVVM.
Redux + MVVM = ReMVVM
Model-View-ViewModel - is well known and widely used architecture on iOS platform. It is very simple, lightweight, doesn’t bring any boilerplate and works well with reactive programming (can be used without it of course). While working with MVVM you will find couple of questions:
Of course you can find couple patterns to solve that such as coordinator but surprisingly easy you can follow the wrong path.
Unidirectional Data Flow (UDF) - the main concept behind is immutable application state that can be changed only in one place in the app (Store) and only by predictable plain functions (in Reducers) ie. State + Action = NewState. The most popular implementation of that concept is JavaScript library called Redux. The first and most popular swift’s implementation is ReSwift by Benjamin Encz. If you are not familiar with that architecture I strongly recommend to look on Benjamin’s presentation and look into ReSwift documentation.
Let’s imagine you have application with complicated view structure and communication with backend API. It will bring complicated Application State, a lot of Reducers and dozens of Actions for the state changes related with both business and view logic.
But… what about making a mix of two architectures ? Can we implement “global” app state by Unidirectional Data Flow and use MVVM pattern for each individual screen ? We can and it is what ReMVVM was made for.
For the simplicity, you can think that Unidirectional Data Flow part stores global data model of the app. View Model takes that data, listens for change and of course converts and serves it for the View layer.
So, we can divide components on two groups related with Unidirectional Data Flow and MVVM.
Store
- contains your application state that can be modified only by dispatching an StoreAction
. Every state change is notified to every StateObserver
.
StoreState
- it’s immutable data structure that holds your application data. In ReMVVM it has to provide ViewModelFactory
that will be used for creating View Models for your view(s).
StoreAction
- describes state change and is handled by corresponding Reducer
.
Middleware
- mechanism for enhance action’s dispatch functionality. It is usually used to simplify asynchronous dispatch and implement ‘side effects’ if required.
Reducer
- provides pure function that returns new state based on current state and the action.
ViewModel
- is designed to store and manage UI related data for the view
ViewModelProvider
- provides View Model(s) using ViewModelFactory from current state
ViewModelFactory
- creates View Model instances
Let’s start with standard „counter” example. We will implement View that presents the counter value with two buttons to increase and decrease it. We will use SwiftUI and Combine but we can do it with UIKit with or without any other Reactive framework.
Counter value will be stored in application state in Store. View model will be listening on it's change and will serve counter’s value as a string value.
First, we need to define our application state. We don’t need custom ViewModelFactory
so we will use default implementation of StoreState
protocol.
struct ApplicationState: StoreState {
let counter: Int
}
Second, we need an StoreAction
that will let us to change state.
enum CounterAction: StoreAction {
case increase
case decrease
}
Third, we need a Reducer
that will update counter value based on action.
enum CounterReducer: Reducer {
static func reduce(state: Int, with action: CounterAction) -> Int {
switch action {
case .increase: return state + 1
case .decrease: return state - 1
}
}
}
Then, we can write view model that observes state changes and serves mapped to String value.
final class CounterViewModel: ObservableObject, Initializable {
@Published private(set) var counter: String = ""
@ReMVVM.State private var state: Int?
required init() {
$state.map(String.init).assign(to: &$counter)
}
}
final class CounterViewModel: ObservableObject, Initializable, StateObserver {
@Published private(set) var counter: String = ""
required init() { }
func didReduce(state: Int, oldState: Int?) {
counter = String(state)
}
}
Please notice StateObserver
declaration!
Finally, we can create our view.
struct CounterView: View {
@ReMVVM.ViewModel private var viewModel: CounterViewModel!
@ReMVVM.Dispatcher private var dispatcher
var body: some View {
VStack {
Text("Counter: \(viewModel.counter)")
.padding(.bottom)
Button(action: dispatcher[CounterAction.increase]) {
Text("Increase")
}
Button(action: dispatcher[CounterAction.decrease]) {
Text("Decrease")
}
}
}
}
Probably you’ve noticed that we wrote Reducer
for the counter integer value and view model also listens for integer values instead of the ApplicationState
. Of course, we could do it with ApplicationState
but I would like to present you the idea about substates.
It’s good practice to separate data as much as possible. We can operate on smaller chunks, view model depends only on the required data not the whole state.
To accomplish that we still need to create Reducer
for ApplicationState
because it's our main state.
struct ApplicationReducer: Reducer {
static func reduce(state: ApplicationState, with action: StoreAction) -> ApplicationState {
ApplicationState(
counter: CounterReducer.reduce(state: state.counter, with: action)
// other substates ....
)
}
}
Also, we need to provide StateMapper
that converts ApplicationState
to our Int
substate. Thanks to StateMapper
we can observe changes of substate instead of whole state.
let counterMapper = StateMapper<ApplicationState>(for: \.counter)
Finally, we can initialise store
let initialState = ApplicationState(counter: 0)
let store = Store(with: initialState,
reducer: ApplicationReducer.self,
stateMappers: [counterMapper])
ReMVVM.initialize(with: store)
and see the result:
ReMVVM project contains two targets:
Basically import ReMVVMSwiftUI when using SwiftUI, import ReMVVMCore otherwise.
Sneak peak for navigation implemented with ReMVVM.
In comparison to our example, CounterView
contains only couple more buttons that send appropriate actions. There are no navigation likns, no navigation logic etc.
Code:
struct CounterView: View {
@ReMVVM.ViewModel private var viewModel: CounterViewModel!
@ReMVVM.Dispatcher private var dispatcher
var body: some View {
VStack {
VStack {
Text("View id: \(viewModel.id)")
Text("Counter: \(viewModel.counter)")
Button(action: dispatcher[CounterAction.increase]) {
Text("Increase")
}
Button(action: dispatcher[CounterAction.decrease]) {
Text("Decrease")
}
}.padding(.bottom)
VStack {
Text("Navigation: -> new CounterView")
Button(action: dispatcher[Push(with: CounterView())]) {
Text("Push new")
}
Button(action: dispatcher[Pop()]) {
Text("Pop")
}
Button(action: dispatcher[ShowModal(view: CounterView())]) {
Text("Show new on modal")
}
Button(action: dispatcher[ShowModal(view: CounterView(), navigation: true)]) {
Text("Show new on modal with nav")
}
Button(action: dispatcher[DismissModal()]) {
Text("Dismiss modal")
}
Button(action: dispatcher[Show(on: Navigation.root, view: CounterView())]) {
Text("Show new on root")
}
Button(action: dispatcher[NavigationTab.profile.action]) {
Text("Show profile tab")
}
}
}
}
}
We can also hide implementations ditails with actions. Let's imagine we meed to show profile page from couple of places in the app. Do we need to know in all that places what view should we instantiate ? Should it be a modal or push to navigation stack ? Good idea is to make high level action for that eg. ShowProfilePageAction
and put all the logic behind the action in one place in eg. ShowProfilePageMiddleware
.
For the example code please have a look here: ReMVVMSampleSwiftUI
ReMVVMExtUIKit navigation implementation used in couple live applications available in AppStore.
ReMVVMExtSwiftUI is a concept of navigation implementation in pure SwiftUI.
ReMVVMSample-iOS is UIKit with RxSwift sample that use ReMVVMExtUIKit.
ReMVVM architecture brings great separation between layers. It's clear where to store model data, who and where creates view model and how it's passed to the view. It takes the biggest advantages of two different architectures and makes the code readable without introducing any boilerplate.
link |
Stars: 186 |
Last commit: 1 week ago |
Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics