As mobile developers, we are always searching for the best architectural pattern to organize our code and make it more maintainable and scalable. One of the most popular architectures used in the iOS community is MVVM (Model-View-ViewModel). In this blog post, we will explore a useful ViewModel structure that can make our MVVM code even cleaner and more understandable. To better understand this ViewModel structure, we will also build a simple app using RxSwift and RxCocoa.
ViewModel Structure
The ViewModel is the heart of the MVVM architecture, responsible for presenting the data and logic to the View. To make our ViewModel code more organized and modular, we will split it into two protocols: TestViewModelInputs
and TestViewModelOutputs
. The TestViewModelInputs
protocol will define the inputs that our ViewModel needs to perform its logic, while the TestViewModelOutputs
protocol will define the outputs that our ViewModel will provide to the View.
Next, we will create a TestViewModelType
protocol that combines the TestViewModelInputs
and TestViewModelOutputs
protocols. This protocol will be implemented by our ViewModel class, and it will expose its inputs and outputs through computed properties.
Finally, we will create the TestViewModel
class that implements the TestViewModelType
protocol. This class will contain our business logic, and it will be responsible for transforming the inputs into the outputs.
Let’s take a look at the code:
import RxSwift
import RxCocoa
protocol TestViewModelInputs: AnyObject {
// Define the inputs here
}
protocol TestViewModelOutputs: AnyObject {
// Define the outputs here
}
protocol TestViewModelType: AnyObject {
var inputs: TestViewModelInputs { get }
var outputs: TestViewModelOutputs { get }
}
class TestViewModel: TestViewModelType, TestViewModelInputs, TestViewModelOutputs {
var inputs: TestViewModelInputs { return self }
var outputs: TestViewModelOutputs { return self }
// Implement the inputs and outputs here
// Use RxSwift to transform the inputs into the outputs
}
As you can see, we created the TestViewModelInputs
and TestViewModelOutputs
protocols, the TestViewModelType
protocol, and the TestViewModel
class that implements them.
To make our ViewModel useful, we need to define its inputs and outputs. Let’s create a simple app to understand this better.
Our app will have a UITextField
and a UILabel
. Whenever the user types something in the UITextField
, the UILabel
will display the length of the input string.
Let’s start by creating a new project in Xcode and adding a UIViewController
to the storyboard. Then, add a UITextField
and a UILabel
to the view controller and create outlets for them.
Next, let’s create the TestViewModel
class and implement its inputs and outputs:
class TestViewModel: TestViewModelType, TestViewModelInputs, TestViewModelOutputs {
var inputs: TestViewModelInputs { return self }
var outputs: TestViewModelOutputs { return self }
// Inputs
let textChanged = PublishRelay<String>()
// Outputs
let textCount: Driver<String>
init() {
textCount = textChanged
.map { $0.count }
.map { "Text count: \($0)" }
.asDriver(onErrorJustReturn: "Error")
}
}
In this example, we defined the textChanged
input as a PublishRelay
that emits a String
every time the text in the UITextField
changes.
For the output, we defined the textCount
property as a Driver
of String
. We used RxSwift operators to transform the `text
countinput into a
Stringwith the text count, and then we used the
asDriveroperator to make it easier to bind to the
UILabel` in our View. We also provided a default value in case of errors.
Finally, we need to bind our ViewModel to the View. In the viewDidLoad
method of our UIViewController
, we can create an instance of the TestViewModel
, and then bind its inputs and outputs to the appropriate UI elements:
import UIKit
import RxSwift
import RxCocoa
class HomeViewController: UIViewController {
@IBOutlet private weak var textField: UITextField!
@IBOutlet private weak var textCountLabel: UILabel!
private let viewModel = TestViewModel()
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// Bind inputs
textField.rx.text
.orEmpty
.bind(to: viewModel.textChanged)
.disposed(by: disposeBag)
// Bind outputs
viewModel.textCount
.drive(textCountLabel.rx.text)
.disposed(by: disposeBag)
}
}
In this code, we used the RxSwift
and RxCocoa
libraries to bind the UITextField
and the UILabel
to the textChanged
input and the textCount
output of our ViewModel, respectively.
Conclusion
In this blog post, we explored a useful ViewModel structure that can make our MVVM code even cleaner and more understandable. By splitting our ViewModel into two protocols – TestViewModelInputs
and TestViewModelOutputs
– and implementing the TestViewModelType
protocol to combine them, we can create modular and organized ViewModel code that is easier to maintain and understand.
We also built a simple app to demonstrate how to use this ViewModel structure with RxSwift and RxCocoa. By binding the inputs and outputs of our ViewModel to the appropriate UI elements in our View, we can create a reactive and responsive user interface that updates in real-time as the user interacts with it.
I hope this blog post was helpful in understanding how to create a useful ViewModel structure in MVVM. With this approach, you can create well-organized and modular code that is easier to maintain and scale as your app grows in complexity.