缘起
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整体上更加的稳定,不管是添加一个模块还是从整体中删除掉某一块子业务模块,对整体架构来说基本是没有很大的影响。
最终输出效果
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…