swift中的MVVM

1,164 阅读3分钟

缘起

MVC、MVVM、MVP……已经是个老生常谈的话题了,我因为中间有两三年时间没有在写iOS项目,对swift也是处在一个慢慢了解的过程,之前也一直没用过。现在需要写一个全新的iOS项目,很自然的就会考虑到UI层面的处理、网络请求框架、缓存、整体的项目构架等,这篇文章用来梳理一下在swift环境中MVVM最纯朴的一个状态和写法。

MVVM的角色说明

  • ViewController:view层的一部分,仅仅用来做跟UI Component的关联
  • ViewModel:从VC得到一些信息(指令),处理完指令并响应回VC
  • Model:数据模型,个人建议不要做别的事情,保持跟MVC里的model一致就好,被VM使用

viewController中保留一个对viewModel的引用,view将会发送一些用户操作、数据请求等一系列的action给到viewModel。viewModel中分发api request给到networking层,并将对应的response回调到viewModel。一旦接收到response的回调,viewModel通过notifies通知到viewController,view做出UI上的一些update处理

ViewModel是MVVM架构中最主要的部分,viewModel应该是不知道view的存在,viewModel和view的弱耦合性让MVVM整体上更加的稳定,不管是添加一个模块还是从整体中删除掉某一块子业务模块,对整体架构来说基本是没有很大的影响。

最终输出效果

Simulator Screenshot - iPhone 14 Pro - 2023-05-29 at 15.45.59.png

Model & APIService

这两个文件比较固定,数据模型和请求数据的接口

import Foundation

struct Employees: Decodable{
    let status: String
    let data: [EmployeeData]
}

struct EmployeeData: Decodable{
    let id, employeeSalary, employeeAge: Int
    let profileImage, employeeName: String
    
    enum CodingKeys: String, CodingKey{
        case id
        case employeeName = "employee_name"
        case employeeSalary = "employee_salary"
        case employeeAge = "employee_age"
        case profileImage = "profile_image"
    }
}

api层为了尽量简单,直接使用的URLSession,测试数据来源于dummy.restapiexample.com/api/v1/empl… 多次连续请求会失败

class APIService {
    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()
    }
}

在viewModel中建立viewController和view的关系

class EmployeeViewModel: NSObject{
    private var apiService: APIService!
    
    private(set) var empData: Employees!{
        didSet{
            self.bindEmployeeViewModelToController()
        }
    }
    
    // 这个bind方法会在controller里调用
    var bindEmployeeViewModelToController: (() -> ()) = {}
    
    override init() {
        super.init()
        
        self.apiService = APIService()
        self.apiService.apiToGetEmployeeData { empData in
            self.empData = empData
        }
    }
}

viewModel中持有empData属性,用于接收从api层回调的response数据,一旦获取到数据调用bindEmployeeViewModelToController,会触发viewController里的调用。controller里调用bindEmployeeViewModelToController后就可以拿到data数据并更新UI界面,核心代码如下

class EmployeeViewController: UIViewController {
    lazy var tableView: UITableView = {
        let tableView = UITableView(frame: .zero, style: .plain)
        tableView.register(EmployeeCell.self, forCellReuseIdentifier: "EmployeeCell")
        return tableView
    }()
    
    private var employeeViewModel: EmployeeViewModel!
    
    private var dataSource: EmployeeTableViewDataSource<EmployeeCell, EmployeeData>!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.navigationItem.title = "MVVM test"
        self.view.backgroundColor = .white
        
        view.addSubview(tableView)
        tableView.snp.makeConstraints { make in
            make.top.equalTo(view.safeAreaLayoutGuide.snp.top)
            make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom)
            make.leading.trailing.equalToSuperview()
        }
        callToViewModelForUIUpdate()
    }
    
    func callToViewModelForUIUpdate(){
        self.employeeViewModel = EmployeeViewModel()
        self.employeeViewModel.bindEmployeeViewModelToController = {
            self.updateSource()
        }
    }
    
    func updateSource(){
        self.dataSource = EmployeeTableViewDataSource(cellIdentifier: "EmployeeCell", items: self.employeeViewModel.empData.data, configureCell: { cell, evm in
            cell.employee = evm
        })
        
        DispatchQueue.main.async {
            self.tableView.dataSource = self.dataSource
            self.tableView.reloadData()
        }
    }
}

tableView delegate和source代理的抽离

为了尽可能的保持viewController的职责专一性,tableview里相关的协议统一收纳到一个文件里管理,单独处理delegate和source的回调事件

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_Swift文件夹下 引用: medium.com/@abhilash.m…