添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
iOS
快速开始
IM Demo 体验
圈组 Demo 体验
IM UIKit(V10)
集成 IM UIKit
组件导入
初始化
界面跳转
AI 数字人
会话列表相关
消息相关
集成会话消息界面
实现地理位置消息功能
实现音视频通话功能
通讯录相关
全局配置
圈组 UIKit
集成圈组 UIKit
组件导入
初始化与登录
集成圈组界面
IM UIKit(V9)
集成 IM UIKit
组件导入
初始化
界面跳转
用户相关
会话列表相关
消息相关
集成会话消息界面
实现地理位置消息功能
实现音视频通话功能
通讯录相关

自定义会话消息 UI

更新时间: 2024/07/26 13:39:23

IM UIKit 的会话模块( NEChatUIKit )提供会话界面 UI 的自定义配置项,助您快速实现该界面的 UI 个性化定制。

会话消息模块( NEChatUIKit )提供以下两种方式对会话消息的 UI 进行个性化定制。

  • 通过接口配置的方式自定义 UI。
  • 通过源码修改的方式自定义 UI。
  • 如果接口中的配置项满足您的界面个性化需求,请优先使用接口配置方式。

    自定义功能概述

    UI 元素自定义

    会话消息界面可自定义的 UI 元素包括但不限于下图中的标注项。

    NEKitChatConfig 是整个会话消息的配置文件,其中 UI 实例化对象类型为 ChatUIConfig

    您可通过 ChatUIConfig 中的个性化配置项修改会话界面 UI 元素,如消息体、输入按钮和消息长按菜单等,具体如下:

    messageProperties MessageProperties 消息页面的UI个性化定制,包括常用的字体颜色、大小、是否可见、背景色等属性等,具体见下文的 MessageProperties 个性化配置项 customController (ChatViewController) -> Void 消息页面中,获取页面的整个视图。可用于在页面中添加、移除或者修改布局元素。 chatInputBar (inout [UIButton]) -> Void 消息页面中,文本输入框下方 tab 按钮定制。 chatInputMenu (inout [NEMoreItemModel]) -> Void 消息页面中,输入框按钮定制。可用于修改输入框按钮以及更多中按钮的增加、修改和删除。 chatPopMenu (inout [OperationItem], MessageContentModel?) -> Void 消息页面中,长按消息体弹出菜单定制。用于对长按菜单的增加、删除和修改。 popMenuClick (OperationItem) -> Void 消息页面中,长按消息体弹出菜单定制。用于对长按菜单点击事件的修改,可修改默认实现和自定义菜单的点击。 fileSizeLimit Double 消息页面中,发送文件大小限制(单位:MB),默认200。 avatarType NEChatAvatarType 头像类型,NEChatAvatarType.rectangle = 1 代表矩形,NEChatAvatarType.cycle 代表圆形 avatarCornerRadius CGFloat 头像的圆角大小,仅在avatarType = NEChatAvatarType.rectangle 时生效 timeTextSize CGFloat 消息列表中,时间字体大小 timeTextColor UIColor 消息列表中,时间字体颜色 signalBgColor UIColor 被标记消息的背景色 showP2pMessageStatus 单聊中是否展示已读未读状态 showTeamMessageStatus 群聊中是否展示已读未读状态 showTeamMessageNick 群聊中是否展示好友昵称 showTitleBar 会话界面是否展示标题栏,具体自定义示例见下文的 隐藏界面标题栏 showTitleBarRightIcon 是否展示标题栏右侧图标按钮 titleBarRightRes UIImage 设置标题栏右侧图标按钮展示图标,具体自定义示例见下文的 修改界面标题右侧图标 titleBarRightClick () -> Void 标题栏右侧图标的点击事件 chatViewBackground UIColor 设置会话界面背景色,具体自定义示例见下文的 修改会话界面背景色

    界面布局自定义

    除了界面的 UI 元素,您也可对界面的布局进行自定义。在 NEChatUIKit 模块中,所有的 Cell 都继承于 NEChatBaseCell ,会话消息界面分成左右会话两部分。以文本消息为例,整个会话消息界面分为 ChatTextLeftCell ChatTextRightCell ,其父类分别为 ChatBaseLeftCell ChatBaseRightCell

    基础信息(如背景颜色,头像或者昵称)会在上层的 BaseCell 中赋值,需要单独处理的逻辑需要在各自对应的业务 Cell 中完成。

    自V9.6.1,IM UIKit 按照消息类型合并界面左右两边的消息体,便于后期维护。

    每种类型的消息体中都包含左右两边消息展示所需要的所有元素,并根据消息方向进行了布局,在展示前可以根据消息方向控制元素的显隐以及完成元素对象的赋值。

    以基础版皮肤中的文本消息为例,整个会话消息界面合并为 ChatMessageTextCell , 其继承链路为: ChatMessageTextCell -> NormalChatMessageBaseCell -> NEBaseChatMessageCell -> NEChatBaseCell

    bodyTopView UIView 消息列表界面上方的小块视图,可在子类直接获取视图进行样式修改。用于在消息详情和顶部标题视图中间增加新的 UI 元素,具体自定义示例请参见下文的 会话消息标题栏下方扩展视图 bodyView UIView 消息列表的内容视图,可在子类直接获取视图进行样式修改。布局中包含 brokenNetworkView 错误提示视图和 contentView 列表视图。 brokenNetworkView NEBrokenNetworkView 聊天界面错误提示视图,可在子类直接获取视图进行样式修改 contentView UIView 消息列表的列表视图,可在子类直接获取视图进行样式修改。布局中包含 tableView 消息列表和 emptyView 空数据提示视图。 tableView UITableView 消息详情列表,可在子类直接获取视图进行样式修改 emptyView NEEmptyDataView 聊天界面空数据提示视图,可在子类直接获取视图进行样式修改 bottomView UIView 消息列表底部视图,可在子类直接获取视图进行样式修改,布局中包含 bodyBottomView 扩展视图和 chatInputView 输入框视图 bodyBottomView UIView 输入框视图上方的小块视图,可在子类直接获取视图进行样式修改,用于在消息详情和底部输入框中间增加新的 UI 元素,具体自定义示例请参见下文的 输入框上方区域扩展视图 chatInputView UIView 聊天界面的底部输入框视图,可在子类中进行样式修改。具体自定义示例请参见下文的 自定义输入框左右间距
    chatInputView.stackView 为输入框底部工具栏视图,可添加或删除按钮。具体自定义示例请参见下文的 自定义底部工具条 自定义 “更多” 功能页

    通过配置项自定义 UI 示例

    会话消息的个性化 UI 样式需要在会话消息界面加载之前配置,例如您可以在 IMKitEngine setupCoreKitIM 方法中设置个性化定制属性。

    修改界面标题栏右侧图标

    Swift
    NEKitChatConfig.shared.ui.messageProperties.titleBarRightRes = UIImage(named: "image name")
    
    Objective-C
    NEKitChatConfig.shared.ui.messageProperties.titleBarRightRes = [UIImage imageNamed:@"image name"];
    
    Swift
    NEKitChatConfig.shared.ui.messageProperties.chatViewBackground = UIColor.red
    
    Objective-C
    NEKitChatConfig.shared.ui.messageProperties.chatViewBackground = [UIColor redColor];
    
  • 示例代码
    Swift
    //修改自己消息体背景色
    NEKitChatConfig.shared.ui.messageProperties.selfMessageBg = UIColor.green
    //修改对方消息体背景色
    NEKitChatConfig.shared.ui.messageProperties.receiveMessageBg = UIColor.gray
    NEKitChatConfig.shared.ui.messageProperties.rightBubbleBg = nil // 隐藏气泡
    
    Objective-C
    //修改自己消息体背景色
    NEKitChatConfig.shared.ui.messageProperties.selfMessageBg = [UIColor blueColor];
    //修改对方消息体背景色
    NEKitChatConfig.shared.ui.messageProperties.receiveMessageBg = [UIColor blueColor];
    NEKitChatConfig.shared.ui.messageProperties.rightBubbleBg = nil; // 隐藏气泡
    

    ChatViewController为会话消息的基类,内部封装了 UITableView

    自V9.6.1,IM UIKit 按照消息类型合并界面左右两边的消息体,便于后期维护。 所有类型的消息都以 Cell 形式展示,需要在 cellRegisterDic 注册列表中声明不同消息体对应的 Cell 类,用于 UITableView 的 Cell 注册。利用面向对象语言的多态性,不同类型的 Cell 重写了父类的 setModel 方法,且每种类型的 Cell 都有与之对应的数据模型。

    对应关系如下:

    基础版 UI Cell

    针对不同的消息类型,使用对应的数据模型进行解析。

    swiftpublic class ChatMessageHelper: NSObject {
        public static func modelFromMessage(message: NIMMessage) -> MessageModel {
            var model: MessageModel
            switch message.messageType {
            case .video:
                model = MessageVideoModel(message: message)
            case .text:
                model = MessageTextModel(message: message)
            case .image:
                model = MessageImageModel(message: message)
            case .audio:
                model =
    
    
    
    
        
     MessageAudioModel(message: message)
            default:
                // 未识别的消息类型,默认为文本消息类型,text为未知消息体
                message.text = chatLocalizable("msg_unknown")
                model = MessageTextModel(message: message)
            return model
    

    ChatViewController 的 Cell 展示数据源方法中进行赋值。

    open func tableView(_ tableView: UITableView,
                        cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let model = viewmodel.messages[indexPath.row]
        var reuseId = ""
        if model.replyedModel?.isReplay == true,
        model.isRevoked == false {
        reuseId = "\(MessageType.reply.rawValue)"
        } else {
        let key = "\(model.type.rawValue)"
        if model.type == .custom, let object = model.message?.messageObject as? NIMCustomObject, let custom = object.attachment as? NECustomAttachmentProtocol {
            if registerCellDic["\(custom.customType)"] != nil {
            reuseId = "\(custom.customType)"
            } else {
            reuseId = "\(NEBaseChatMessageCell.self)"
        } else if model.type == .time || model.type == .notification || model.type == .tip {
            reuseId = "\(MessageType.time.rawValue)"
        } else if registerCellDic[key] != nil {
            reuseId = key
        } else {
            reuseId = "\(NEBaseChatMessageCell.self)"
        let cell = tableView.dequeueReusableCell(withIdentifier: reuseId, for: indexPath)
        if let c = cell as? NEBaseChatMessageTipCell {
        if let m = model as? MessageTipsModel {
            m.resetNotiText()
            c.setModel(m)
        return c
        else {
        return NEBaseChatMessageCell()
    

    P2PChatViewControllerGroupChatViewController 均继承于 ChatViewController

    这里以 P2PChatViewController 为例,具体如下:

    swift@objcMembers
    open class P2PChatViewController: ChatViewController {
        open override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
        override open func getSessionInfo(session: NIMSession) {
        let user = viewmodel.getUserInfo(userId: session.sessionId)
        let showName = user?.showName() ?? ""
        title = showName
        titleContent = showName
        let text = "\(chatLocalizable("send_to"))\(showName)"
        let attribute = NSMutableAttributedString(string: text)
        let style = NSMutableParagraphStyle()
        style.lineBreakMode = .byTruncatingTail
        style.alignment = .left
        attribute.addAttribute(.font, value: UIFont.systemFont(ofSize: 16), range: NSMakeRange(0, text.utf16.count))
        attribute.addAttribute(.foregroundColor, value: UIColor.gray, range: NSMakeRange(0, text.utf16.count))
        attribute.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, text.utf16.count))
        menuView.textView.attributedPlaceholder = attribute
    

    通过源码自定义 UI

    布局中的内容概略如下:

    swift//这里以 ChatMessageTextCell 为例。
    open class ChatMessageTextCell: NormalChatMessageBaseCell {
        override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            commonUI()
        public required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        open func commonUI() {
            bubbleImageLeft.addSubview(contentLabelLeft)
            NSLayoutConstraint.activate([
            contentLabelLeft.rightAnchor.constraint(equalTo: bubbleImageLeft.rightAnchor, constant: -chat_content_margin),
            contentLabelLeft.leftAnchor.constraint(equalTo: bubbleImageLeft.leftAnchor, constant: chat_content_margin),
            contentLabelLeft.topAnchor.constraint(equalTo: bubbleImageLeft.topAnchor, constant: chat_content_margin),
            contentLabelLeft.bottomAnchor.constraint(equalTo: bubbleImageLeft.bottomAnchor, constant: -chat_content_margin),
            bubbleImageRight.addSubview(contentLabelRight)
            NSLayoutConstraint.activate([
            contentLabelRight.rightAnchor.constraint(equalTo: bubbleImageRight.rightAnchor, constant: -chat_content_margin),
            contentLabelRight.leftAnchor.constraint(equalTo: bubbleImageRight.leftAnchor, constant: chat_content_margin),
            contentLabelRight.topAnchor.constraint(equalTo: bubbleImageRight.topAnchor, constant: chat_content_margin),
            contentLabelRight.bottomAnchor.constraint(equalTo: bubbleImageRight.bottomAnchor, constant: -chat_content_margin),
        override open func showLeftOrRight(showRight: Bool) {
            super.showLeftOrRight(showRight: showRight)
            contentLabelLeft.isHidden = showRight
            contentLabelRight.isHidden = !showRight
        override open func setModel(_ model: MessageContentModel, _ isSend: Bool) {
            super.setModel(model, isSend)
            let contentLabel = isSend ? contentLabelRight : contentLabelLeft
            if let m = model as? MessageTextModel {
            contentLabel.attributedText = m.attributeStr
    

    自定义标题栏按钮

    自V9.6.1 起,自定义导航栏需要使用 customNavigationView 来实现, V9.6.5更名为 navigationView

    navigationView 采用固定按钮,包括左侧的返回按钮(backButton)和右侧的更多按钮(moreButton),此外还包含中间的导航栏标题(navTitle)和底部分割线(titleBarBottomLine),具体定义详见 NENavigationView 类,所有控件均可访问修改。

    Swift
    // 修改标题
    navigationView.navTitle.text = "title"
    // 修改右侧按钮文案
    navigationView.setMoreButtonTitle("clear")
    // 修改右侧按钮文案颜色
    navigationView.moreButton.setTitleColor(.red, for: .normal)
    // 修改右侧按钮点击事件
    navigationView.moreButton.addTarget(self, action: #selector(clearMessage), for: .touchUpInside)
    
    Objective-C
    // 修改标题
    navigationView.navTitle.text = @"title";
    // 修改右侧按钮文案
    [navigationView setMoreButtonTitle:@"clear"];
    // 修改右侧按钮文案颜色
    [navigationView.moreButton setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
    // 修改右侧按钮点击事件
    [navigationView.moreButton addTarget:self action:@selector(clearMessage) forControlEvents:UIControlEventTouchUpInside];
    

    V9.6.1 之前版本实现自定义标题栏按钮的示例代码(Swfit)如下:

    单击查看示例代码
    class CustomP2PChatViewController: P2PChatViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
        let backItem = UIBarButtonItem(
        image: UIImage(named: "arrow_right"),
        style: .plain,
        target: self,
        action: #selector(backEvent) // backEvent 为标题左侧返回按钮的默认点击事件
        let settingItem = UIBarButtonItem(
        image: UIImage(named: "mine_setting"),
        style: .plain,
        target: self,
        action: #selector(toSetting) // toSetting 为标题右侧设置按钮的默认点击事件
        navigationItem.leftBarButtonItems = [backItem]
        navigationItem.rightBarButtonItems = [settingItem]
    

    示例代码中可设置标题左右两侧的多个按钮,若只有单个按钮需求,也可使用内置方法 addLeftActionaddRightAction 分别设置标题左侧和右侧的按钮。

    自定义标题栏标题

    示例代码(Swfit)

    swiftclass CustomP2PChatViewController: P2PChatViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
        override func getSessionInfo(session: NIMSession) {
            super.getSessionInfo(session: session)
            title = "小易助手"
    

    云信 IM 默认会在数据加载完成后设置会话消息页面标题,如需自定义标题,请重写 getSessionInfo 方法,并在父类方法调用之后设置其值。

    继承 NIMCustomAttach 类和 NECustomAttachmentProtocol 协议,实现自定义消息类和序列化方法。

    swift public class CustomAttachment: NECustomAttachment {
         public var goodsName = "name"
         public var goodsURL = "url"
         override public func encode() -> String {
             // 自定义序列化方法之前必须调用父类的序列化方法
             let neContent = super.encode()
             var info: [String: Any] = getDictionaryFromJSONString(neContent) as? [String
    
    
    
    
        
    : Any] ?? [:]
             info["goodsName"] = goodsName
             info["goodsURL"] = goodsURL
             let jsonData = try? JSONSerialization.data(withJSONObject: info, options: [])
             var content = ""
             if let data = jsonData {
             content = String(data: data, encoding: .utf8) ?? ""
             return content
    

    NECustomAttachmentProtocol 协议中的 customType 和 cellHeight 必须实现,前者用于表明自定义消息对应的UI 类型,在步骤4中注册单元格时需要和自定义 cell 进行绑定;后者用于设置自定义 cell 的高度。

    继承 NIMCustomAttachmentCoding 类,对自定义消息进行反序列化。

    swiftpublic class CustomAttachmentDecoder: NECustomAttachmentDecoder {
         override public func decodeCustomMessage(info: [String: Any]) -> CustomAttachment {
             // 自定义反序列化方法之前必须调用父类的反序列化方法
             let neCustomAttachment = super.decodeCustomMessage(info: info)
             let customAttachment = CustomAttachment(customType: neCustomAttachment.customType,
                                                     cellHeight: neCustomAttachment.cellHeight,
                                                     data: neCustomAttachment.data)
             if customAttachment.customType == 20 {
             customAttachment.cellHeight = 50
             customAttachment.goodsName = info["goodsName"] as? String ?? ""
             customAttachment.goodsURL = info["goodsURL"] as? String ?? ""
             return customAttachment
    

    自定义消息类(CustomAttachment)中的 customType 和 cellHeight 需要重点关注,前者用于表明自定义 type 类型,在步骤4中注册单元格时需要和自定义 cell 进行绑定;后者用于设置自定义 cell 的高度。根据自身业务需求进行设置。

    继承 NEChatBaseCell 类,实现自定义单元格,对自定义消息进行展示。

    swift class CustomChatCell: NEChatBaseCell {
         public var testLabel = UILabel()
         override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
             super.init(style: style, reuseIdentifier: reuseIdentifier)
             selectionStyle = .none
             testLabel.translatesAutoresizingMaskIntoConstraints = false
             contentView.addSubview(testLabel)
             NSLayoutConstraint.activate([
                 testLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
                 testLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
             testLabel.font = UIFont.systemFont(ofSize: 14)
             testLabel.textColor = UIColor.black
         override func setModel(_ model: MessageContentModel) {
             print("this is custom message")
             testLabel.text = "this is custom message"
    

    继承 P2PChatViewController 类,注册自定义消息的解析器。

    swift class CustomP2PChatViewController: P2PChatViewController {
         override func viewDidLoad() {
             // 自定义消息以及外部扩展 覆盖cell UI 样式示例
             // 注册自定义消息的解析器
             NIMCustomObject.registerCustomDecoder(CustomAttachmentDecoder())
             // 自定义消息类型的 customtype 与自定义 cell 需要绑定注册、一一对应
             // 自定义消息cell绑定需要放在 super.viewDidLoad() 之前
             NEChatUIKitClient.instance.regsiterCustomCell(["20": CustomChatCell.self])
             super.viewDidLoad()
    

    regsiterCustomCell 方法的入参类型为 [String: UITableViewCell.Type], 其中 key 对应步骤2中的 customType 的String值,需要在此处与其对应的自定义 cell 进行绑定,从而实现不同类型的自定义消息的对应展示。

    会话消息标题栏下方扩展视图

    可在标题栏和消息详情中间插入扩展视图,例如插入温馨提示

    示例代码(Swfit)

    swiftclass CustomP2PChatViewController: P2PChatViewController {
        public lazy var customTopView: CustomView = {
            let view = CustomView(frame: CGRect(x: 0, y: 10, width: NEConstant.screenWidth, height: 40))
            view.backgroundColor = .blue
            return view
        override func viewDidLoad() {
            super.viewDidLoad()
            // 聊天页顶部导航栏下方扩展视图示例
            customTopView.btn.setTitle("通过重写方式添加", for: .normal)
            bodyTopView.addSubview(customTopView)
            bodyTopView.backgroundColor = .yellow
            bodyTopViewHeight = 80
    

    示例代码中 bodyTopView 为聊天页顶部标题栏下方预留的扩展视图,bodyTopViewHeight 为该扩展视图的高度,更改此变量会刷新页面布局。

    输入框上方区域扩展视图

    用户可继承聊天界面详情页(P2PChatViewController/GroupChatViewController),在父类渲染页面之前更改输入框上方区域。

    示例代码(Swfit)

    swiftclass CustomP2PChatViewController: P2PChatViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
            // 输入框上区域扩展视图示例
            customBottomView.btn.setTitle("通过重写方式添加", for: .normal)
            bodyBottomView.addSubview(customBottomView)
            bodyBottomView.backgroundColor = .yellow
            bodyBottomViewHeight = 60
    

    示例代码中 bodyBottomView 为输入框上方预留的扩展视图,bodyBottomViewHeight 为该扩展视图的高度,更改此变量会刷新页面布局。

    示例代码(Swfit)

    swiftclass CustomP2PChatViewController: P2PChatViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
            // 聊天页输入框左右间距自定义
            chatInputView.textviewLeftConstraint?.constant = 100
            chatInputView.textviewRightConstraint?.constant = -100
    

    示例代码中 textviewLeftConstraint/textviewRightConstraint 表示输入框左侧/右侧距页面左边框/右边框的距离,即输入框左右间距,更改此变量会刷新页面布局。

    swift    class CustomP2PChatViewController: P2PChatViewController {
        override func viewDidLoad() {
        NEKitChatConfig.shared.ui.chatInputBar = { [weak self] item in
          // 修改
          let takePicBtn = item[2]
          takePicBtn.setImage(nil, for: .normal)
          takePicBtn.setTitle("拍照", for: .normal)
          takePicBtn.setTitleColor(.blue, for: .normal)
          takePicBtn.removeTarget(takePicBtn.superview, action: nil, for: .allEvents)
          takePicBtn.addTarget(self, action: #selector(self?.customClick), for: .touchUpInside)
          // 新增
          let button = UIButton(type: .custom)
          button.setTitle("新增", for: .normal)
          button.setTitleColor(.blue, for: .normal)
          button.addTarget(self, action: #selector(self?.customClick), for: .touchUpInside)
          item.append(button)
        super.viewDidLoad()
        // 通过重写实现
        ```swift
        class CustomP2PChatViewController: P2PChatViewController {
            override func viewDidLoad() {
                super.viewDidLoad()
                let subviews = chatInputView.stackView.subviews
                subviews.forEach { view in
                    view.removeFromSuperview()
                    chatInputView.stackView.removeArrangedSubview(view)
                let titles = ["表情", "语音", "照片", "更多"]
                for i in 0 ..< titles.count {
                    let button = UIButton(type: .custom)
                    let title = titles[i]
                    button.translatesAutoresizingMaskIntoConstraints = false
                    button.addTarget(self, action: #selector(buttonEvent), for: .touchUpInside)
                    button.tag = i
                    button.setTitle(title, for: .normal)
                    button.setTitleColor(.blue, for: .normal)
                    chatInputView.stackView.addArrangedSubview(button)
            @objc func buttonEvent(_ btn: UIButton) {
                if btn.tag == 0 { // 表情
                    layoutInputView(offset: bottomExanpndHeight)
                    chatInputView.addEmojiView()
                } else if btn.tag == 1 { // 语音
                    layoutInputView(offset: bottomExanpndHeight)
                    chatInputView.addRecordView()
                } else if btn.tag == 2 { // 照片
                    goPhotoAlbumWithVideo(self)
                } else if btn.tag == 3 { // 更多
                    layoutInputView(offset: bottomExanpndHeight)
                    chatInputView.addMoreActionView()
        :::note note 
        示例代码中,`chatInputView` 为输入框视图,`chatInputView.stackView` 为输入框底部工具栏视图,代码先将工具栏视图中的所有子视图(按钮)移除,然后依次添加自定义的子视图(按钮),并为按钮绑定点击事件buttonEventbuttonEvent 中利用 tag 对按钮进行区分,实现具体的逻辑
    - 效果对比
      |默认|自定义后|
      |:----|:----|
      |![消息详情页默认.png](https://yx-web-nosdn.netease.im/common/1f92c687026734891bbb897355bda14e/消息详情页默认.png)|![消息详情底部工具条按钮.png](https://yx-web-nosdn.netease.im/common/6decd7562874d063cc48780fe6a19ba2/消息详情底部工具条按钮.png)
    ### 自定义 “更多” 功能页
    用户可根据业务需求,自定义聊天输入区域的 “更多” 功能区
    - 示例代码(Swfit)
        // 通过配置项实现
        ```swift
        class CustomP2PChatViewController: P2PChatViewController {
        NEKitChatConfig.shared.ui.chatInputMenu = { [weak self] menuList in
          // 新增未知类型
          let itemNew = NEMoreItemModel()
          itemNew.customImage = UIImage(named: "mine_collection")
          itemNew.customDelegate = self
          itemNew.action = #selector(self?.customClick)
          itemNew.title = "新增"
          menuList.append(itemNew)
          // 覆盖已有类型
          // 遍历 menuList, 根据type 覆盖已有类型
          for item in menuList {
            if item.type == .rtc {
              item.customImage = UIImage(named: "mine_setting")
              item.customDelegate = self
              item.action = #selector(self?.customClick)
              item.type = .rtc
              item.title = "覆盖"
          // 移除已有类型
          // 遍历 menuList, 根据type 移除已有类型
          for (i, item) in menuList.enumerated() {
            if item.type == .file {
              menuList.remove(at: i)
            // 需要在父类视图加载之前完成自定义
            super.viewDidLoad() 
        // 通过重写方式实现
        ```swift
        class CustomP2PChatViewController: P2PChatViewController {
        override func viewDidLoad() {
            // 新增未知类型
            let itemNew = NEMoreItemModel()
            itemNew.customImage = UIImage(named: "itemNew")
            itemNew.customDelegate = self
            itemNew.action = #selector(itemNewClicked)
            itemNew.title = "新增"
            NEChatUIKitClient.instance.moreAction.append(itemNew)
            // 覆盖已有类型
            // 遍历 NEChatUIKitClient.instance.moreAction, 根据type 覆盖已有类型
            for (i, item) in NEChatUIKitClient.instance.moreAction.enumerated() {
                if item.type == .rtc {
                    let itemReplace = NEChatUIKitClient.instance.moreAction[i]
                    itemReplace.customImage = UIImage(named: "itemReplace")
                    // 覆盖默认的点击事件
                    // itemReplace.customDelegate = self
                    // itemReplace.action = #selector(itemReplaceClicked)
                    itemReplace.type = .rtc
                    itemReplace.title = "覆盖"
            // 移除已有类型
            // 遍历 NEChatUIKitClient.instance.moreAction, 根据type 移除已有类型
            for (i, item) in NEChatUIKitClient.instance.moreAction.enumerated() {
                if item.type == .file {
                    NEChatUIKitClient.instance.moreAction.remove(at: i)
                    break
            // 需要在父类视图加载之前完成自定义
            super.viewDidLoad() 
        上述代码中的 `NEChatUIKitClient.instance.moreAction` 为全局 “更多” 功能列表,通过 `itemNew.customImage` 和 `itemNew.customDelegate` 共同组合来实现按钮的自定义点击事件IM UIKit 默认实现的能力如下表所示,内部对这些类型的点击事件做了默认实现,可根据业务需要来决定是否覆盖
        <div style="width:140px">标识Action</div>  | 说明 
        :---- | :---------
        `NEMoreActionType.takePicture` |  拍照
        `NEMoreActionType.file` |  发送文件
        `NEMoreActionType.rtc` |  音视频通话
        `NEMoreActionType.location`  | 地理位置
    - 效果对比
      |默认|自定义后|
      |:----|:----|
      |![消息详情页默认.png](https://yx-web-nosdn.netease.im/common/1f92c687026734891bbb897355bda14e/消息详情页默认.png)|![消息详情更多功能页.png](https://yx-web-nosdn.netease.im/common/6ee4a3d3d23cbbccb822222fba349cf7/消息详情更多功能页.png)
    ### 消息长按菜单过滤部分能力
    - 示例代码
    ```swift
    class CustomP2PChatViewController: P2PChatViewController {
        override func viewDidLoad() {
        // 长按消息功能弹窗过滤列表(过滤列表中的能力会在整个页面中禁用)
        operationCellFilter = [.delete]
    

    示例代码中的 operationCellFilter 为长按菜单过滤列表, 其中的类型在整个会话详情页不会显示,示例代码最终的效果表现为:整个会话详情页的所有消息类型的长按菜单中均不显示【删除】功能

    swiftclass CustomP2PChatViewController: P2PChatViewController {
        override func viewDidLoad() {
            /// 消息长按弹出菜单自定义
            NEKitChatConfig.shared.ui.chatPopMenu = { menuList, model in
                // 遍历 menuList, 根据 type 覆盖已有类型
                // 将所有文本消息的【复制】替换成【粘贴】
                if model?.type == .text {
                    for item in menuList {
                        if item.type == .copy {
                            item.text = "粘贴"
            /// 消息长按弹出菜单点击事件回调,根据按钮类型进行区分
            NEKitChatConfig.shared.ui.popMenuClick = { [weak self] item in
            switch item.type {
                case .copy:
                    // 更改【复制】类型按钮的点击事件
                    self?.customClick()
                default:
                    break
    

    示例代码中的 chatPopMenu 为消息长按弹出菜单回调、popMenuClick 为消息长按弹出菜单点击事件回调, 可以根据 type 类型进行区分进而实现自定义

    基础版 UI Cell 类别
  •