iOS 客户端 IM 消息卡片插件化

news2024/9/29 0:45:34

背景

目前探探 IM 聊天消息列表由于长年累月的代码堆积,对业务迭代产生了很多的困扰。所以趁着工作中的一些空隙,对聊天页消息卡片做了插件化,使得不同的消息类型,可以根据具体需求方便的增删迭代。下面分享一下自己重构过程中一些有趣的想法。虽然目前是在聊天消息列表中进行实践的,但对于各种复杂 Feed 流业务也有一定的借鉴意义。

此次插件化改造使用了 IGListKit,这是一个优秀的数据驱动 UICollectionView 展示的框架,有不少思路也是从这个框架里的一些设计中得到了灵感,有兴趣可以找一些文章看看。当然 IGListKit 也不是必要的,是否使用它也不影响我们的设计。

聊天消息列表可以泛化为一个有较多不同复杂业务逻辑卡片的 Feed 列表,不过聊天消息列表还是有不少独特的内容,比如涉及数据库存储读取,消息收发,实时更新等等,不过在本文中我们暂时忽略这些内容。这里先立个 flag,有时间另外写一篇文章,来单独介绍 IM 消息数据的管理以及如何驱动消息列表展示的。希望这篇文章能够对大家日常开发中有一定的帮助,不足之处,敬请指正。

应用场景

下面简要描述一下当前的应用场景,目前探探 IM 聊天消息列表中流通着约 120+ 种消息,很多消息除了展示以外还有特殊逻辑,下面简单描述一下

  1. 点击跳转到特殊页面:比如邀请类消息、活动类消息,Feed 流里跳转详情等;
  2. 存在特殊功能按钮:比如投票类消息,卡片里有点赞按钮;
  3. 同步展示业务数据:比如消息里引用了一个有时效的状态,并且产品要求需要实时更新状态;
  4. 多消息之间的联动:比如切换播放多条音频消息,或者某条卡片展示的时候要更新其他卡片。

下面我们看一下如何设计架构,来优雅的支持这些需求

消息卡片都涉及哪些内容

MessageCardViewModel 是负责携带消息卡渲染信息、以及逻辑处理工具的模型,消息 UI 数据源以及逻辑处理均通过 ViewModel 信息以及附属的工具。主要承载通过 message 解析完成后的静态内容;还会绑定一些具有逻辑处理能力的工具以及业务配置信息,各种不同消息可以通过 BaseMessageCardViewModel 来实现多态。

下面我们介绍一下 ViewModel 里都包含哪些内容,都涉及哪些类,这些类都是做什么的:
需要注意一下,为防止 ViewModel 在数据流传递的过程中被意外修改,对外所有属性都是 readonly 的,来保证数据流的数据生产到消费是单向的,下面是 ViewModel 所包含的具体内容

一、message 解析内容

message 解析内容是通过对消息原始数据,解析为消息卡片直接使用的数据,BaseMessageCardViewModel 包含 message 的原始数据,以及通过 message 解析而来可以直接用于兜底展示的属性,比如 displayedText 等。

其他消息可以通过继承 BaseMessageCardViewModel 来组装自己所需要的属性,比如 VideoMessageCardViewModel 可以通过继承,增加 videoInfo 属性,该属性也是通过 message 原始数据解析而来,对应的 UI 可以通过 videoInfo 等信息来渲染。

二、配置信息

通过列表的代理传入,通过 MessageCardViewModelFactory 绑定到具体的 ViewModel 中,配置信息包含主题配置、样式配置等。

主题配置,包括各 UI 元素的颜色、字体、背景等等
样式配置,包括是否展示昵称、备注、是否需要模糊头像、自定义的头像 url 等等

比如这种文本的样式,就需要代理根据具体的产品逻辑,对不同的消息返回不同的气泡背景颜色,然后绑定到 ViewModel 中,消息卡片 UI 直接使用 ViewModel 中主题信息对应的背景颜色字段来渲染
请添加图片描述

三、卡片 UI 组件信息

卡片 UI 组件是指可以独立通过 MessageCardViewModel 模型来渲染的 UI 组件,每个 UI 组件信息初始化方法以及内容更新方法一致,每一个独立的 UI 组件都可以灵活的复用。也就是说拿到类名以及对应的 ViewModel 就可以渲染,以及内部交互手势。通常一个消息卡片是一个容器,内部包含多个卡片 UI 组件,所以卡片 UI 组件信息主要包含一个 UI 组件的 Class 数组。

该部分内容是外部根据消息类型进行注册的,然后通过 MessageCardViewModelFactory 绑定到具体的 ViewModel。

下面举个例子说明一下:

每个暗黄色框起来的元素都是一个独立的卡片 UI 组件,其中每个红色框起来的元素是一个消息卡片。
上面这条消息引用了一条动态,会包含了一个动态 UI 组件以及一个普通文本消息 UI 组件;
下面这条消息就引用了一个话题,会包含一个话题 UI 组件以及一个普通文本消息组件。
业务接入方,就可以根据对应的消息类型,注册对应的卡片 UI 组件

请添加图片描述
四、MessageCardPlugin

MessageCardPlugin 后面会单独详细介绍,和卡片 UI 组件信息类似,也是是通过业务接入方,根据消息类型进行注册的,然后通过 MessageCardViewModelFactory 绑定到具体的 ViewModel 上。消息 Cell(卡片 UI 组件)可以直接从 ViewModel 拿到 MessageCardPlugin,直接进行业务数据之间的交互,从而避免通过业务 delegate 传给 cellDelegate 再传给对应 ViewDelegate 等等多层传递,每次更改接口就改好多层的情况。

MessageCardPlugin 负责为消息卡片提供业务相关数据源,同时可以通过初始化传入的上下文工具,来更新全部或者局部的 UI。消息卡片内部的每个卡片 UI 组件,都可以通过 ViewModel 获取到对应的 MessageCardPlugin。

同一聊天会话中,相同业务的消息,可以注册同一个 MessageCardPlugin,来使多个消息卡片共同持有同一个 MessageCardPlugin 实例,从而拥有共同的业务数据源。

由于同一条消息不太可能同时属于两个业务,为防止开发风险,每种消息类型只可绑定一个 Plugin,来隔离各个业务。如果确实存在横跨多个业务的情况,Plugin 内部可以采用伪单例或者其他共享内存的方式来实现,避免对消息列表架构造成破坏。

贴一张图看一下 ViewModel 是怎么生成的,其中,message 解析对应具体的 ViewModel 类、Plugin 以及 UI 组件信息都是业务接入方注册的。

请添加图片描述

如何解决问题呢

我们回到 #应用场景 小节提到的几个问题,看如何通过 MessageCardPlugin 来优雅的解决:

  1. 点击跳转到特殊页面存在特殊功能按钮 本质属于同一种问题,可以给消息对应的 Plugin 暴露业务跳转或者点赞等功能逻辑接口,消息 Cell 可以通过 ViewModel 拿到 Plugin,来调用对应的业务功能。

  2. 同步展示业务数据:这个也比较简单,消息 Cell 通过 ViewModel 拿到 Plugin,Plugin 内部可以和具体业务进行交互,根据消息体引用的内容 ID 字段直接获取缓存信息,或者异步获取再刷新均可;

  3. 多消息之间的联动:Plugin 内部监听数据变更,刷新对应的消息卡片。也可以暴露回调接口,对应的消息卡片监听回调接口,每次内容变更都通知对应的消息卡片更新 UI 就实现了多消息之间的联动等等。

下面来段伪代码示例如何处理上面的问题的

// 这个是 Cell
class MomentPreviewMessageCell: BaseMessageCell {
    
    private var plugin: MomentMessageCardPlugin? {
        return viewModel.messageCardPlugin as? MomentMessageCardPlugin
    }
    
    ... 省略代码

    // 针对问题 1,处理动态点赞事件(处理特殊事件)
    private func handleTappedLikeButton() {
        plugin.performLikeAction(viewModel.momentID)
    }

    override func render(_ viewModel: BaseMessageCardViewModel) {
        super.render(viewModel)
        // 从 plugin 获取对应的业务数据, 刷新 UI
        renderMoment(plugin.moment(viewModel.momentID))
    }
    ... 省略代码
}

class MomentMessageCardPlugin: BaseMessageCardPlugin {
    
    // 针对问题 1,
    func performLikeAction(_ momentID: String) {
        // 请求 like 接口
        requestLike(momentID) { success in
            // updateCaches
            // 完成回调, 刷新消息卡片
            context.reloadMessageCard()
        }
    }
    
    // 为消息卡片提供,业务方面的数据源
    func moment(momentID: String) -> Moment? {
        // 优先获取缓存
        if let moment = likedMomentCaches[momentID] {
            return moment
        }
        requestMoment(momentID) { moment in 
            // updateCaches
        }
        return nil
    }
    
    // 针对问题 2、3,同步业务数据,联动刷新多个 Cell 等
    private func handleNotification(_ data: Any) {
        // 收到了业务变更的通知,保存数据源,刷新列表
        updateCaches(data)
        context.reloadMessageCards()
    }
}

通过上面的代码可以看出来,消息 Cell 和 MessageCardPlugin 是业务耦合比较紧密的,这时候要注意规范明确的接口,保证 UI 与逻辑是分开的,如果 MessageCardPlugin 业务逻辑比较复杂的时候也需要一定的设计模式保证代码质量。

总结

这里大概介绍了 IM 消息卡片的插件化,以及如何处理前面提出的问题,不过并没有介绍进行插件化之前的情况,缺少了点对比说明,考虑到每个项目都有自己的实现方式,还有有兴趣的朋友结合自己项目中的实际情况来对比吧。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/349300.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

项目经理,千万不要在这时候跳槽

早上好,我是老原。节后开工也一段时间了,有不少小友私信老原想要面试题库,大多都是想要跳槽涨薪的......当然除了在做准备的,也有不少朋友都在诉苦:其实,不少人回头去看自己过去经验感觉就像个打杂的&#…

PCB中的HDI板生产中的变化

关键词:HDI概述 HDI发展演变 HDI生产难点如果把一整个电子产业比作浩瀚的宇宙,那些智能电子设备就像宇宙中闪耀的星光,当你以“上帝”的视角手持放大镜去观察时,这些闪烁的星光点点其实都是一个个由精密的“自然规律”所“设计”好…

金三银四丨黑蛋老师带你剖析-CTF岗

作者丨黑蛋二进制是个庞大的方向,对应着许许多多方向的岗位,除了之前说过的逆向岗位,漏洞岗位,病毒岗位,还有专门打CTF的岗位,CTF是网络安全领域的一种比赛。普遍来讲,大学生学习网络安全都会参…

percona软件介绍 、 innobackupex备份与恢复

1. 常用的mysql备份工具 物理备份缺点: 跨平台差。备份时间长、冗余备份、浪费存储空间。 解释如下:如Linux操作系统和Windows操作系统之间,由于文件系统不一样,如Linux操作系统的文件系统是ext4、xfs,Windows操作系统…

K8s+SpringBoot+gRpc

本文使用K8s当做服务注册与发现、配置管理&#xff0c;使用gRpc用做服务间的远程通讯一、先准备K8s我在本地有个K8s单机二、准备service-providerpom<?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.…

浅谈性能测试监控系统,做好关键指标的监控

随着业务的增长&#xff0c;服务器部署由单一架构向分布式集群架构转变&#xff0c;性能测试过程中指标监控也由单一服务器向集群服务器转变。 对于性能测试团队来说&#xff0c;需要建立起适用于测试的多机监控系统&#xff0c;以便后期顺利且高效地进行监控分析调优&#xf…

Java程序员拿下高薪offer需要具备哪些能力?这份Java面试专题汇总助你拿下心仪offer!!

背景今天这篇文章的灵感来自一个粉丝的亲身经历&#xff0c;想必也是求职浪潮中很多朋友的经历&#xff0c;内卷大环境找不到满意工作的人太多了&#xff0c;之前也有很多人问过我怎么才能找到不错的工作&#xff0c;甚至是进大厂&#xff0c;所以今天就借这位粉丝的经历来聊聊…

对JAVA 中“指针“理解

对于Java中的指针&#xff0c;以下典型案例会让你对指针的理解更加深刻。 首先对于&#xff1a; 系统自动分配对应空间储存数字 1&#xff0c;这个空间被变量名称b所指向即: b ——> 1 变量名称 空间 明…

linux下yum安装consul实现动态配置管理

一、yum安装consul #安装yum-utils yum install -y yum-utils#配置consul的下载仓库 yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo#必须上面步骤&#xff0c;不然会找不到仓库 yum -y install consul#查看版本 consul -v 二、启动…

基于深度学习的三维重建(二):pytorch的简单操作及DataLoader、Dataset类简介

目录 1.numpy举几个demo 2.pytorch基础 2.1 tensor介绍 3.简单版DataSet & DataLoader 4.模型构建 5.深度学习模型demo&#xff1a;手写文字识别 5.1 构建网络 5.2 前向传播过程 5.3 训练部分 5.4 测试部分 5.5 模型导出 5.6 模型测试 6.pytorch可视化工具ten…

MySQL数据库调优————索引数据结构

B-TREE B-TREE数据结构 B-TREE特性 根节点的子结点个数2 < X < m&#xff0c;m是树的阶 假设m 3&#xff0c;则根节点可有2-3个孩子 中间节点的子节点个数m/2 < y < m 假设m 3&#xff0c;中间节点至少有2个孩子&#xff0c;最多3个孩子 每个中间节点包含n个关…

《MySql学习》 行锁对业务的影响

一. 行锁介绍 行锁由各个存储引擎分别实现&#xff0c;MyISAM存储引擎是不支持行锁的&#xff0c;这也是MySQL使用InnoDB作为默认存储引擎的一个重要原因&#xff0c;锁更细的InnoDB能支持更多的并发业务。但需要注意的是&#xff0c;行锁在InnoDB的实现是给索引加的锁&#x…

智慧养殖无线通讯解决方案

一、方案概述农植畜禽/水产养殖智能监控系统可以在远端设备实现对如温度、湿度、气体浓度、光照度等传感设备的自动调节与控制功能。管理者可随时通过电脑了解养殖场各环节的运行状况&#xff0c;并根据养殖现场内外环境因子的变化情况将命令下发到现场执行设备。为动植物营造舒…

docker-compose安装SonarQube

前言SonarQube 是一个开源的代码分析平台, 用来持续分析和评测项目源代码的质量。 通过SonarQube我们可以检测出项目中重复代码&#xff0c; 潜在bug&#xff0c; 代码规范&#xff0c;安全性漏洞等问题&#xff0c; 并通过SonarQube web UI展示出来。一、docker-compose配置#v…

【Python】编写代码实现指定下标值顺序进行正序和倒序排序算法编程

&#x1f389;&#x1f389; 在本次python文章中&#xff0c;主要通过定义一个排序方法&#xff0c;实现一组数列能够按照另一组数列指定的位置进行重新排序输出&#xff0c;默认正序排序&#xff0c;可通过True表示逆序输出 目录1、知识点2、数列和元组1&#xff09;错误遍历方…

全网多种方式解决Knife4j文档请求异常

文章目录1. 复现问题2. 分析问题3. 解决问题4. 其他方法解决此异常5. 其他说明1. 复现问题 今天在本地启动项目后&#xff0c;刷新Knife4j接口文档&#xff0c;却报出如下错误&#xff1a; 即Knife4j文档请求异常。 2. 分析问题 报出Knife4j文档请求异常错误时&#xff0c;赶…

生活不一定很酷,但是一定要全力以赴

题记&#xff1a;努力是为了让自己不平庸 当看到这个话题“竞赛那些事”&#xff0c;我还是有所触动的&#xff0c;我本身就是一个不喜欢安逸&#xff0c;喜欢折腾的人&#xff0c;纵使不能把日子过成诗&#xff0c;也要折腾成向往的样子。 我的记忆在脑海中不停翻着页&#x…

黑马redis学习记录:分布式锁

一、基本原理和实现方式对比 分布式锁&#xff1a;满足分布式系统或集群模式下多进程可见并且互斥的锁。 分布式锁的核心思想就是让大家都使用同一把锁&#xff0c;只要大家使用的是同一把锁&#xff0c;那么我们就能锁住线程&#xff0c;不让线程进行&#xff0c;让程序串行…

Linux_基本权限

Linux入门第二篇已送达&#xff01; Linux_基本权限shell外壳权限Linux的用户分类角色划分Linux的文件文件类型查看权限目录的权限默认权限粘滞位shell外壳 为了保护操作系统&#xff0c;用户的指令不能由操作系统直接进行执行&#xff0c;需要一个中间者&#xff0c;比如Linu…

MySQL优化篇-MySQL压力测试

备注:测试数据库版本为MySQL 8.0 MySQL压力测试概述 为什么压力测试很重要&#xff1f;因为压力测试是唯一方便有效的、可以学习系统在给定的工作负载下会发生什么的方法。压力测试可以观察系统在不同压力下的行为&#xff0c;评估系统的容量&#xff0c;掌握哪些是重要的变化…