Перевод статьи MVVM in IOS от Abhilash Mathur
Всякий раз года мы создаем приложение, этот вопрос всегда появляется у нас в голове, какой архитектурный паттерн использовать в нашем проекте. Часто используемый архитектурный паттерн в IOS это MVC. Большая часть разработчиков использует шаблон MVC в их проектах. MVC хорошо справляется с маленькими проектами, но когда вы начинаете расширять его, то ваш проект становится запутанным.
Я всегда находил, что архитектурный паттерн хорош в использовании, но мы не должны строго придерживаться паттерна в нашем проекте. Не все архитектурные паттерны достаточны , чтобы дать тебе все, есть достоинства и недостатки в каждом паттерне. Если у нас есть много модулей в нашем проекте, мы может определить архитектурный шаблон в соответствии с модулем. MVVM хорошо подходит для некоторых модулей, но может быть ваш новый модуль не будет хорошо работать с MVVM, поэтому вместо этого используйте другой паттерн как MVP, VIPER. Итак мы не может полностью полагаться на единственный паттерн, вместо этого, мы может проверить его соответствие модулю.
Существует много различных статей в интернете объясняющих определение и плюсы и минусы паттерна MVVM. По большей части здесь мы сфокусируемся на практический части реализации паттерна вместо простого чтения определений.
Давайте начнем
В этом проекте мы постоим простое приложение использующее архитектурный паттерн MVVM. В большинстве наших приложений, мы имеем ViewController (UI) который необходим для получения данных с сервера(API) и отображения их на UI. Мы реализуем некоторое поведение с использованием паттерна MVVM.
Это мы ожидаем увидеть в конце статьи.
Здесь мы будем использовать публичный фиктивный сервер с простор интернета.
http://dummy.restapiexample.com/api/v1/employees
Этот веб сервер возвращает нам ответ в виде списка данных работников и мы будем отображать этот список в нашей таблице.
Обзор компонентов и их роли
ViewController: Он только предоставляет объекты связанный с UI — отображение/получение информации. Часть слоя представления.
ViewModel: Он получает данные от ViewController’а, обрабатывает их и отсылает обратно в VC.
Model: Это всего лишь ваша модель, здесь больше ничего нет. Это такая же модель как в MVC. Она используется VideModel и обновляется всякий раз ViewModel шлет новое обновление.
Давайте структурируем наш код и создадим необходимые файлы в соответствующих папках. Итак мы создали 3 новых файла, каждый в своей папке (Models, ViewModels, API service).
import Foundation class EmployeeViewModel: NSObject { }
Модель
Модель представляет простые данные. Она просто хранит данные и ничего не реализует никакой бизнес логики. Мы может сказать что она просто хранит структуру данных которую мы ожидаем от нашего API.
здесь мы может проверить ответ от вышеуказанного URL и создадим класс модели для этого ответа. Вы можете создать модель сами руками или вы можете использовать онлайн сайты генераторы.
/ MARK: - Employee struct Employees: Decodable { let status: String let data: [EmployeeData] } // MARK: - EmployeeData struct EmployeeData: Decodable { let id, employeeName, employeeSalary, employeeAge: String let profileImage: String enum CodingKeys: String, CodingKey { case id case employeeName = "employee_name" case employeeSalary = "employee_salary" case employeeAge = "employee_age" case profileImage = "profile_image" } }
Флоу приложения выглядит следующим образом:
- Будет вызван ViewController и представление будет ссылаться на ViewModel.
- представление будет получать пользовательскние действия и далее вызывать ViewModel
- VideModel будет запрашивать APIService и APIService возвращает результата во ViewModel
- Когда мы получим ответ, ViewModel уведомляет представление через биндинг
- Представление обновляет наш интерфейс с новыми данными.
Итак, теперь мы последовательно начнем писать наш код. Во первых вызовем ViewController и экран ViewController’а, затем мы вызовем наш класс ViewModel. На данный момент мы не будет создавать биндинг. Мы сделаем это позже.
import UIKit class ViewController: UIViewController { private var EmployeeViewModel: EmployeeViewModel! override func viewDidLoad() { super.viewDidLoad() callToViewModelForUIUpdate() } func callToViewModelForUIUpdate() { self.EmployeeViewModel = EmployeeViewModel() } }
ViewModel
ViewModel это главный компонент этого архитектурного шаблона. ViewModel никогда не узнает что такое представление и что оно делает. Это позволяет сделать архитектуру более тестируемой и убрать сложный код из представления.
Во ViewModel, мы вызовем наш класс APIServer для получения данных из сервера.
import Foundation class EmployeeViewModel: NSObject { private var apiService: APIService! override init() { super.init() self.apiService = APIService() callFuncToGetEmpData() } func callFuncToGetEmpData() { self.apiService.apiToGetEmployeeData { (empData) in print(empData) } } }
Когда вы будете писать этот код в классе EmployeeViewModel, вы получите ошибку что не реализовали класс APIService. Итак давайте сделаем это и реализуем класс APIService.
API сервис
import Foundation class APIService : NSObject { private let sourcesURL = URL(string: "http://dummy.restapiexample.com/api/v1/employees")! func apiToGetEmployeeData(completion : @escaping (Employees) -> ()){ URLSession.shared.dataTask(with: sourcesURL) { (data, urlResponse, error) in if let data = data { let jsonDecoder = JSONDecoder() let empData = try! jsonDecoder.decode(Employees.self, from: data) completion(empData) } }.resume() } }
Класс APIService очень простой, где мы получаем данные работников используя класс URLSession. Вы можете использовать любой способ работы с сетью для получения данных из сервера. Из нашего класса ViewModel мы запрашиваем класс APIService.
Мы получим ответ от API в класс ViewModel. Сейчас пришло время создать биндинг между ViewController’ом и ViewModel.
MVVM бидинг
Биндинг MVVM играет жизненно важную роль в нашем проекте. Общается между ViewModel и ViewСontroller очень важно. Биндинг можно осуществить разними способами.
import Foundation class EmployeesViewModel : NSObject { private var apiService : APIService! private(set) var empData : Employees! { didSet { self.bindEmployeeViewModelToController() } } var bindEmployeeViewModelToController : (() -> ()) = {} override init() { super.init() self.apiService = APIService() callFuncToGetEmpData() } func callFuncToGetEmpData() { self.apiService.apiToGetEmployeeData { (empData) in self.empData = empData } } }
Мы оздадим свойство в классе ViewModel с именем bindEmployeeViewModelToController.
var bindEmployeeViewModelToController : (() -> ()) = {}
Это свойство должно вызывается из класса ViewController’а
Мы создадим другое свойство в классе ViewModel с названием empData типа Employee (Модель), которая получает результат выполнения APIService и уведомляет представление что были изменения.
private(set) var empData : Employees! { didSet { self.bindEmployeeViewModelToController() } }
empData устанавливает результат ответа от APIService. При помощи обсервера, и как только мы получили данные в empData в качестве ответа от API, вызывается didSet переменной empData и далее вызывается bindEmployeeViewModelToController() внутри didSet.
Когда мы получили данные в представлении из ViewModel, значит пора обновлять UI.
Представление
Для получения данных из ViewModel мы связали наше свойство ViewModel внутри класса ViewController.
self.employeeViewModel.bindEmployeeViewModelToController = { self.updateDataSource() }
import UIKit class ViewController: UIViewController { @IBOutlet weak var employeeTableView: UITableView! private var employeeViewModel : EmployeesViewModel! private var dataSource : EmployeeTableViewDataSource<EmployeeTableViewCell,EmployeeData>! override func viewDidLoad() { super.viewDidLoad() callToViewModelForUIUpdate() } func callToViewModelForUIUpdate(){ self.employeeViewModel = EmployeesViewModel() self.employeeViewModel.bindEmployeeViewModelToController = { self.updateDataSource() } } func updateDataSource(){ self.dataSource = EmployeeTableViewDataSource(cellIdentifier: "EmployeeTableViewCell", items: self.employeeViewModel.empData.data, configureCell: { (cell, evm) in cell.employeeIdLabel.text = evm.id cell.employeeNameLabel.text = evm.employeeName }) DispatchQueue.main.async { self.employeeTableView.dataSource = self.dataSource self.employeeTableView.reloadData() } } }
Для обновления вашего UI вы можете создать таблицу прямо со ViewController’е, но чтобы уменьшить ViewController и сделать менее запутанным, здесь я создам раздельный класс EmployeeTableViewDataSource расширяющийся от UITableViewDataSource.
import Foundation import UIKit class EmployeeTableViewDataSource<CELL : UITableViewCell,T> : NSObject, UITableViewDataSource { private var cellIdentifier : String! private var items : [T]! var configureCell : (CELL, T) -> () = {_,_ in } init(cellIdentifier : String, items : [T], configureCell : @escaping (CELL, T) -> ()) { self.cellIdentifier = cellIdentifier self.items = items self.configureCell = configureCell } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { items.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! CELL let item = self.items[indexPath.row] self.configureCell(cell, item) return cell } }
Как я уже сказал каждая архитектура имеет достоинства и недостатки, если у нас много преимущества с использованием шаблона MVVM, то у нас также есть и недостатки.
Недостатки MVVM:
- Сложность реализации MVVM для новичков.
- Приложения с простым UI MVVM может просто убить.
- Для больших проектов, биндинг данных будет сложный поэтому будет сложное обслуживание
Заключение
На этом этапе мы закончили с созданием архитектурного шаблона MVVM и его использования. Надеюсь это было полезно. Спасибо Вам! Счастливого кодирования!!
Программный код
Программный код для этого демо приложения лежит на ГитХабе в ветке MVVM_Swift. Вы можете скопировать ветку и поиграть с проектом MVVM.