MVVM в IOS Swift

Перевод статьи 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"
    }
}

Флоу приложения выглядит следующим образом:

  1. Будет вызван ViewController и представление будет ссылаться на ViewModel.
  2. представление будет получать пользовательскние действия и далее вызывать ViewModel
  3. VideModel будет запрашивать APIService и APIService возвращает результата во ViewModel
  4. Когда мы получим ответ, ViewModel уведомляет представление через биндинг
  5. Представление обновляет наш интерфейс с новыми данными.

Итак, теперь мы последовательно начнем писать наш код. Во первых вызовем 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.