MVC

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
    }

  • 实现的效果如下
裁剪视频.2019-01-25 16_54_19.gif
大致上实现了效果,但是VC内部的逻辑冗余。
  1. moel层分布在ViewController中
  2. 点击添加按钮和删除操作时
    1. 维护model
    2. 添加或者删除Cell
    3. 维护按钮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])
    }

 }

注意点
  1. 数据模型,ToDoItem,要实现Hashable协议,主要是数组(Array)转集合(Set)操作的时候要用,主要是查找查找变化的数据。用集合比较方便。
  2. 根据model查找index 的时候要用.lastIndex
  3. 通知的名字要初始换成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
    }

总结

  1. model层比较清晰,易于维护
  2. UI操作 -> Controller 模型修改 -> 更新UI, 这样数据流向就是严格可预测的
  3. model层不再被单一controller所拥有,其他的controller也可以操作数据模型,达到一处修改,处处更新UI的目的。

相关项目demo

本文相关想法及其灵感来自大神 OneV's Den, 原文 博客地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。