实现 1 对 1 消息

更新时间: 2023/08/24 02:15:59

本文介绍通过消息聊天组件 chatKit 实现 1 对 1 消息。消息聊天组件 chatKit 底层基于IM UIKit。

功能介绍

1 对 1 消息的主要功能包括会话列表、聊天消息、通知消息和语音输入。

1对1消息.png

前提条件

请确保您已完成以下操作:

准备工作

注册云信 IM 测试账号,获取 accid 和 token

为了方便您调试,您可以在云信控制台注册云信 IM 测试账号,获取 accid 和 token。accid 和 token 将用于登录云信服务端。

  1. 云信控制台的首页单击指定应用名称,进入该应用的详情页面。

  2. 在左侧导航栏选择产品功能 > IM即时通讯,单击基础功能页签。

  3. 测试账号管理区域单击子功能配置
    测试账号管理.png

  4. 在测试账号管理页面,单击新建账号,并填写账号(即accid)、昵称(即 name)、密码(即 Token)后,单击确定

    新建账号.png

配置高德地图参数(地理位置消息功能需要)

地理位置消息功能基于高德地图,因此需要您配置高德地图相关信息。在初始化路由的同时,调用 setupMapClient 方法设置高德地图的 API Key,对地图 Map 进行初始化。

  • Swift
 func loadService() {
        //初始化路由
        ContactRouter.register()
        ChatRouter.register()
        TeamRouter.register()
        ConversationRouter.register()
        //注册个人设置页面,用于实现单击头像后跳转至个人设置页面功能
        Router.shared.register(MeSettingRouter) { param in
            if let nav = param["nav"] as? UINavigationController {
                let me = PersonInfoViewController()
                nav.pushViewController(me, animated: true)
            }
        }
        
        //地图map初始化
        NEMapClient.shared().setupMapClient(withAppkey: AppKey.gaodeMapAppkey)
    }
  • Objective-C
    - (void)registerRouter {
        [ContactRouter register];
        [ChatRouter register];
        [TeamRouter register];
        [ConversationRouter register];
            //注册个人设置页面,用于实现单击头像后跳转至个人设置页面功能
        [[Router shared] register:MeSettingRouter
                          closure:^(NSDictionary<NSString *, id> *_Nonnull param) {
            NSObject *param1 = [param objectForKey:@"nav"];
            if ([param1 isKindOfClass:[UINavigationController class]]) {
            UINavigationController *nav = (UINavigationController *)param1;
            PersonInfoViewController *me =
                [[PersonInfoViewController alloc] init];
    
            [nav pushViewController:me animated:YES];
            }
        }];
    
        //地图map初始化
        [[NEMapClient shared] setupMapClientWithAppkey:@"gaodeMap Appkey"];
    }
    

开发环境

开发环境要求如下:

环境要求 说明
iOS 版本 11.0 及以上的 iPhone 或者 iPad 真机
CPU 架构 ARM64、ARMV7
IDE XCode 10 及以上版本
其他 安装 CocoaPods。

示例项目源码

1 对 1 娱乐社交示例项目源码,跑通示例项目的方法请参见跑通示例项目

步骤1:集成 chatKit 组件

您可通过添加远端仓库依赖或者添加本地代码依赖,导入组件。

添加远端仓库依赖

本节介绍如何通过 Cocoapods 添加远程依赖,将您业务所需的的 UI 组件导入到您的项目,进行项目构建。

  1. 创建 Podfile 文件,并在 Podfile 文件中引入 Ui 组件。

    引入 UI 组件时,需要指定相同版本的基础 Kit 库引入,否则可能导致后续出现报错。具体可参见下文的常见问题排查

    swift# Uncomment the next line to define a global platform for your project
    platform :ios, '9.0'
    
    target 'your project name' do
    # Comment the next line if you don't want to use dynamic frameworks
    use_frameworks!
    
    # UI组件
    pod 'NEConversationUIKit', '9.4.0' //会话列表组件
    pod 'NEChatUIKit', '9.4.0' //会话(聊天)组件
    pod 'NERtcCallUIKit', '1.8.2'//呼叫组件
    
    # Kit组件(和UI组件对应)
    pod 'NEConversationKit', '9.4.0'
    pod 'NEChatKit', '9.4.0'
    pod 'NERtcCallKit', '1.8.2'
    
    # 基础Kit库
    pod 'NECommonUIKit', '9.4.0'
    pod 'NECommonKit', '9.4.0'
    pod 'NECoreIMKit', '9.4.0'
    pod 'NECoreKit', '9.4.0'
    
    # 扩展库
    pod 'NEMapKit', '9.4.0' //地理位置组件
    pod 'NERtcSDK', '4.6.29'//RTC音视频组件
    
    end
    
  2. 执行以下命令导入组件。

pod install
  • 上述示例代码中的 9.4.0 为版本号,仅用于示例。建议使用最新版本。 如果出现类似“版本不存在”的报错,可执行pod update命令,然后双击 .xcworkspace 文件,启动项目即可。
  • 如果需要在 Objective-C 项目中导入组件,头文件引用请参考以下引用方式。
    #import <NEConversationUIKit/NEConversationUIKit-Swift.h>
    #import <NEContactUIKit/NEContactUIKit-Swift.h>
    #import <NEChatUIKit/NEChatUIKit-Swift.h>
    #import <NETeamUIKit/NETeamUIKit-Swift.h>
    #import <NERtcCallUIKit/NERtcCallUIKit.h>
    ...
    

添加本地代码依赖

本节介绍如何通过 Cocoapods 添加本地依赖,将所需的 IM UIKit 源码导入到您的项目。

  1. 前往云信开源代码仓库,下载开源的 IM UIKit 到本地,然后将源码文件夹拷贝到项目目录。
  2. 在 Podfile 文件中写入以下内容。
  pod 'NEQChatUIKit', :path => 'NEQChatUIKit/NEQChatUIKit.podspec'
  pod 'NEContactUIKit', :path => 'NEContactUIKit/NEContactUIKit.podspec'
  pod 'NEConversationUIKit', :path => 'NEConversationUIKit/NEConversationUIKit.podspec'
  pod 'NETeamUIKit', :path => 'NETeamUIKit/NETeamUIKit.podspec'
  pod 'NEChatUIKit', :path => 'NEChatUIKit/NEChatUIKit.podspec'
  pod 'NEMapKit', :path => 'NEMapKit/NEMapKit.podspec'
  pod 'NERtcCallUIKit', :path => 'NERtcCallUIKit/NERtcCallUIKit.podspec'

上述内容中的 path 为相对路径,即当对应组件源码文件与 Podfile 文件处于同级目录时,才能通过上述相对路径正确引入组件。

  1. 执行以下命令导入 IM UIKit 源码。
pod install

步骤2:初始化

在应用启动后,调用 setupCoreKitIM 方法进行初始化。

option 参数 是否必传 说明
appKey 云信控制台获取到的 App Key
apnsCername APNs 推送证书名,如不需要实现离线推送可不配置
pkCername PushKit 推送证书名,如不需要实现离线推送可不配置

示例代码:

Swift
let option = NIMSDKOption()
option.appKey = "your app key"
option.apnsCername = "云信控制台配置的 APNS 推送证书名称"
option.pkCername = "云信控制台配置的 PushKit 推送证书名称"
IMKitClient.instance.setupCoreKitIM(option)
let _ = NEAtMessageManager.instance
Objective-C
NIMSDKOption *option = [NIMSDKOption optionWithAppKey:AppKey];
option.apnsCername = @"";
option.pkCername = @"";
[[IMKitClient instance] setupCoreKitIM:option];
NEAtMessageManager * _ = [NEAtMessageManager instance];

更多初始化说明,请参见初始化

步骤3:登录

在完成初始化后,调用 loginIM 方法登录 IM。

示例代码:

Swift
    IMKitClient.instance.loginIM(accid, token) { error in
        if let err = error {
            print("NEKitCore login error : ", err)
        }else {
            //在登录成功回调中初始化路由以及配置各个模块首页
            /*
             weakSelf?.setupTabbar()
             */
        }
    }
Objective-C
[[IMKitClient instance] loginIM:@"accid" :@"token" :^(NSError * _Nullable error) {
    if (error != nil) {
        NSLog(@"NEKitCore login error : %@", [error description]);
    } else {
        //在登录成功回调中初始化路由以及配置各个模块首页
        /*
        [weakSelf setupTabbar];
        */
    }
}];

调用登录的方法时,将示例代码中的 accidtoken 分别替换为您的云信账号 ID (即 accid)和 Token。

步骤4:初始化路由

如果未在登录成功回调中初始化路由,需要单独初始化路由,才能进行后续的界面搭建。在初始化路由时可同时初始化地图 Map,初始化后,您的应用即可实现地理位置消息功能。具体请参见实现地理位置消息功能

示例代码:

Swift
 func loadService() {
        //初始化路由
        ContactRouter.register()
        ChatRouter.register()
        TeamRouter.register()
        ConversationRouter.register()
        //注册个人设置页面,用于实现单击头像后跳转至个人设置页面功能
        Router.shared.register(MeSettingRouter) { param in
            if let nav = param["nav"] as? UINavigationController {
                let me = PersonInfoViewController()
                nav.pushViewController(me, animated: true)
            }
        }
        
        //地图map初始化
        NEMapClient.shared().setupMapClient(withAppkey: AppKey.gaodeMapAppkey)
    }
Objective-C
- (void)registerRouter {
    [ContactRouter register];
    [ChatRouter register];
    [TeamRouter register];
    [ConversationRouter register];
        //注册个人设置页面,用于实现单击头像后跳转至个人设置页面功能
    [[Router shared] register:MeSettingRouter
                      closure:^(NSDictionary<NSString *, id> *_Nonnull param) {
        NSObject *param1 = [param objectForKey:@"nav"];
        if ([param1 isKindOfClass:[UINavigationController class]]) {
        UINavigationController *nav = (UINavigationController *)param1;
        PersonInfoViewController *me =
            [[PersonInfoViewController alloc] init];

        [nav pushViewController:me animated:YES];
        }
    }];

    //地图map初始化
    [[NEMapClient shared] setupMapClientWithAppkey:@"gaodeMap Appkey"];
}

若需要注册个人设置页面,实现单击头像后跳转至个人设置页面的功能,首先需要在 XCode 中拖入相关的源码文件至您的工程。相关的源码文件包括:

  • app 下的 Mine 文件
  • app 下 Assets 中的 Mine 文件

步骤5:界面搭建(Swift)

1 对 1 娱乐社交消息系统常用的功能包括会话列表和聊天界面,本文介绍 chatKit 如何基于 IM UIKit 搭建相应界面。

搭建会话列表

基于 Fragment 方式集成 IM UIKit 的会话列表,具体步骤请参见IM UIKit 的集成会话列表

搭建聊天界面

通过会话消息模块(chatkit-ui)搭建聊天界面,实现接收和发送基本的消息类型,包括文本消息、图片消息、语音消息、视频消息、表情和地理位置消息。

IM UIKit 提供基于 UITableview 实现的会话消息界面,其类名为 ChatViewController下的 P2PChatViewController 子类。

方法原型

let p2pChatVC = P2PChatViewController(session: session)

参数说明

参数 类型 说明
session NIMSession 会话对象
anchor NIMMessage 锚点(用于历史消息搜索)

代码示例

    if conversationModel?.recentSession?.session?.sessionType == .P2P {
        let session = NIMSession(commonId, type: .P2P)
        Router.shared.use(PushP2pChatVCRouter, parameters: ["nav": self.navigationController as Any, "session" : session as Any], closure: nil)
    }else if conversationModel?.recentSession?.session?.sessionType == .team {
        
        let session = NIMSession(commonId, type: .team)
        Router.shared.use(PushTeamChatVCRouter, parameters: ["nav": self.navigationController as Any, "session" : session as Any], closure: nil)
    }
 

使用时,根据不同的会话类型,跳转到对应的界面,开源代码中内部跳转通过 Router 路由实现。路由器的具体使用说明,请参见界面跳转

实现通知消息

通知消息.png

调用以下方法实现发送通知消息,通知消息只有本端可见,对方不可见。

示例代码:


sendSalutionMsg(type: 1, antiSpamMessage: nil)

  // 发送打招呼消息:本地消息
  func sendSalutionMsg(type: Int, antiSpamMessage: NIMMessage?) {
    let message = NIMMessage()
    if let antiSpamMessage = antiSpamMessage {
      message.timestamp = antiSpamMessage.timestamp + 1
    }
    let object = NIMCustomObject()
    let attachment = CustomAttachment()
    // 本地消息不回走到 CustomAttachment的解析,所以需要手动 cellHeight 以及 customType ,否则不会刷新。发送消息不需要
    attachment.cellHeight = 50 + 20
    switch type {
    case 1:
      // audio
      attachment.type = OneOnOneChatCustomMessageType.TRY_AUDIO_CALL_MESSAGE_TYPE
      attachment.customType = OneOnOneChatCustomMessageType.TRY_AUDIO_CALL_MESSAGE_TYPE
    case 2:
      // video
      attachment.type = OneOnOneChatCustomMessageType.TRY_VIDEO_CALL_MESSAGE_TYPE
      attachment.customType = OneOnOneChatCustomMessageType.TRY_VIDEO_CALL_MESSAGE_TYPE
    case 3:
      // 三方消息违规
      attachment.type = OneOnOneChatCustomMessageType.PRIVACY_RISK_MESSAGE_TYPE
      attachment.customType = OneOnOneChatCustomMessageType.PRIVACY_RISK_MESSAGE_TYPE
    case 4:
      // 通用违规消息
      attachment.type = OneOnOneChatCustomMessageType.COMMON_RISK_MESSAGE_TYPE
      attachment.customType = OneOnOneChatCustomMessageType.COMMON_RISK_MESSAGE_TYPE
    default:
      attachment.type = OneOnOneChatCustomMessageType.ACCOST_MESSAGE_TIPS_TYPE
      attachment.customType = OneOnOneChatCustomMessageType.ACCOST_MESSAGE_TIPS_TYPE
      attachment.cellHeight = 30 + 20
    }
    object.attachment = attachment
    message.messageObject = object
    let setting = NIMMessageSetting()
    setting.shouldBeCounted = false
    message.setting = setting
    viewmodel.repo.saveMessageToDB(message, viewmodel.session) { error in
      print("send custom message error : ", error?.localizedDescription as Any)
    }
  }
  //注:customType 需要有对应的注册:如下所示

  registerCellDic[String(OneOnOneChatCustomMessageType.ACCOST_MESSAGE_TIPS_TYPE)] = NEOneOnOneTextSalutuionCell.self
    registerCellDic[String(OneOnOneChatCustomMessageType.PRIVACY_RISK_MESSAGE_TYPE)] = NEOneOnOneTextThirdPrivacyCell.self
    registerCellDic[String(OneOnOneChatCustomMessageType.TRY_AUDIO_CALL_MESSAGE_TYPE)] = NEOneOnOneAudioSalutuionCell.self
    registerCellDic[String(OneOnOneChatCustomMessageType.TRY_VIDEO_CALL_MESSAGE_TYPE)] = NEOneOnOneVideoSalutuionCell.self
    registerCellDic[String(OneOnOneChatCustomMessageType.COMMON_RISK_MESSAGE_TYPE)] = NEOneOnOneTextNonComplianceCell.self
    registerCellDic[String(SEND_GIFT_TYPE_SEND)] = NEOneOnOneRewardRightCell.self
    registerCellDic[String(SEND_GIFT_TYPE_RECV)] = NEOneOnOneRewardLeftCell.self
    registerCellDic[String(OneOnOneChatCustomMessageType.OFFICIAL_GIFT_TYPE)] = NEOneOnOneOfficialCell.self

注:上述的cell 都是基于NEChatBaseCell 的自定义UI

搭建语音输入页面

按住说话1.png

示例代码:

// 语音输入条
  lazy var audioInputButton: NEOneOnOneSpeakButton = {
    let audioInputButton = NEOneOnOneSpeakButton()
    let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressGesture(_:)))
    audioInputButton.addGestureRecognizer(longPressGesture)
    return audioInputButton
  }()
  //将语音输入条添加到页面
  menuView.addSubview(audioInputButton)
    audioInputButton.snp.makeConstraints { make in
      make.left.right.top.bottom.equalTo(menuView.textView)
    }
    view.bringSubviewToFront(audioInputButton)

// 长按手势处理函数
  @objc func handleLongPressGesture(_ gesture: UILongPressGestureRecognizer) {
    let semaphore = DispatchSemaphore(value: 0)

    var hasPermissions = false
    var firstRequest = false

    let microphoneStatus = AVCaptureDevice.authorizationStatus(for: .audio)

    switch microphoneStatus {
    case .authorized:
      hasPermissions = true
      print("Microphone access granted")
    case .denied:
      print("Microphone access denied")
    case .notDetermined:
      firstRequest = true
      print("Microphone access not determined")
    case .restricted:
      print("Microphone access restricted")
    @unknown default:
      fatalError("Unknown microphone status")
    }

    if firstRequest {
      AVCaptureDevice.requestAccess(for: .audio) { granted in
        if granted {
          hasPermissions = true
          semaphore.signal()
        } else {
          semaphore.signal()
          print("Microphone access denied")
        }
      }
    } else {
      semaphore.signal()
    }

    semaphore.wait()
    if !hasPermissions {
      NEOneOnOneToast.show(ne_localized("麦克风权限已关闭,请开启后重试"))
      return
    }
    if hasPermissions, firstRequest {
      /// 第一次申请不进行录制操作
      return
    }
    print("权限判断通过")

    // 判断是否在小窗:弹出Toast
    if NEOneOnOneUIKitEngine.sharedInstance().canCall() != nil {
      let message = NEOneOnOneUIKitEngine.sharedInstance().canCall()

      if let message = message, message.count > 0 {
        /// 不能操作
        NEOneOnOneToast.show(ne_localized("您当前处于语聊房中,请结束后再试"))
        return
      }
    }

    switch gesture.state {
    case .began:

      NEOneOnOneLog.infoLog(
        tag,
        desc: "开始录音"
      )
      // 添加全屏视图
      _audioInputingView = nil
      UIApplication.shared.keyWindow?.addSubview(audioInputingView)
      // TODO: 录制开始
      startRecord()

    case .changed:
      print("changed")
      // 手指不离开进行滑动
      // 判断是否滑动到全屏视图的某一个区域
      let inreact = CGRectContainsPoint(audioInputingView.audioInputImageView.frame, gesture.location(in: UIApplication.shared.keyWindow))
      if inreact {
        audioInputingView.needCancel = true
      } else {
        audioInputingView.needCancel = false
      }
    case .ended:
      // 手指离开
      if !audioInputingView.needCancel {
        // 发送
        NEOneOnOneLog.infoLog(
          tag,
          desc: "结束录音并发送"
        )
        endRecord(insideView: true)
      } else {
        // 不发送
        // 发送
        NEOneOnOneLog.infoLog(
          tag,
          desc: "结束录音不发送"
        )
        endRecord(insideView: false)
      }
//      audioInputingView.endAudioiInputing()
      audioInputingView.removeFromSuperview()
    default:
      break
    }
  }
此文档是否对你有帮助?
有帮助
去反馈
  • 功能介绍
  • 前提条件
  • 准备工作
  • 开发环境
  • 示例项目源码
  • 步骤1:集成 chatKit 组件
  • 添加远端仓库依赖
  • 添加本地代码依赖
  • 步骤2:初始化
  • 步骤3:登录
  • 步骤4:初始化路由
  • 步骤5:界面搭建(Swift)
  • 搭建会话列表
  • 搭建聊天界面
  • 实现通知消息
  • 搭建语音输入页面