Для шаблона MVVM
вы должны понимать, что это слой, разделенный на 2 части: входы и выходы.
В терминах входных данных ваша viewModel должна перехватывать каждое событие из viewController, идля выходов это способ, которым viewModel будет отправлять данные (правильно отформатированные) в viewController.
Так что, в принципе, если у нас есть viewController, подобный этому:
final class HomeViewController: UIViewController {
// MARK: - Outlets
@IBOutlet private weak var titleLabel: UILabel!
// MARK: - View life cycle
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Actions
@IBAction func buttonTouchUp(_ sender: Any) {
titleLabel.text = "toto"
}
}
Нам нужночтобы извлечь обязанности для viewModel, так как viewController обрабатывает событие touchUp и владеет данными для переноса на метку.
Извлекая это, вы сохраняете ответственность правильно, и, в конце концов, вы 'Я смогу правильно протестировать вашу viewModel ?
Так как это сделать?Легко, давайте взглянем на нашу будущую viewModel:
final class HomeViewModel {
// MARK: - Private properties
private let title: String
// MARK: - Initializer
init(title: String) {
self.title = title
}
// MARK: - Outputs
var titleText: ((String) -> Void)?
// MARK: - Inputs
func viewDidLoad() {
titleText?("")
}
func buttonDidPress() {
titleText?(title)
}
}
Итак, теперь, выполняя это, вы сохраняете различные обязанности, давайте посмотрим, как связать нашу viewModel с нашим предыдущим viewController:
final class HomeViewController: UIViewController {
// MARK: - public var
var viewModel: HomeViewModel!
// MARK: - Outlets
@IBOutlet private weak var titleLabel: UILabel!
// MARK: - View life cycle
override func viewDidLoad() {
super.viewDidLoad()
bind(to: viewModel)
viewModel.viewDidLoad()
}
// MARK: - Private func
private func bind(to viewModel: HomeViewModel) {
viewModel.titleText = { [weak self] title in
self?.titleLabel.text = title
}
}
// MARK: - Actions
@IBAction func buttonTouchUp(_ sender: Any) {
viewModel.buttonDidPress()
}
}
Итак, одна вещь отсутствует, вы спросите меня «а как инициализировать нашу viewModel внутри viewController?»
По сути, вы должны еще раз извлечь обязанности, у вас может быть Screens
слой, который будет отвечать за создание вида следующим образом:
final class Screens {
// MARK: - Properties
private let storyboard = UIStoryboard(name: StoryboardName, bundle: Bundle(for: Screens.self))
// MARK: - Home View Controller
func createHomeViewController(with title: String) -> HomeViewController {
let viewModel = HomeViewModel(title: title)
let viewController = storyboard.instantiateViewController(withIdentifier: "Home") as! HomeViewController
viewController.viewModel = viewModel
return viewController
}
}
И, наконец, сделайте что-то вроде этого:
let screens = Screens()
let homeViewController = screens.createHomeViewController(with: "Toto")
Но основной задачей было предоставить возможность протестировать егоправильно, так как это сделать?очень просто!
import XCTest
@testable import mvvmApp
final class HomeViewModelTests: XCTestCase {
func testGivenAHomeViewModel_WhenViewDidLoad_titleLabelTextIsEmpty() {
let viewModel = HomeViewModel(title: "toto")
let expectation = self.expectation("Returned title")
viewModel.titleText = { title in
XCTAssertEqual(title, "")
expectation.fulfill()
}
viewModel.viewDidLoad()
waitForExpectations(timeout: 1.0, handler: nil)
}
func testGivenAHomeViewModel_WhenButtonDidPress_titleLabelTextIsCorrectlyReturned() {
let viewModel = HomeViewModel(title: "toto")
let expectation = self.expectation("Returned title")
var counter = 0
viewModel.titleText = { title in
if counter == 1 {
XCTAssertEqual(title, "toto")
expectation.fulfill()
}
counter += 1
}
viewModel.viewDidLoad()
viewModel.buttonDidPress()
waitForExpectations(timeout: 1.0, handler: nil)
}
}
И это все ?