RxMVVM is the modern and reactive architecture for RxSwift application. This repository introduces the basic concept of RxMVVM and describes how to build an application using RxMVVM.
You may want to check Resources section first if you'd like to see the actual code.
RxMVVM is based on MVVM architecture. It uses RxSwift as a communication method between each layers: View, ViewModel and Model. For example, user interactions are delivered from View to ViewModel via PublishSubject
. Data is exposed by properties or Observable
properties. It depends on whether ViewModel can provide mutable property or not.
View refers to the component which displays data. In RxMVVM, a ViewController is treated as a View. A Cell is treated as a View as well.
A View only defines how to map the ViewModel's data to each UI components. These bindings are usually created in configure()
method.
func configure(viewModel: MyViewModelType) {
// Input
self.button.rx.tap
.bindTo(viewModel.buttonDidTap)
.addDisposableTo(self.disposeBag)
// Output
viewModel.isButtonEnabled
.drive(self.button.rx.isEnabled)
.addDisposableTo(self.disposeBag)
}
It's recommended to define configure()
as private
or fileprivate
if it's called only from the initializer. For example, every ViewController takes ViewModel in the initializer so configure()
can be called in the initializer.
class ProfileViewController {
init(viewModel: ProfileViewModelType) {
super.init(nibName: nil, bundle: nil)
self.configure(viewModel: viewModel)
}
private func configure(viewModel: ProfileViewModelType) {
// ...
}
}
On the other hand, the Cell's configure()
method is called from outside such as tableView(_:cellForRowAt:)
, or configureCell
closure if you're using RxDataSources.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(...)
cell.configure(viewModel: viewModel)
return cell
}
In order to manage Disposables, a View typically has its own DisposeBag.
class MyView: UIView {
let disposeBag = DisposeBag()
}
ViewModel receives user input and creates output so that View can bind it to its UI components. View usually has its corresponding ViewModel. For example, ProfileViewController
has ProfileViewModel
. ViewController should have its ViewModel but not all View should have its ViewModel.
ViewModel follows the naming convention of the corresponding View. Here are some examples:
View | ViewModel |
---|---|
ProfileViewController | ProfileViewModel |
CommentInputView | CommentInputViewModel |
ArticleCardCell | ArticleCardCellModel |
ViewModel protocols have two types of property: Input and Output. This is an example ViewModel protocol. It's recommended to define ViewModel protocol before implementing it. It gives you more testability.
Inputs are usually defined as PublishSubject
so that View can bind user inputs to ViewModel. Outputs are usually defined as Driver
which ensures every events to be subscribed on the main thread.
protocol ProfileViewModelType {
// Input
var followButtonDidTap: PublishSubject<Void> { get }
// Output
var isFollowButtonSelected: Driver<Void> { get }
}
Alternatively, you can do as following if you'd like to separate inputs and outputs explicitly:
protocol ProfileViewModelInput {
var followButtonDidTap: PublishSubject<Void> { get }
}
protocol ProfileViewModelOutput {
var isFollowButtonSelected: Driver<Void> { get }
}
typealias ProfileViewModelType = ProfileViewModelInput & ProfileViewModelOutput
ViewModel should initialize inputs and outputs in the initializer.
class ProfileViewModel {
// MARK: Input
let followButtonDidTap = PublishSubject<Int>()
// MARK: Output
let isFollowButtonSelected: Driver<Void>
// MARK: Init
init(provider: ServiceProvider) {
self.isFollowButtonSelected = self.followButtonDidTap
.flatMap { (userID: Int) -> Observable<Void> in
return provider.userService.follow(userID: userID).map { _ in true }
}
.asDriver(onErrorJustReturn: false)
}
}
Model only represents data structure.
RxMVVM has a special layer named Service. Service layer does actual business logic such as networking. ViewModel is a middle layer which manages event streams. When ViewModel receives user input from View, ViewModel manipulates the event stream and passes it to Service. Service will make a network request, map the response to Model, then send it back to ViewModel.
Single ViewModel can communicate with many Services. ServiceProvider provides the references of Services to ViewModel. ServiceProvider is created once and passed to the first ViewModel. ViewModel should pass its ViewModel reference to child ViewModel.
let serviceProvider = ServiceProvider()
let firstViewModel = FirstViewModel(provider: serviceProvider)
let firstViewController = FirstViewController(viewModel: firstViewModel)
window.rootViewController = firstViewController
ServiceProvider is not complicated. Here is an example code of ServiceProvider:
protocol ServiceProviderType: class {
var userService: UserServiceType { get }
var articleService: ArticleServiceType { get }
}
final class ServiceProvider: ServiceProviderType {
lazy var userService: UserServiceType = UserService(provider: self)
lazy var articleService: ArticleServiceType = ArticleService(provider: self)
}
RxMVVM suggests some conventions to write clean and concise code.
-
View doesn't have control flow. View cannot modify the data. View only knows how to map the data.
Bad
viewModel.titleLabelText .map { $0 + "!" } // Bad: View should not modify the data .bindTo(self.titleLabel)
Good
viewModel.titleLabelText .bindTo(self.titleLabel.rx.text)
-
View doesn't know what ViewModel does. View can only communicate to ViewModel about what View did.
Bad
viewModel.login() // Bad: View should not know what ViewModel does (login)
Good
self.loginButton.rx.tap .bindTo(viewModel.loginButtonDidTap) // "Hey I clicked the login button" self.usernameInput.rx.controlEvent(.editingDidEndOnExit) .bindTo(viewModel.usernameInputDidReturn) // "Hey I tapped the return on username input"
-
Model is hidden by ViewModel. ViewModel only exposes the minimum data so that View can render.
Bad
struct ProductViewModel { let product: Driver<Product> // Bad: ViewModel should hide Model }
Good
struct ProductViewModel { let productName: Driver<String> let formattedPrice: Driver<String> let formattedOriginalPrice: Driver<String> let isOriginalPriceHidden: Driver<Bool> }
This chapter describes some architectural considerations.
Almost applications have more than one ViewController. In MVC architecture, ViewController(ListViewController
) creates next ViewController(DetailViewController
) and just presents it. This is same in RxMVVM but the only difference is the creation of ViewModel.
In RxMVVM, ListViewModel
creates DetailViewModel
and passes it to ListViewController
. Then the ListViewController
creates DetailViewController
with the DetailViewModel
received from ListViewModel
.
Here is an example code of ListViewModel
:
class ListViewModel: ListViewModelType {
// MARK: Input
let detailButtonDidTap: PublishSubject<Void> = .init()
// MARK: Output
let presentDetailViewModel: Observable<DetailViewModelType>
// MARK: Init
init(provider: ServiceProviderType) {
self.presentDetailViewModel = self.detailButtonDidTap
.map { _ -> DetailViewModelType in
return DetailViewModel(provider: provider)
}
}
}
And ListViewController
:
class ListViewController: UIViewController {
private func configure(viewModel: ListViewModelType) {
// Output
viewModel.detailViewModel
.subscribe(onNext: { viewModel in
let detailViewController = DetailViewController(viewModel: viewModel)
self.navigationController?.pushViewController(detailViewController, animated: true)
})
.addDisposableTo(self.disposeBag)
}
}
Sometimes ViewModel should receive data (such as user input) from the other ViewModel. In this case, use rx
extension to communicate between View and View. Then bind it to ViewModel.
MessageInputView.swift
extension Reactive where Base: MessageInputView {
var sendButtonTap: ControlEvent<String?> { ... }
var isSendButtonLoading: ControlEvent<String?> { ... }
}
MessageListViewModel.swift
protocol MessageListViewModelType {
// Input
var messageInputViewSendButtonDidTap: PublishSubject<String?> { get }
// Output
var isMessageInputViewSendButtonLoading: Driver<Bool> { get }
}
MessageListViewController.swift
func configure(viewModel: MessageListViewModelType) {
// Input
self.messageInputView.rx.sendButtonTap
.bindTo(viewModel.messageInputViewSendButtonDidTap)
.addDisposableTo(self.disposeBag)
// Output
viewModel.isMessageInputViewSendButtonLoading
.drive(self.messageInputView.rx.isSendButtonLoading)
.addDisposableTo(self.disposeBag)
}
- RxTodo: iOS Todo Application using RxMVVM architecture