Skip to content
能力中心
产品中心
应用市场
WebOffice
开发者后台

自定义消息

协作中台 SDK 提供了丰富的消息类型(见 消息类型),当这些消息类型无法满足业务需求时,可接入新的消息类型。如下图的天气消息。

快速上手

步骤1:处理自定义消息的发送

调用 消息发送接口 发送自定义消息。举例:

Swift
let appCustomizeMsg = KIMAppCustomizeMsg(content: "your_content", customizeType: "your_customize_type",  msgDesc: "your_message_summary")
KIMCore.shared.sendMessage(chatId: "12312412", appCustomizeMsg: appCustomizeMsg)
Objective-c
KIMAppCustomizeMsg *appCustomizeMsg = [[KIMAppCustomizeMsg alloc] initWithContent:@"your_content" customizeType:@"your_customize_type" msgDesc:@"your_message_summary"];
[KIMCore.shared sendMessageWithChatId:@"12312412" appCustomizeMsg:appCustomizeMsg notices:nil extra:nil replyMsgId:nil msgConfig:nil pushConfig:nil];

步骤2:自定义消息 Cell(自定义消息的解析与样式处理)

处理自定义消息的解析

a. 实现KIMChatMessageDataSourceBuilder协议:将KIMMessageappCustomizeMsg字段内容,按需进行转换,并存储到KIMChatMessagecustomData字段中

b. 实现KIMChatMessageDataSourceBuilder协议:将KIMChatMessagecustomData字段内容,按需进行转换,并存储到KIMMessageCellStatemessageContent.customContent字段中

原理请参考 数据结构与转换流程

自定义消息 Cell

每一种自定义消息,都需要定义消息气泡的显示 Cell:

Swift
class YourCustomCell: KIMMessageBaseCell {
    override func setUp() {
        super.setUp()
        // 构建 cell 视图
    }

    override func configureCell(model: KIMMessageCellViewModel, tableView: UITableView) {
        super.configureCell(model: model, tableView: tableView)
        // 根据 model.cellState.messageContent 的 customContent 字段,配置 cell
        guard let content = model.cellState.messageContent.customContent as? yourCustomContent else {
            return
        }
        // 配置 cell 数据
        yourLabel.text = content.xxx
    }

}

步骤3:自定义消息摘要

系统默认使用自定义消息的msgDesc字段作为会话列表消息摘要。如果需要对摘要进行自定义,可以重写 manager 的getSummaryBodyNamegetSummaryBodyContent方法:

会话列表的消息摘要由两个部分构成:

  • name:消息发布者名称,可通过重写getSummaryBodyName来自定义

  • content:消息摘要,可通过重写getSummaryBodyContent来自定义

示例如下:

Swift
class YourMessageManager: KIMCustomChatMessageManager<YourMessageCell, YourAsyncTask, YourCacheType> {
    /// 自定义消息摘要名称
    override func getSummaryBodyName(appCustomizeMsg: KIMAppCustomizeMsg, msgId: String) -> String {
        return "your custom summary body name"
    }

    /// 自定义消息摘要正文
    override func getSummaryBodyContent(appCustomizeMsg: KIMAppCustomizeMsg, msgId: String) -> String {
        return "your custom summary body content"
    }
}

步骤4:定义并注册自定义消息管理器

自定义消息框架提供了一个管理器KIMCustomChatMessageManager,用于封装自定义消息的各类接口,定义如下:

Swift
class KIMCustomChatMessageManager<
    C: KIMMessageBaseCell,
    T: KIMAsyncTask,
    CacheType
> {
}

泛型类型说明:

泛型用途说明
C自定义消息 Cell 类需继承自KIMMessageBaseCell
T自定义消息关联的异步任务类需继承自KIMAsyncTask,如果此自定义消息不涉及异步任务 (如 HTTP 请求),此处直接填KIMAsyncTask即可;具体见 异步任务管理
CacheType自定义消息缓存的数据结构如果此自定义消息不需要使用缓存,此处直接填Any即可;具体见 缓存管理

每接入一种自定义消息,都需要提供一个继承自KIMCustomChatMessageManager的自定义 manager 子类:

Swift
class YourMessageManager: KIMCustomChatMessageManager<YourMessageCell, YourAsyncTask, YourCacheType> {
}

class YourMessageCell: KIMMessageBaseCell {
}

class YourAsyncTask: KIMAsyncTask {
}

class YourCacheType {
}

子类化 manager 后,需要在 App 启动时初始化 manager 并注册。KIMCustomChatMessageManager初始化函数如下:

Swift
public init(
    customizeType: String,
    dataSourceBuilderBlock: @escaping (KIMServicePlugin.BodyType) -> KIMChatMessageDataSourceBuilder,
    cellStateBuilderBlock: @escaping (KIMServicePlugin.BodyType) -> KIMChatMessageCellStateBuilder,
    operationMenuServiceBlock: ((KIMServicePlugin.BodyType) -> KIMChatMessageOperationMenuService)? = nil,
    quickReplyMenuServiceBlock: ((KIMServicePlugin.BodyType) -> KIMQuickReplyMenuService)? = nil
)

参数说明:

参数名类型说明
customizeTypeString此自定义消息的唯一类型标识
dataSourceBuilderBlock(KIMServicePlugin.BodyType) -> KIMChatMessageDataSourceBuilder在 block 中创建并返回KIMChatMessageDataSourceBuilder,用于将业务 ModelKIMMessage转换为 ViewModelKIMChatMessage;具体见 数据结构与转换流程自定义 DataSourceBuilder
cellStateBuilderBlock(KIMServicePlugin.BodyType) -> KIMChatMessageCellStateBuilder在 block 中创建并返回KIMChatMessageCellStateBuilder,用于将 ViewModelKIMChatMessage转换为 CellModelKIMMessageBuildInCellModel,具体见 数据结构与转换流程自定义 CellStateBuilder
operationMenuServiceBlock(KIMServicePlugin.BodyType) -> KIMChatMessageOperationMenuService在 block 中创建并返回KIMChatMessageOperationMenuService,用于 自定义消息长按菜单
quickReplyMenuServiceBlock(KIMServicePlugin.BodyType) -> KIMQuickReplyMenuService在 block 中创建并返回KIMQuickReplyMenuService,用于 自定义快捷回复

完成 manager 初始化后,调用register实例方法进行注册。注意:需在 App 启动的时候调用;仅调用一次即可。下面的例子在 App 启动的application(_:didFinishLaunchingWithOptions:)方法中调用:

Swift
@main
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    let manager = YourMessageManager(
        customizeType: "unique_message_customize_type",
        dataSourceBuilderBlock: { _ in YourChatMessageDataSourceBuilder() },
        cellStateBuilderBlock: { _ in YourChatMessageCellStateBuilder() },
        operationMenuServiceBlock: { _ in YourChatMessageOperationMenuService() }
    )
    manager.register()
  }
}

调用register方法完成 manager 注册后,可以通过default类属性,来获取注册的 manager 实例:

Swift
YourMessageManager.default

进而调用 manager 提供的各种方法:

Swift
YourMessageManager.default.someMethod()

注:manager 的定义涉及到了泛型,而 Objective-C 不支持泛型,因此 manager 的定义只能使用 Swift 来完成。关于如何在 Objective-C 代码中注册 Swift 定义的 manager,请见 Objective-C 接入方案

至此,我们便完成了接入自定义消息的所有必要步骤。

可选流程:

下方的文档为详细的 API 说明,可在接入的过程中按需查看。

API 说明

发送自定义消息

调用举例:

Swift
let msgContent = [
    city: "广州",
    longitude: "113.280637",
    latitude: "23.125178"
]

guard let jsonData = try? JSONEncoder().encode(msgContent),
    let jsonString = String(data: jsonData, encoding: .utf8) else {
    return
}

let customizeMsg = KIMAppCustomizeMsg(content: jsonString, customizeType: "check_weather", msgDesc: "查询广州未来一周天气")
KIMCore.shared.sendMessage(chatId: "908753402", appCustomizeMsg: customizeMsg)
Objective-c
NSDictionary *msgContent = @{
    @"city": @"广州",
    @"longitude": @"113.280637",
    @"latitude": @"23.125178"
};

NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:msgContent options:0 error:&error];
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];

if (error) {
    return;
}

KIMAppCustomizeMsg *customizeMsg = [[KIMAppCustomizeMsg alloc] initWithContent:jsonString customizeType:@"check_weather" msgDesc:@"查询广州未来一周天气"];
[[KIMCore shared] sendMessageWithChatId:@"908753402" appCustomizeMsg:customizeMsg notices:nil extra:nil replyMsgId:nil msgConfig:nil pushConfig:nil];

自定义消息长按菜单

步骤如下:

  • 自定义类,实现KIMChatMessageOperationMenuService协议

  • 在协议方法makeOperationMenuItems中:

    • 仅处理当前注册的自定义消息

    • 初始化KIMMessageMenuItem,并设置action属性来实现菜单项的点击处理

    • 系统默认提供 5 种内置菜单项:回复、撤回、转发、多选、收藏;如果需要集成这些内置菜单项,需调用KIMChatUI.shared.servicePlugin.getDefaultService获取系统默认的KIMChatMessageOperationMenuService实现,并调用其makeOperationMenuItems生成内置菜单项

    • 最终,以 array 的形式返回所有的KIMMessageMenuItem

  • 在manager 初始化时传入

示例如下:

Swift
class YourChatMessageOperationMenuService: KIMChatMessageOperationMenuService {
    let bodyType: KIMServicePlugin.BodyType
    weak var delegate: KIMChatMessageOperationMenuServiceDelegate?

    /// 获取系统默认的 KIMChatMessageOperationMenuService 实现
    private lazy var def: KIMChatMessageOperationMenuService? = {
        let service = KIMChatUI.shared.servicePlugin.getDefaultService(
            KIMChatMessageOperationMenuService.self,
            bodyType: bodyType
        )
        /// 这一步不能少
        service?.delegate = delegate
        return service
    }()

    init(bodyType: KIMServicePlugin.BodyType) {
      self.bodyType = bodyType
    }

    func makeOperationMenuItems(chatMessage: KIMMessageCellViewModel, view: UIView?, controller: KIMChatMessageViewController) -> [KIMMessageMenuItem]? {
        /// 仅对当前自定义消息类型生效,其他类型均返回 nil
        guard YourMessageManager.default.shouldProcessChatMessage(chatMessage) else {
            return nil
        }

        /// 生成系统内置的菜单项
        let systemItems = def?.makeOperationMenuItems(chatMessage: chatMessage, textView: textView, controller: controller) ?? []

        /// 自定义菜单项
        let customItem = KIMOperationMenuItem(icon: UIImage(named: "kim_chat_add"), title: "自定义1", identifier: "custom_op_1")
        customItem.action = { [weak self] in
            // 点击处理逻辑
            return true
        }

        return systemItems + [customItem]
    }
}
Objective-C
//
//  YourChatMessageOperationMenuService.h
//
#import <Foundation/Foundation.h>
@import KIMKit;

@interface YourChatMessageOperationMenuService: NSObject<KIMChatMessageOperationMenuService>

@property (nonatomic, weak) id<KIMChatMessageOperationMenuServiceDelegate> delegate;

- (instancetype)initWithBodyType:(enum KIMServiceBodyType)bodyType;

@end
Objective-c
//
//  YourChatMessageOperationMenuService.m
//
#import "YourChatMessageOperationMenuService.h"
@import KIMKit;
@import KIMCore;

@interface YourChatMessageOperationMenuService ()

@property (strong, nonatomic) id<KIMChatMessageOperationMenuService> def;
@property (nonatomic, assign) enum KIMServiceBodyType bodyType;

@end

@implementation YourChatMessageOperationMenuService

- (instancetype)initWithBodyType:(enum KIMServiceBodyType)bodyType {
    self = [super init];
    if (self) {
        _bodyType = bodyType;
    }
    return self;
}

- (NSArray<KIMOperationMenuItem *> * _Nullable)makeOperationMenuItemsWithChatMessage:(KIMChatMessage * _Nonnull)chatMessage textView:(UITextView * _Nullable)textView controller:(UIViewController * _Nullable)controller {
    // 仅对当前自定义消息类型生效,其他类型均返回 nil
    if (![chatMessage.coreMessage.type isEqualToString:@"kim-app-customize"] || ![chatMessage.coreMessage.appCustomizeMsg.customizeType isEqualToString:@"weather_2"]) {
        return nil;
    }

    // 生成系统内置的菜单项
    NSArray<KIMOperationMenuItem *> *items = [self.def makeOperationMenuItemsWithChatMessage:chatMessage textView:textView controller:controller] ?: @[];

    // 自定义菜单项
    KIMOperationMenuItem *weatherCustom1 = [[KIMOperationMenuItem alloc] initWithIcon:[UIImage imageNamed:@"kim_chat_add"] title:@"自定义1" identifier:[[KIMOperationMenuIdentifier alloc]  initWithRawValue:@"custom_weather_op_1"]];
    weatherCustom1.action = ^{
        // 点击处理逻辑
        return YES;
    };

    return [items arrayByAddingObjectsFromArray:@[weatherCustom1]];
}

// 获取系统默认的 KIMChatMessageOperationMenuService 实现
- (id<KIMChatMessageOperationMenuService>)def {
    if (_def == nil) {
        _def = [ObjcBridge getDefaultChatMessageOperationMenuServiceWithBodyType:self.bodyType];
        // 这一步不能少
        _def.delegate = _delegate;
    }
    return _def;
}

@end

注:由于KIMChatUI.shared.servicePlugin.getDefaultService的定义包含泛型,因此无法直接在 Objective-C 代码中调用。这里使用 Swift 类做了一个桥接:

Swift
@objcMembers
class ObjcBridge: NSObject {
    class func getDefaultChatMessageOperationMenuService(bodyType: KIMServicePlugin.BodyType) -> KIMChatMessageOperationMenuService? {
        KIMChatUI.shared.servicePlugin.getDefaultService(
            KIMChatMessageOperationMenuService.self,
            bodyType: bodyType
        )
    }
}

自定义收藏页菜单

消息收藏列表页、消息收藏详情页,均支持自定义消息的渲染。

在消息收藏列表页,长按自定义消息,会弹出操作菜单:

在自定义消息的收藏详情页,点击右上角 ... 按钮,也会弹出操作菜单:

上述页面中的“自定义 1”即为自定义操作项,自定义步骤如下:

  • 自定义类,实现KIMFavoriteChatMessageActionService协议

  • 实现协议方法makeFavoriteActionItems

    • 判断是否为应该处理的自定义消息

    • 初始化KIMFavoriteActionItem,并设置action属性来实现菜单项的点击处理

    • 最终,以 array 的形式返回所有的KIMFavoriteActionItem

  • 在manager 初始化时传入

示例如下:

Swift
class YourFavoriteChatMessageActionService: KIMFavoriteChatMessageActionService {
    func makeFavoriteActionItems(message: KIMMessage, controller: UIViewController) -> [KIMFavoriteActionItem]? {
        /// 仅对当前自定义消息类型生效,其他类型均返回空数组
        guard YourMessageManager.default.shouldProcessMessage(message) else {
            return nil
        }

        /// 自定义操作项
        let item1 = KIMFavoriteActionItem(title: "自定义1", identifier: "custom_op_1")
        item1.action = { [weak self] in
            /// 点击处理逻辑
            return true
        }

        return [item1]
    }
}

消息快捷回复菜单

SDK 已对自定义消息提供了默认的快捷回复实现,无需编写代码,即可支持上图中的快捷回复控件。

如果需要对快捷回复项进行定制,或者希望隐藏快捷回复,则需要进行自定义,步骤如下:

  • 自定义类,实现KIMQuickReplyMenuService协议

  • 在协议方法makeQuickReplyMenuItems中:

    • 判断是否为应该处理的自定义消息

    • 初始化KIMQuickReplyItem,并设置action属性来实现菜单项的点击处理

    • 系统默认提供 6 项 Emoji 快捷回复项,如果需要集成这些内置菜单项,需调用KIMChatUI.shared.servicePlugin.getDefaultService获取系统默认的KIMQuickReplyMenuService实现,并调用其makeQuickReplyMenuItems生成内置菜单项

    • 最终,以 array 的形式返回所有的KIMQuickReplyItem(最多返回 6 项)

  • 在manager 初始化时传入

示例如下:

Swift
class YourQuickReplyMenuService: KIMQuickReplyMenuService {
    let bodyType: KIMServicePlugin.BodyType
    var delegate: KIMKit.KIMQuickReplyMenuServiceDelegate?

    private lazy var def: KIMQuickReplyMenuService? = {
        let service = KIMChatUI.shared.servicePlugin.getDefaultService(
            KIMQuickReplyMenuService.self,
            bodyType: bodyType
        )
        /// 这一步不能少
        service?.delegate = delegate
        return service
    }()

    init(bodyType: KIMServicePlugin.BodyType) {
      self.bodyType = bodyType
    }

    func makeQuickReplyMenuItems(chatMessage: KIMKit.KIMChatMessage) -> [KIMKit.KIMQuickReplyItem]? {
        /// 仅对当前自定义消息类型生效,其他类型均返回 nil
        /// 这一步不能少
        guard YourMessageManager.default.shouldProcessChatMessage(chatMessage) else {
            return nil
        }

        /// 生成系统内置的菜单项
        let systemItems = def?.makeQuickReplyMenuItems(chatMessage: chatMessage) ?? []

        /// 自定义菜单项
        /// 不可用的 id:0、-1000、 1000100 ~ 1013500
        let customItem = KIMQuickReplyItem(id: 1, name: "custom_quick_reply_item_1", image: UIImage(named: "your_image"))
        customItem.action = {
            // 点击处理逻辑
            return true
        }

        // 取前 6 项
        return Array((systemItems + [customItem]).prefix(6))
    }

    func makeQuickReplyDataSource(chatMessage: KIMKit.KIMChatMessage) -> [KIMKit.KIMQuickReplyItem]? {
        nil
    }
}
Objective-C
//
//  YourQuickReplyMenuServiceObjc.h
//
#import <Foundation/Foundation.h>
@import KIMKit;

@interface YourQuickReplyMenuServiceObjc: NSObject<KIMQuickReplyMenuService>

@property (nonatomic, weak) id<KIMQuickReplyMenuServiceDelegate> delegate;

- (instancetype)initWithBodyType:(enum KIMServiceBodyType)bodyType;

@end
Objective-c
//
//  YourQuickReplyMenuServiceObjc.m
//
#import "YourQuickReplyMenuServiceObjc.h"
@import KIMKit;
@import KIMCore;

@interface YourQuickReplyMenuServiceObjc ()

@property (strong, nonatomic) id<KIMQuickReplyMenuService> def;
@property (nonatomic, assign) enum KIMServiceBodyType bodyType;

@end

@implementation YourQuickReplyMenuServiceObjc

- (instancetype)initWithBodyType:(enum KIMServiceBodyType)bodyType {
    self = [super init];
    if (self) {
        _bodyType = bodyType;
    }
    return self;
}

- (NSArray<KIMQuickReplyItem *> * _Nullable)makeQuickReplyMenuItemsWithChatMessage:(KIMChatMessage * _Nonnull)chatMessage {
    // 仅对当前自定义消息类型生效,其他类型均返回 nil
    if (![chatMessage.coreMessage.type isEqualToString:@"kim-app-customize"] || ![chatMessage.coreMessage.appCustomizeMsg.customizeType isEqualToString:@"weather_2"]) {
        return nil;
    }

    // 获取系统默认的菜单项
    NSArray<KIMQuickReplyItem *> *systemItems = [self.def makeQuickReplyMenuItemsWithChatMessage:chatMessage] ?: @[];

    // 自定义菜单项
    // 不可用的 id:0、-1000、 1000100 ~ 1013500
    KIMQuickReplyItem *customItem = [[KIMQuickReplyItem alloc] initWithId:1 name:@"custom_quick_reply_item_1" image:[UIImage imageNamed:@"kim_chat_add"]];
    customItem.action = ^{
        // 点击处理逻辑
        return YES;
    };

    NSArray<KIMQuickReplyItem *> *allItems = [@[customItem] arrayByAddingObjectsFromArray:[systemItems arrayByAddingObject:customItem]];
    NSArray<KIMQuickReplyItem *> *limitedItems = [allItems subarrayWithRange:NSMakeRange(0, MIN(6, allItems.count))];

    return limitedItems;
}

- (NSArray<KIMQuickReplyItem *> * _Nullable)makeQuickReplyDataSourceWithChatMessage:(KIMChatMessage * _Nonnull)chatMessage {
    return @[];
}

- (id<KIMQuickReplyMenuService>)def {
    if (_def == nil) {
        _def = [ObjcBridge getDefaultQuickReplyMenuServiceWithBodyType:self.bodyType];
        _def.delegate = _delegate;
    }
    return _def;
}

@end

注:由于KIMChatUI.shared.servicePlugin.getDefaultService的定义包含泛型,因此无法直接在 Objective-C 代码中调用。这里使用 Swift 类做了一个桥接:

Swift
@objcMembers
class ObjcBridge: NSObject {
    class func getDefaultQuickReplyMenuService(bodyType: KIMServicePlugin.BodyType) -> KIMQuickReplyMenuService? {
        KIMChatUI.shared.servicePlugin.getDefaultService(
            KIMQuickReplyMenuService.self,
            bodyType: bodyType
        )
    }
}

消息气泡生命周期

为提供精细化的消息管理能力,协作中台 SDK 针对自定义消息,定义了 6 个生命周期:

生命周期说明使用建议
onFetchData数据预请求发起异步请求
onBindData预请求完毕更新消息相关数据结构体,刷新 cell
onCreateView准备消息 View创建自定义消息 cell,并完成 cell 配置
onAppear消息即将显示按需
onDisappear消息结束显示资源清理
onRecycled取消数据预请求取消异步请求

注意:以上生命周期,SDK 均提供默认的标准实现,在不做任何重载实现的情况下,框架可提供默认功能。

重载案例如下:

Swift
class YourMessageManager: KIMCustomChatMessageManager<YourMessageCell, YourAsyncTask, YourCacheType> {
    override func onRecycled(messages: [KIMChatMessage], viewModel: KIMChatMessageViewModel) {
        super.onRecycled(messages: messages, viewModel: viewModel)

        // 资源清理
    }
}

其他

数据结构与转换流程

自定义消息会涉及到 3 种数据结构(KIMMessageKIMChatMessageKIMMessageCellState),以及 2 种用于转换这些数据结构的类(KIMChatMessageDataSourceBuilderKIMChatMessageCellStateBuilder)。理解这些数据结构,以及数据转换的过程,是接入自定义消息必不可少的环节。

下图为数据结构与转换流程的示意图:

KIMMessage为中台业务模型,是原始的消息结构。其中有一个属性appCustomizeMsg,即为自定义消息:

Swift
public class KIMMessage: NSObject, Codable {
    /// 自定义消息
    @objc public var appCustomizeMsg: KIMAppCustomizeMsg?
}

KIMAppCustomizeMsg结构如下:

Swift
@objc public class KIMAppCustomizeMsg: NSObject {
    /// 自定消息内容
    @objc public var content: String = String()
    /// 消息子类型,由用户自己定义
    @objc public var customizeType: String = String()
    /// 消息描述文本,最大长度 5000 字符,该描述信息用于消息关键词检索及消息摘要显示
    @objc public var msgDesc: String = String()
}

其中属性content,即为发送方发送的自定义消息内容,收发双方需提前约定好此字段的数据结构。

KIMChatMessage为 ViewModel 列表数据源使用的消息类型,其中有一个属性customData,即可用于存储自定义数据:

Swift
open class KIMChatMessage: NSObject {
    /// 用户自定义数据
    @objc public var customData: Any?
}

KIMChatMessageDataSourceBuilder则用于将KIMMessage转化为KIMChatMessage。需自定义类实现此协议,从KIMMessageappCustomizeMsg解析数据,按需进行转换,并存储到KIMChatMessagecustomData中。

KIMMessageCellState为消息单元格的视图模型,其中有一个属性messageContent用于存储消息内容:

Swift
open class KIMMessageCellState: NSObject {
    /// 消息内容,包括文本、图片、音频等视图数据。
    @objc public let messageContent: KIMMessageCellContent
}

KIMMessageCellContent结构如下,包括一个用于存储自定义内容的customContent属性:

Swift
open class KIMMessageCellContent: NSObject {
     /// 单元格自定义内容
     public var customContent: Any?
}

KIMChatMessageCellStateBuilder则用于将KIMChatMessage转化为KIMMessageCellContent。需自定义类实现此协议,从KIMMessageCellContentcustomContent中读取数据,按需进行转换,并存储到KIMChatMessagemessageContent.customContent中。

自定义 DataSourceBuilder

基本流程:

  • 自定义结构实现KIMChatMessageDataSourceBuilder协议

  • 定义 lazy 属性,使用KIMChatUI.shared.servicePlugin.getDefaultService获取获取系统的默认实现

  • 实现buildDataSource协议方法

    • 仅处理当前注册的自定义消息

    • 调用默认实现,生成KIMChatMessage

    • 根据收发双方约定的数据结构,从appCustomizeMsgcontent属性中解析消息

    • 构建自定义内容,并存储到KIMChatMessagecustomData属性中

    • 返回KIMChatMessage

示例代码:

Swift
class YourChatMessageDataSourceBuilder: KIMChatMessageDataSourceBuilder {
    public let bodyType: KIMServicePlugin.BodyType

    init(bodyType: KIMServicePlugin.BodyType) {
        self.bodyType = bodyType
    }

    /// 第 1 步:获取系统默认的 DataSourceBuilder
    private lazy var def: KIMChatMessageDataSourceBuilder? = {
        KIMChatUI.shared.servicePlugin.getDefaultService(
            KIMChatMessageDataSourceBuilder.self,
            bodyType: self.bodyType
        )
    }()

    func buildDataSource(chatModel: KIMChatModel, coreMessage: KIMMessage, prevChatMessage: KIMChatMessage?, viewModel: KIMChatMessageViewModel) -> KIMChatMessage? {
        /// 第 2 步:调用默认实现,生成 KIMChatMessage
        guard let chatMessage = def?.buildDataSource(chatModel: chatModel, coreMessage: coreMessage, prevChatMessage: prevChatMessage, viewModel: viewModel) else {
            return nil
        }

        /// 第 3 步:仅处理当前注册的自定义消息
        guard WeatherMessageManager.default.shouldProcessMessage(coreMessage) else {
            return chatMessage
        }

        /// 第 4 步:从 appCustomizeMsg 的 content 属性中解析消息
        /// 此处假设 content 的数据结构为 YourAppCustomizeMsgContent 的 json 字符串
        guard let content = coreMessage.appCustomizeMsg?.content,
            let contentObject = try? JSONDecoder().decode(YourAppCustomizeMsgContent.self, from: Data(content.utf8)) else {
            return chatMessage
        }

        /// 第 5 步:构建自定义内容,按需进行转换,并存储到 KIMChatMessage 的 customData 中
        /// 此处 yourCreateCustomDataFunction 为示例函数
        let customData = yourCreateCustomDataFunction(contentObject)
        chatMessage.customData = customData
        chatMessage.checkStatus = .none /// 如果需要启用当前自定义消息的多选能力,注释本行代码

        /// 第 6 步:返回 KIMChatMessage
        return chatMessage
    }
}
Objective-c
//
//  YourChatMessageDataSourceBuilder.h
//
#import <Foundation/Foundation.h>
@import KIMKit;

@interface YourChatMessageDataSourceBuilder: NSObject<KIMChatMessageDataSourceBuilder>

- (instancetype)initWithBodyType:(enum KIMServiceBodyType)bodyType;

@end
Objective-c
//
//  YourChatMessageDataSourceBuilder.m
//
#import "YourChatMessageDataSourceBuilder.h"
@import KIMKit;
@import KIMCore;

@interface YourChatMessageDataSourceBuilder ()

@property (strong, nonatomic) id<KIMChatMessageDataSourceBuilder> def;
@property (nonatomic, assign) enum KIMServiceBodyType bodyType;

@end

@implementation YourChatMessageDataSourceBuilder

- (instancetype)initWithBodyType:(enum KIMServiceBodyType)bodyType {
    self = [super init];
    if (self) {
        _bodyType = bodyType;
    }
    return self;
}

- (KIMChatMessage *)buildDataSourceWithChatModel:(KIMChatModel *)chatModel coreMessage:(KIMMessage *)coreMessage prevChatMessage:(KIMChatMessage *)prevChatMessage viewModel:(KIMChatMessageViewModel *)viewModel {
    // 第 2 步:调用默认实现,生成 KIMChatMessage
    KIMChatMessage *chatMessage = [self.def buildDataSourceWithChatModel:chatModel coreMessage:coreMessage prevChatMessage:prevChatMessage viewModel:viewModel];
    if (chatMessage == nil) {
        return nil;
    }

    // 第 3 步:仅处理当前注册的自定义消息
    if (![coreMessage.type isEqualToString:@"kim-app-customize"] || ![coreMessage.appCustomizeMsg.customizeType isEqualToString:@"your_customize_type"]) {
        return chatMessage;
    }

    // 第 4 步:从 appCustomizeMsg 的 content 属性中解析消息
    // 此处假设 content 的数据结构为 json 字符串
    NSString *content = coreMessage.appCustomizeMsg.content;
    if (content == nil) {
        return chatMessage;
    }
    NSData *jsonData = [content dataUsingEncoding:NSUTF8StringEncoding];
    NSError *error;
    NSDictionary *contentObject = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&error];
    if (error != nil) {
        return chatMessage;
    }

    // 第 5 步:构建自定义内容,按需进行转换,并存储到 KIMChatMessage 的 customData 中
    // 此处 yourCreateCustomDataFunction 为示例函数
    id customData = [self yourCreateCustomDataFunction:contentObject];
    chatMessage.customData = customData;
    chatMessage.checkStatus = KIMMessageCheckStatusNone;    /// 如果需要启用当前自定义消息的多选能力,注释本行代码

    return chatMessage;
}

// 第 1 步:获取系统默认的 DataSourceBuilder
- (id<KIMChatMessageDataSourceBuilder>)def {
    if (_def == nil) {
        _def = [ObjcBridge getDefaultChatMessageDataSourceBuilderWithBodyType:self.bodyType];
    }
    return _def;
}
@end

注:由于KIMChatUI.shared.servicePlugin.getDefaultService的定义包含泛型,因此无法直接在 Objective-C 代码中调用。这里使用 Swift 类做了一个桥接:

Swift
@objcMembers
class ObjcBridge: NSObject {
    class func getDefaultChatMessageDataSourceBuilder(bodyType: KIMServicePlugin.BodyType) -> KIMChatMessageDataSourceBuilder? {
        KIMChatUI.shared.servicePlugin.getDefaultService(
            KIMChatMessageDataSourceBuilder.self,
            bodyType: bodyType
        )
    }
}

自定义 CellStateBuilder

基本流程:

  • 自定义结构实现KIMChatMessageCellStateBuilder协议

  • 定义 lazy 属性,使用KIMChatUI.shared.servicePlugin.getDefaultService获取获取系统的默认实现

  • 实现buildCellState协议方法

    • 仅处理当前注册的自定义消息

    • 调用默认实现,生成KIMMessageCellState

    • 读取KIMChatMessagecustomData,按需进行转换,并将转换结果用于构建KIMMessageCellState

    • 返回KIMMessageCellState

示例代码:

Swift
class YourChatMessageCellStateBuilder: KIMChatMessageCellStateBuilder {
    public let bodyType: KIMServicePlugin.BodyType

    init(bodyType: KIMServicePlugin.BodyType) {
        self.bodyType = bodyType
    }

    /// 第 1 步:获取系统默认的 CellStateBuilder
    private lazy var defaultBuilder: KIMChatMessageCellStateBuilder? = {
        KIMChatUI.shared.servicePlugin.getDefaultService(
            KIMChatMessageCellStateBuilder.self,
            bodyType: self.bodyType
        )
    }()

    func buildCellState(chatMessage: KIMChatMessage, viewModel: KIMChatMessageViewModel) -> KIMMessageCellState? {
        /// 第 2 步:调用默认实现,生成 KIMMessageCellState
        guard let cellState = defaultBuilder?.buildCellState(chatMessage: chatMessage, viewModel: viewModel) else {
            return nil
        }

        /// 第 3 步:仅处理当前注册的自定义消息
        guard WeatherMessageManager.default.shouldProcessChatMessage(chatMessage) else {
            return chatMessage
        }

        /// 第 4 步:读取 KIMChatMessage 的 customData
        /// 此处 YourChatMessageCustomData 为 chatMessage.customData 的数据结构
        guard let content = chatMessage.customData as? YourChatMessageCustomData else {
            return nil
        }

        // 第 5 步:构建 KIMMessageCellState 的自定义内容
        // 此处的 yourCreateCustomContentFunction 为示例函数
        let costomContent = yourCreateCustomContentFunction(content)

        // 第 6 步:构建 KIMMessageCellState 并返回
        let messageContent = KIMMessageCellContent(cellContentType: .custom, customContent: costomContent)
        return KIMMessageCellState(baseInfo: cellState.baseInfo, messageContent: messageContent, refMsgContent: cellState.refMsgContent, layoutOptions: cellState.layoutOptions)
    }
}
Objective-C
//
//  YourChatMessageCellStateBuilder.h
//
#import <Foundation/Foundation.h>
@import KIMKit;

@interface YourChatMessageCellStateBuilderObjc: NSObject<KIMChatMessageCellStateBuilder>

- (instancetype)initWithBodyType:(enum KIMServiceBodyType)bodyType;

@end
Objective-c
//
//  YourChatMessageCellStateBuilder.m
//
#import "YourChatMessageCellStateBuilder.h"
@import KIMKit;
@import KIMCore;

@interface YourChatMessageCellStateBuilder ()

@property (strong, nonatomic) id<KIMChatMessageCellStateBuilder> def;
@property (nonatomic, assign) enum KIMServiceBodyType bodyType;

@end

@implementation YourChatMessageCellStateBuilder

- (instancetype)initWithBodyType:(enum KIMServiceBodyType)bodyType {
    self = [super init];
    if (self) {
        _bodyType = bodyType;
    }
    return self;
}

- (KIMMessageCellState *)buildCellStateWithChatMessage:(KIMChatMessage *)chatMessage viewModel:(KIMChatMessageViewModel *)viewModel {
    // 第 2 步:调用默认实现,生成 KIMMessageCellState
    KIMMessageCellState *cellState = [self.def buildCellStateWithChatMessage:chatMessage viewModel:viewModel];
    if (cellState == nil) {
        return nil;
    }

    // 第 3 步:仅处理当前注册的自定义消息
    if (![chatMessage.coreMessage.type isEqualToString:@"kim-app-customize"] || ![chatMessage.coreMessage.appCustomizeMsg.customizeType isEqualToString:@"weather_2"]) {
        return cellState;
    }

    /// 第 4 步:读取 KIMChatMessage 的 customData
    /// 此处 YourChatMessageCustomData 为 chatMessage.customData 的数据结构
    if (chatMessage.customData == nil || ![chatMessage.customData isKindOfClass:[YourChatMessageCustomData class]]) {
        return cellState;
    }

    WeatherViewModelCustomContent *customData = chatMessage.customData;

    // 第 5 步:构建 KIMMessageCellState 的自定义内容
    // 此处的 yourCreateCustomContentFunction 为示例函数
    id costomContent = yourCreateCustomContentFunction(customData)

    // 第 6 步:构建 KIMMessageCellState 并返回
    KIMMessageCellContent *messageContent = [[KIMMessageCellContent alloc] initWithCellContentType:KIMMessageCellContentTypeCustom noticeContent:nil attributedTextContent:nil pictureContent:nil videoContent:nil voiceContent:nil fileContent:nil mergedMsgsCardContent:nil recallContent:nil cardContent:nil customContent:customContent];
    return [[KIMMessageCellState alloc] initWithBaseInfo:cellState.baseInfo messageContent:messageContent refMsgContent:cellState.refMsgContent layoutOptions:cellState.layoutOptions];
}

// 第 1 步:获取系统默认的 CellStateBuilder
- (id<KIMChatMessageCellStateBuilder>)def {
    if (_def == nil) {
        _def = [ObjcBridge getDefaultChatMessageCellStateBuilderWithBodyType:self.bodyType];
    }
    return _def;
}

@end

注:由于KIMChatUI.shared.servicePlugin.getDefaultService的定义包含泛型,因此无法直接在 Objective-C 代码中调用。这里使用 Swift 类做了一个桥接:

Swift
@objcMembers
class ObjcBridge: NSObject {
    class func getDefaultChatMessageCellStateBuilder(bodyType: KIMServicePlugin.BodyType) -> KIMChatMessageCellStateBuilder? {
        KIMChatUI.shared.servicePlugin.getDefaultService(
            KIMChatMessageCellStateBuilder.self,
            bodyType: bodyType
        )
    }
}

异步任务管理

自定义消息可能会涉及到异步操作,比如:发送的消息自定义内容中,仅包含资源 id,接收端需要通过此 id 发起 HTTP 请求,才能获得完整的资源信息并展示。上述过程的 HTTP 请求就是一种异步请求,SDK 针对异步请求提供了接入方案:

自定义 Task

在自定义 manager时,需要提供继承自KIMAsyncTask的自定义类,此类即用于管理消息涉及到的异步任务(注:如果不涉及到异步请求,直接提供KIMAsyncTask即可)。

自定义 Task 需要重载父类的 3 个方法:

方法含义使用说明
main()任务启动在其中发起异步请求
finish(Error?)标记任务完成main 中发起的异步请求结束后,需调用此方法标记任务已完成;如果请求失败,需传入 Error
cancel()任务取消在其中取消异步操作,并调用 super.cancel(),以释放资源

使用案例:

Swift
class YourAsyncTask: KIMAsyncTask {
    private let resourceId: String
    private var task: URLSessionDataTask?
    /// 存储请求结果
    private var result: String?

    init(resourceId: String) {
        self.resourceId = resourceId
        super.init()
    }

    override func main() {
        guard let url = URL(string: "https://api.yourserver.com/resource/\(resourceId)") else {
            return
        }

        // 重载 main,发起异步请求
        let request = URLRequest(url: url)
        request.httpMethod = "GET"
        task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            if let error {
                // 如果出现错误,调用 finish 传入方法
                self.finish(error)
                return
            }

            if let data = data {
                self.result = String(data: data, encoding: .utf8)
            }
            // 如果请求成功,同样调用 finish
            self.finish()
        }
        task?.resume()
    }

    override func cancel() {
        // 重载 cancel 方法,取消任务,释放资源
        task?.cancel()
        task = nil
        super.cancel()
    }
}

注:由于 Swift 编译器的限制,目前无法在 Objective-C 中继承 Swift 类,因此 KIMAsyncTask 子类只能使用 Swift 来编写

KIMAsyncTask公共属性如下:

属性类型说明
errorError?异步任务的错误记录。此属性为只读属性,只能由 finish(Error?) 写入
timeoutTimeInterval?异步任务超时时间。默认 10 秒,可自行设置,最大可设置为 10 秒
stateKIMAsyncTaskState异步任务当前状态

KIMAsyncTaskState取值如下:

Swift
enum KIMAsyncTaskState {
    /// 等待发起
    case pending
    /// 正在执行
    case executing
    /// 执行结束
    case finished
    /// 取消
    case cancelled
}

状态之间的转换流程图如下:

使用 Task

定义好KIMAsyncTask后,需重载KIMCustomChatMessageManager的两个方法,以发起 task:

  • createTask:此方法会在需要发起异步任务的时候,由 SDK 自动调用

  • updateMessageWhenFetched:此方法会在异步任务成功后,由 SDK 自动调用

Swift
class YourMessageManager: KIMCustomChatMessageManager<YourMessageCell, YourAsyncTask, YourCacheType> {
    override func createTask(message: KIMChatMessage) -> WeatherTask? {
        guard let content = message.customData as? YourKIMChatMessageCustomContent else {
            return nil
        }

        /// 根据 customMessageContent 创建 task 实例,并返回
        let task = YourAsyncTask(
            resourceId: content.resourceId
        )
        return task
    }

    override func updateMessageWhenFetched(message: KIMChatMessage, task: YourAsyncTask) {
        guard let customData = message.customData as? YourKIMChatMessageCustomContent else {
            return
        }
        /// 从 task 中获取请求结果数据,存储到 KIMChatMessage 的 customData 中
        customData.skycon = task.skycon
        customData.state = (task.skycon != nil) ? .succeed : .failed
        message.customData = customData
    }
}

manager 提供了如下的方法用于任务管理:

Swift
/// 启动异步任务
/// SDK 会在合适的时机自动启动任务,使用方一般无需主动调用
func addTask(message: KIMChatMessage, task: T)

/// 获取 KIMChatMessage 关联的异步任务
func getTask(message: KIMChatMessage) -> T?

/// 取消异步任务
/// SDK 会在合适的时机自动取消任务,使用方一般无需主动调用
func cancelTask(message: KIMChatMessage)

/// 取消所有异步任务
/// SDK 会在会话页面销毁时,自动调用此方法,使用方一般无需主动调用
func cancelAllTasks()

缓存管理

在 自定义 task 时,在YourAsyncTask中定义了存储请求结果的变量result:

Swift
class YourAsyncTask: KIMAsyncTask {
    /// 存储请求结果
    private var result: String?
}

这其实就是一种数据缓存。不过,在真实业务需求中,除了和异步请求相关联的数据需要缓存,可能还有其他的数据缓存需求。manager 提供了如下的缓存 API:

API使用说明
getCache(cid: String) -> CacheType?根据消息 cid 获取缓存
saveCache(cid: String, data: CacheType?)写缓存
removeCache(cid: String)清楚缓存数据
removeAllCache()清除全部缓存数据

注:

  • 上述CacheType为自定义 manager时传入的

  • 如果暂时不需要缓存能力,可以直接传Any

  • 此缓存为内存缓存,框架暂未提供磁盘缓存能力

调用举例:

Swift
/// 写缓存
YourManager.shared.saveCache(cid: message.cid, data: yourData)

/// 读缓存
let data = YourManager.shared.getCache(message.cid)

/// 清除缓存
YourManager.shared.removeCache(message.cid)

/// 清除所有缓存
YourManager.shared.removeAllCache()

Objective-C 工程接入方案

自定义消息框架是使用 Swift 语言编写的,如果需要在 Objective-C 工程中接入此框架,请参考如下的兼容性说明:

数据类型涉及操作语言兼容性原因
KIMCustomChatMessageManager子类化仅支持使用 Swift 定义子类Objective-C 类无法继承 Swift 类
KIMMessageBaseCell子类化Swift 或 Objective-C
KIMChatMessageDataSourceBuilder实现协议Swift 或 Objective-C
KIMChatMessageCellStateBuilder实现协议Swift 或 Objective-C
KIMChatMessageOperationMenuService实现协议Swift 或 Objective-C
KIMQuickReplyMenuService实现协议Swift 或 Objective-C
KIMAsyncTask子类化仅支持使用 Swift 定义子类Objective-C 类无法继承 Swift 类

即仅KIMCustomChatMessageManagerKIMAsyncTask的子类化需要使用 Swift 完成,其他都可以使用 Objective-C 来编写。

同时,由于KIMCustomChatMessageManager的定义使用到了泛型,因此无法直接在 Objective-C 代码中使用其子类,可以采取如下的方式:

  • 使用 Swift 子类化KIMCustomChatMessageManager

  • 使用 Swift 定义一个用于桥接的类,继承自NSObject,并标记为@objc

  • 在此类中封装 manager 的各种接口,并使用@objc对需要暴露的 API 进行标记

  • 最后,在 Objective-C 代码中使用该类

举例:

Swift
@objc class YourMessageManagerForObjc: NSObject {
    /// 封装 Swift 方法,用于对 Objc 代码暴露接口

    @objc class func register() {
        let manager = YourMessageManager(
            customizeType: "unique_message_customize_type",
            dataSourceBuilderBlock: { _ in YourChatMessageDataSourceBuilder() },
            cellStateBuilderBlock: { _ in YourChatMessageCellStateBuilder() },
            operationMenuServiceBlock: { _ in YourChatMessageOperationMenuService() }
        )
        manager.register()
    }

    @objc class func getCache(cid: String) -> Any? {
        YourMessageManager.default.getCache(cid: cid)
    }
}

然后在 Objective-C 中使用上述类:

Objective-C
#import "Your-Project-Swift.h"

+ (void)test {
    [YourMessageManagerForObjc register];

    id result = [YourMessageManagerForObjc getCacheWithCid:@"23123521"];
}

工程结构参考:

Plaintext
- YourMessageManager.swift
- YourMessageManagerForObjc.h
- YourMessageManagerForObjc.m
- YourMessageCell.h
- YourMessageCell.m
- YourChatMessageDataSourceBuilder.h
- YourChatMessageDataSourceBuilder.m
- YourChatMessageCellStateBuilder.h
- YourChatMessageCellStateBuilder.m
// 以下为按需实现的结构
- YourAsyncTask.swift
- YourChatMessageOperationMenuService.h
- YourChatMessageOperationMenuService.m
- YourQuickReplyMenuService.h
- YourQuickReplyMenuService.m