MVC误区及后期维护成本
iOS开发中最老生长谈的就是MVC, 但是我们正常人使用MVC架构的时候,把逻辑代码往往都堆积在C(Controller)层中,C层中就混杂了UI刷新,数据层的构建,页面跳转。现在有两个vc,一个展示数据列表命名为vc1, 另一个是数据详情页命名为vc2。 正常会把包括“用户操作,模型变更,UI反馈”等操作全部放在Controller中,造成Controller过度臃肿,逻辑不清晰,后期维护或者业务扩充的时候牵一发而动全身。
例子
举一个简单的View Controlelr例子,假设有一个Table View Controller,用来展示To Do列表,我们需要声明一个To Do List数组用来存储数据。然后其他的UI操作有,点“加号+” 添加一个ToDoItem, 左滑删除一项To Do, 当To Do List中数据大于10条时“ + 号”不可点击。当我们点击 “ +” 或者 左滑删除时需要实时的判断To Do List 的数据条数然后修改按钮的可点击状态。
代码
- 申明一个ToDoItem 模型,注意用struct类型, 占用更少的内存。
```
/// 数据源: 添加记录
struct ToDoItem{
/// id
var id: Int
/// 标题
var title: String
/// initialize
///
/// - Parameter id: Int
init(id: Int) {
self.id = id
self.title = "\(id)"
}
}
```
-数据源数组, 一个List
///datasource array
var toDoList: [ToDoItem] = []
-添加调用方法和删除调用方法,以及刷新UI相关方法
/// 添加方法
@objc func addAction(){
let count = self.toDoList.count
self.toDoList.append(ToDoItem.init(id: count))
self.tableView.insertRows(at: [IndexPath.init(row: count, section: 0)], with: .automatic)
updateEditButtonStatus()
}
/// 删除方法
@objc func deleteAction(index: Int){
self.toDoList.remove(at: index)
self.tableView.deleteRows(at: [IndexPath.init(row: index, section: 0)], with: .automatic)
updateEditButtonStatus()
}
///更新按钮的状态
func updateEditButtonStatus(){
self.editButtonItem.isEnabled = self.toDoList.count < 10
}
-左滑删除部分
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let contextAction = UIContextualAction.init(style: .destructive, title: "Delete") { (_, _, done) in
self.deleteAction(index: indexPath.row)
done(true)
}
let actionsConfig = UISwipeActionsConfiguration.init(actions: [contextAction])
return actionsConfig
}
- 实现的效果如下
大致上实现了效果,但是VC内部的逻辑冗余。
- moel层分布在ViewController中
- 点击添加按钮和删除操作时
- 维护model
- 添加或者删除Cell
- 维护按钮Button的状态
也就是说数据源的添加删除和UI的刷新和用户的触摸事件都在一起。如果在vc2中也就是详情页中做出了删除操作,这时要同步刷新vc1列表中的数据,这时我们要用代理,先声明一个协议,协议中中添加一个删除方法和一个修改方法,然后在vc2中添加协议的实例变量,vc1中申明为协议的代理。代理中也要调用删除操作。这样又增加了vc1中的代理,显示上更乱, 层级更多了。
Controller层优化解耦合
添加一个ToDoItemStore的管理方法,里面有ToDoItemList,还有对list的操作,比如删除,添加,查找索引对饮数据源,返回数据源对应索引, 返回数据源的个数,最后还有操作完数据源之后的通知操作。
// MARK: - 通知的名字
extension Notification.Name {
static let didChangeItemsNotification = Notification.Name.init("change.notification")
}
class ToDoItemStore: NSObject {
static let share = ToDoItemStore()
enum ToDoItemProviderBehavior{
case add([Int])
case remove([Int])
case reload
}
/// 数据源
private var items = [ToDoItem]() {
didSet{
let behavior = diff(originItems: oldValue, now: items)
sendNotificationWith(behavior: behavior)
}
}
/// 数量
var count: Int {
return self.items.count
}
/// 添加一个数据
///
/// - Parameter index: 索引
func append(index: ToDoItem) {
if let _ = items.lastIndex(of: index){
return
}
self.items.append(index)
//let maxIndex = self.items.count - 1
}
/// 移除某一行的数据
///
/// - Parameter index: 索引
func remove(item: ToDoItem){
if let index = self.items.lastIndex(of: item){
self.items.remove(at: index)
}
}
func removeIndex(of index: Int) {
if index < self.items.count{
self.items.remove(at: index)
}
}
/// 获取最大的当前数据源中,id最大的Element的id
var maxIndex: Int {
let max = self.items.max { $1.id > $0.id }?.id
print(max)
if let max = max {
return max + 1
}
return 0
}
/// 获取index对应的Item
///
/// - Parameter index: Int对象
/// - Returns: 返回Element
func itemOfIndex(index: Int) -> ToDoItem?{
guard index < self.items.count else{
return nil
}
return self.items[index]
}
/// 返回Element对应的索引,分别匹配id和title,使用过程是重载了“==”
///
/// - Parameter item: Element = ToDoItem
/// - Returns: Int 对应的索引
func indexOf(item: ToDoItem) -> Int?{
guard let index = self.items.lastIndex(of: item) else{
return nil
}
return index
}
/// 比较两个数据源的数据
///
/// - Parameters:
/// - originItems: 修改之前的数据
/// - now: 现在的真实数据
/// - Returns: ToDoItemProviderBehavior
func diff(originItems: [ToDoItem], now: [ToDoItem]) -> ToDoItemProviderBehavior{
let nowSet = Set(now)
let originSet = Set(originItems)
//originSet是nowSet的子集
if originSet.isSubset(of: nowSet) {
//找到新增的元素集
let addedItems = nowSet.subtracting(originSet)
print(addedItems)
// let addedItemsIndex = addedItems.compactMap{ now.index(of: $0)}
let addedItemsIndex = addedItems.compactMap { (item) -> Int in
print(item)
let index = now.lastIndex(of: item)
return index!
}
return .add(addedItemsIndex)
}else if nowSet.isSubset(of: originSet) {
let lessItems = originSet.subtracting(nowSet)
let addedItemsIndex = lessItems.compactMap { (item) -> Int in
print(item)
let index = originItems.lastIndex(of: item)
return index!
}
return .remove(addedItemsIndex)
}
return .reload
}
/// 发送一条通知
///
/// - Parameter behavior: ToDoItemProviderBehavior
func sendNotificationWith(behavior: ToDoItemProviderBehavior) {
NotificationCenter.default.post(name: .didChangeItemsNotification, object: self, userInfo: [Notification.Name.didChangeItemsNotification: behavior])
}
}
注意点
- 数据模型,ToDoItem,要实现Hashable协议,主要是数组(Array)转集合(Set)操作的时候要用,主要是查找查找变化的数据。用集合比较方便。
- 根据model查找index 的时候要用.lastIndex
- 通知的名字要初始换成Notification.Name, 这样一方面比较统一,使用方便
结果是 ViewController中代码就得到了最大程度的简化, 只需要接收通知,然后根据类型,刷新UI就行了
@objc func toItemChange(notification: Notification) {
let userinfo = notification.userInfo
let behavior = userinfo![Notification.Name.didChangeItemsNotification] as! ToDoItemStore.ToDoItemProviderBehavior
switch behavior {
case .add(let todoItemList):
let addListPath = todoItemList.map { IndexPath.init(row: $0, section: 0)}
print(addListPath)
if addListPath.isEmpty {
return
}
self.tableView.insertRows(at: addListPath, with: .none)
case .remove(let todoItemList):
let deleteListPath = todoItemList.map { IndexPath.init(row: $0, section: 0)}
print(deleteListPath)
self.tableView.deleteRows(at: deleteListPath, with: .automatic)
case .reload:
self.tableView.reloadData()
break
}
self.addButton.isEnabled = store.count < 10
}
总结
- model层比较清晰,易于维护
- UI操作 -> Controller 模型修改 -> 更新UI, 这样数据流向就是严格可预测的
- model层不再被单一controller所拥有,其他的controller也可以操作数据模型,达到一处修改,处处更新UI的目的。
本文相关想法及其灵感来自大神 OneV's Den, 原文 博客地址