一个有效解决多种类型cell带来的代码臃肿和事件处理不明确的设计架构

2,780 阅读5分钟

前言

在日常开发中一个好的项目结构可以带来清晰的思路在维护代码和同事之间的协助上降低了成本,而在当前IM技术日益推广的背景下很多同行都会面临对IM相关业务的开发。在这里我们通过对业务层对IM架构进行分拆和大家探讨一下合理的项目架构 项目架构是MVP设计模式:

image.png

现状

IM消息有多种多样例如文字消息、图片消息、语音消息、图文消息、视频消息等,这些消息在数据结构上来讲都有相同的数据模型可以分为消息内容、消息ID、消息类型、是否展示时间、是否重发等.在界面呈现上每个消息直接的cell同样存在相同的元素因此我们往往对模型和cell采取继承方案 但是在tableivew的数据源方法中我们不免会判断当前的数据模型是属于哪一种消息从而去加载对应类型的cell,如果每个cell当中都存在一些事件需要传递到controller那么我们的tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell方法就会十分臃肿。下面我用3种cell的情况我们看看效果

image.png

img_v2_b72abe49-e865-4e56-8631-1de8417109ag.png

从上面可以看出,3种类型的cell所产生的代码已经相当多,如果有更多的消息类型这里的代码变得更臃肿。另外如果我们把所有的事件处理都放在presenter中处理那么随着消息类型的增加cellforrow方法和presenter也会不断增加代码,不方便维护。特别是像音频播放会涉及多个逻辑当其他同事来维护或者修改逻辑时要查找和修改对应的代码也是费心费力。

改造

下面我们来聊聊我的一个改造方案

1.对Controller的改造

  1. cell的注册,这里的技巧是采用消息的messageType作为cell的唯一id.这样做可以达到模型驱动UI的效果,下面结合tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell就能看出效果
  private func registCell(){
        tableView.register(ChatTextCell.self, forCellReuseIdentifier: Self.cellIdentifier+"(MessageModel.IMMessageType.text.rawValue)")
        tableView.register(ChatImageCell.self, forCellReuseIdentifier: Self.cellIdentifier+"(MessageModel.IMMessageType.image.rawValue)")
        tableView.register(ChatVoiceCell.self, forCellReuseIdentifier: Self.cellIdentifier+"(MessageModel.IMMessageType.voice.rawValue)")
    }
  1. tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell方法的改造,下面不到6行的方法就能解决多种类型cell的判断问题
   func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
        let msgModel = presenter.dataSource[indexPath.row]
        let cell:BaseTableViewCell? = tableView.dequeueReusableCell(withIdentifier: Self.cellIdentifier + "(msgModel.messageType.rawValue)") as? BaseTableViewCell
        return cell ?? UITableViewCell()
        
    }

看一下UI效果和原来的一致,代码量大大减少

img_v2_dac8209a-c1fd-421e-ba2a-486de6ba47cg.png

2.对cell的事件进行改造

  1. 在BaseTableViewCell中定义事件统一回调
     /*
      event: ChatEvenType枚举类型的事件
     msgModel: 消息模型
     */
    var eventCallback:((_ event: ChatEvenType, _ msgModel: MessageModel?)->Void)?

ChatEvenType如下

   enum ChatEvenType: Int32{
      case CellTapText = 0
      case CellImageLongPress  = 1
      case CellVoiceStopPlayTap  = 2
       //如果有其他事件可以在下面定义
  }
  1. 在对应的cell的事件中添加回调,举文字信息为例,图片、声音的原理相同
    //MARK: Action
    @objc private func mesLabelClick(){
        guard let textModel = msg as? TextMsgModel else{return}
//        self.clickTextClosure?(textModel) 原来的回调方案
        self.eventCallback?(.CellTapText,textModel) 
    }
    
  1. Controller上的回调
 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
        let msgModel = presenter.dataSource[indexPath.row]
        let cell:BaseTableViewCell? = tableView.dequeueReusableCell(withIdentifier: Self.cellIdentifier + "(msgModel.messageType.rawValue)") as? BaseTableViewCell
        cell?.msg = msgModel
        cell?.eventCallback = {
            [weak self](evenType,model) in
            guard let self = self else {
                return
            }
            //presenter新增dispatchEvent方法
            self.presenter.dispatchEvent(evenType, model)
        }
        return cell ?? UITableViewCell()
        
    }
    

由此看20行以内的代码 就实现了在func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell方法加载不同类型cell并且把事件响应交给了presenter。最重要的事情是此时无论添加多少种消息类型每种消息当中添加多少种事件回调cellforRow方法都不需要动任何代码,只需要在注册cell的方法 private func registCell()把对应的消息类型注册进去就可以了

3. Presenter的改造

  1. 目前我们还没解决所有时间响应都是在Presenter中处理的困境,代码臃肿不利维护的问题还是存在。为了解决这个问题我引入了Action这个概念,每个Action对应指定的一种消息,该消息的事件处理就在对应的Action中处理,每个Action遵循ChatActionProtocol协议

image.png 2. 初始化时init()注册Action

//MARK: 注册Action
    private mutating func registAction(){
        
        let  textActionKey = ViewController.cellIdentifier + "(MessageModel.IMMessageType.text.rawValue)"
        actionDic[textActionKey] = ChatTextAction()
        
        let imgActionKey = ViewController.cellIdentifier + "(MessageModel.IMMessageType.image.rawValue)"
        actionDic[imgActionKey] = ChatImageAction()
        
        let voiceActionKey = ViewController.cellIdentifier + "(MessageModel.IMMessageType.voice.rawValue)"
        actionDic[voiceActionKey] = ChatVoiceAction()
        
    }
  1. func dispatchEvent(_ evenType:ChatEvenType, _ msgModel: MessageModel)方法中找出对应的action对事件进行分发
func dispatchEvent(_ evenType:ChatEvenType, _ msgModel: MessageModel){
        let key = ViewController.cellIdentifier + "(msgModel.messageType.rawValue)"
        if actionDic.keys.contains(key), let action:ChatActionProtocol = actionDic[key]{
            action.dispatchEvent(evenType, msgModel)
        }
    }

4. Action中的实现

  1. ChatTextAction
import Foundation
struct ChatTextAction: ChatActionProtocol {
    func dispatchEvent(_ evenType: ChatEvenType, _ msgModel: MessageModel) {
        if let textModel = msgModel as? TextMsgModel {
            switch evenType {
            case .CellTapText:
                print("当前点击的文字是" + textModel.title)
            default:
                print("其他的事件忽略")
            }
        } 
    }
}
  1. ChatImageAction
import Foundation
struct ChatImageAction:ChatActionProtocol {
    func dispatchEvent(_ evenType: ChatEvenType, _ msgModel: MessageModel) {
        if let imgModel = msgModel as? ImageMsgModel {
            switch evenType {
            case .CellImageLongPress:
                print("长按图片查看高清图")
            default:
                print("其他的事件忽略")
            }
        }
    }
}

事件传递架构

image.png

小结

至此IM业务架构构建完成,我们可以看到在在新增消息时候在func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell中我们是不需要添加任何代码的,只需要把对应的cell注册就好实现用少代码实现复杂功能的效果;另外事件分工上是明确的,当我们要修改或者处理音频相关的事件时,直接找到ChatVoiceAction即可,大大提高了代码的可维护度,以及降低了同事之间的沟通成本.因本人业务中是IM业务所以以IM业务侧举例,如果大家遇到类似的情况也可以参考上述框架。

Demo gitee地址:gitee.com/liangaijun/…