最近学习iOS用到了IM 集成了融云,跳了很多坑 记录下
PS: 代码不一定要集成一次写一次,应该一个功能尽可能的写一次,其他的地方都是尽可能的copy
- 集成
pod 'RongCloudIM/IMKit', '~> 2.9.17'
2.管理及监听类(本版本暂未集成离线推送,后期迭代添加)
//
// RongHelp.swift
//
// Created by mac on 2019/9/20.
// Copyright © 2019 mac . All rights reserved.
//
import Foundation
class RongHelp: NSObject {
static let shared = RongHelp()
private var userHandler = UserHandler()
func initRong() {
RCIM.shared().initWithAppKey(KRongKey)
RCIM.shared().enablePersistentUserInfoCache = false
/// 是否在发送的所有消息中携带当前登录的用户信息 设置为false 否则Android和ios交互可能会问题 经测试消息显示用户名会偶尔显示未用户id
RCIM.shared().enableMessageAttachUserInfo = false
RCIM.shared().receiveMessageDelegate = self
RCIM.shared().connectionStatusDelegate = self
RCIM.shared().userInfoDataSource = self
}
/// 注册用户
func registerRongYun() {
RCIM.shared().connect(
withToken: UserToken.shared.imToken,
success: { cuccessString in
},
error: { (errorCode) in
},
tokenIncorrect: { [weak self] in
self?.requestRongToken()
})
}
/// 刷新用户信息
func refreshUserInfoCache(userInfo: RCUserInfo? = nil) {
if userInfo != nil {
RCIM.shared().refreshUserInfoCache(userInfo, withUserId: userInfo!.userId)
return
}
if let bean = UserToken.shared.userBean {
RCIM.shared().refreshUserInfoCache(
RCUserInfo(
userId: "\(bean.userBean.id)",
name: bean.profileBean.nickName,
portrait: TSRouter.fileCheckUrl + bean.profileBean.avatar
),
withUserId: "\(bean.userBean.id)"
)
}
}
}
// MARK: 登录状态监听
extension RongHelp: RCIMConnectionStatusDelegate{
func onRCIMConnectionStatusChanged(_ status: RCConnectionStatus) {
/// 挤下线
if status == .ConnectionStatus_KICKED_OFFLINE_BY_OTHER_CLIENT {
MBProgressHUD.show(info: "你的账号在其他地方登录,请重新登录")
// TODO 挤下线代码逻辑
}
}
}
// 融云代理
extension RongHelp: RCIMUserInfoDataSource, RCIMReceiveMessageDelegate{
/// 获取消息未读数
func unReadCount() -> Int {
return Int(RCIMClient.shared().getTotalUnreadCount())
}
/// 消息个数
func onRCIMReceive(_ message: RCMessage!, left: Int32) {
/// 如下的代码 是一个融云头像问题的解决思路,稍后会说到,如果有更好的办法 这个方法可以省略
NotificationCenter.default.post(name: UserNotification.rongImMessageCount.notification, object: self)
if message.conversationType == RCConversationType.ConversationType_PRIVATE {
do{
if let textMessage = message.content as? RCTextMessage, let extra = textMessage.extra {
let json = JSON(parseJSON: extra)
let userInfo = RCUserInfo(userId: json["id"].stringValue, name: json["name"].stringValue, portrait: json["avatar"].stringValue)
refreshUserInfoCache(userInfo: userInfo)
}
}catch{
print("-------rong 解析异常")
}
}
}
// 内容提供者
func getUserInfo(withUserId userId: String!, completion: ((RCUserInfo?) -> Void)!) {
guard let rongUserId = userId else {
return
}
// 调用自己接口查询userInfo
requestRongUserInfo(userId: rongUserId)
}
}
// 网络请求部分
extension RongHelp {
// 查询用户信息
func requestRongUserInfo(userId: String) {
userHandler.requestRongUserInfo(userID: userId, success: { userInfo in
RCIM.shared().refreshUserInfoCache(userInfo
, withUserId: userId)
}) { (_) in
}
}
//获取登录ImToken 总共重试5次 每次间隔1S
func requestRongToken() {
var retryCount = 0
func requestToken() {
userHandler.requestRongToken(
success: { (token) in
retryCount = 0
UserToken.shared.imToken = token
}) { (error) in
retryCount += 1
if retryCount < 5 {
requestToken()
}
}
}
requestToken()
}
}
- 使用
本app为强登录类型app, 具体注册地方看项目需求
AppDelegate 中RongHelp.shared.initRong()
账号登录后 重新注册融云 防止被挤下线 账号登录后融云未登录
/// 获取用户信息
func requestUserInfo() {
UserHandler().requestUserInfo(
isThread: false,
success: {
[weak self] _ in
RongHelp.shared.registerRongYun()
/// more 其他的业务逻辑
}) { (error) in
/// more
}
}
/// 注册用户
func registerRongYun() {
RCIM.shared().connect(
withToken: UserToken.shared.imToken,
success: { cuccessString in
},
error: { (errorCode) in
},
tokenIncorrect: { [weak self] in
self?.requestRongToken()
})
}
4,自定义聊天列表
class HomeRongMessageController: RCConversationListViewController {
override func viewDidLoad() {
super.viewDidLoad()
title = "消息"
// 设置头像大小
setConversationPortraitSize(CGSize(width: 60, height: 60))
// 头像形状
setConversationAvatarStyle(RCUserAvatarStyle.USER_AVATAR_CYCLE)
//显示的会话类型 本页面为系统消息和单聊
setDisplayConversationTypes([
RCConversationType.ConversationType_PRIVATE.rawValue,
RCConversationType.ConversationType_SYSTEM.rawValue]
)
conversationListTableView.separatorStyle = UITableViewCell.SeparatorStyle.none
conversationListTableView.register(R.nib.homeRongMessageCell)
// 自定义空数据占位图
// emptyConversationView = emptyView
}
}
extension HomeRongMessageController {
// 加数据标签 加了之后 才会调用下面的rcConversationListTableView代理方法
override func willReloadTableData(_ dataSource: NSMutableArray!) -> NSMutableArray! {
dataSource.forEach {
let model: RCConversationModel = $0 as! RCConversationModel
model.conversationModelType = RCConversationModelType.CONVERSATION_MODEL_TYPE_CUSTOMIZATION
}
return dataSource
}
override func rcConversationListTableView(_ tableView: UITableView!, heightForRowAt indexPath: IndexPath!) -> CGFloat {
return 90
}
// 自定义显示样式
override func rcConversationListTableView(_ tableView: UITableView!, cellForRowAt indexPath: IndexPath!) -> RCConversationBaseCell! {
let cell = conversationListTableView.dequeueReusableCell(withIdentifier: R.reuseIdentifier.homeRongMessageCell, for: indexPath)!
cell.delegate = self
cell.config(model: conversationListDataSource[indexPath.row] as! RCConversationModel)
return cell
}
// 点击事件
override func onSelectedTableRow(_ conversationModelType: RCConversationModelType, conversationModel model: RCConversationModel!, at indexPath: IndexPath!) {
}
}
// 解决融云头像的一种思路 有好办法 可以去掉
extension HomeRongMessageController: HomeRongMessageCellDelegate {
func refreshUserInfo(userId: String) {
UserHandler().requestRongUserInfo(userID: userId, success: { userInfo in
RongHelp.shared.refreshUserInfoCache(userInfo: userInfo)
self?.conversationListTableView.reloadData()
}) { (_) in
}
}
}
自定义显示的ui及代码逻辑
以下是消息接受的自定义cell中的代码逻辑(只添加了一部分常用的,更多的自己仿写,ps:融云开发文档没法看,代码注释写的是真好,给前辈点赞)
let userModel = RCIM.shared().getUserInfoCache(model.targetId)
// 没有缓存用户信息 去接口查询
if userModel == nil {
delegate?.refreshUserInfo(userId: model.targetId)
}
// 消息的发送状态
switch model.sentStatus {
case .SentStatus_FAILED:
contentLeftConstraint.constant = 24
stateTipImage.isHidden = false
stateTipImage.image = R.image.im.im_tip()!
case .SentStatus_SENDING:
contentLeftConstraint.constant = 24
stateTipImage.isHidden = false
stateTipImage.image = R.image.im.im_send_ing()!
default:
contentLeftConstraint.constant = 0
stateTipImage.isHidden = true
}
// 接受状态
switch model.receivedStatus {
case .ReceivedStatus_READ:
contentLabel.textColor = UIColor.colour.gamut999999
default:
contentLabel.textColor = UIColor.blues.gamut24BD90
}
// 草稿
if !model.draft.isEmpty {
contentLabel.attributedText = PublicHelper.attributedString(texts: ["[草稿] ", model.draft], colors: [UIColor.red, UIColor.colour.gamut4D4D4D])
return
}
// 正常内容
if let textMessage = (model.lastestMessage as? RCTextMessage) {
contentLabel.text = textMessage.content
contentLabel.textColor = UIColor.colour.gamut999999
}
// 图片信息
if model.lastestMessage is RCImageMessage {
contentLabel.text = "[图片]"
}
// 图片信息
if model.lastestMessage is RCVoiceMessage {
contentLabel.text = "[语音]"
contentLabel.textColor = (model.receivedStatus == .ReceivedStatus_LISTENED)
? UIColor.colour.gamut999999
: UIColor.blues.gamut24BD90
}
// 文件
if let bean = model.lastestMessage as? RCFileMessage {
contentLabel.text = "[文件] " + bean.name
contentLabel.textColor = UIColor.colour.gamut999999
}
效果如下 (只截图部分,消息发送失败等都已添加)
5.单聊会话页
class ImDetailController: RCConversationViewController {
var userMap: [String: String]? = nil
override func viewDidLoad() {
super.viewDidLoad()
initView()
}
func initView() {
// 去掉聊天页右上交自带的设置按钮
self.navigationItem.rightBarButtonItem = nil
// 如果用到IQKeyboardManager 必须false 否则键盘和数据框之间会有间距
IQKeyboardManager.shared().isEnabled = false
// 更新下历史聊天列表中自己的头像 防止自己更新头像 返回本页面 部分头像显示旧头像问题
RongHelp.shared.refreshUserInfoCache()
enableNewComingMessageIcon = true
enableUnreadMessageIcon = true
// 这段代码很重要 稍后会单独讲到
if let bean = UserToken.shared.userBean {
userMap = [
"id" : "\(bean.userBean.id)",
"name": bean.profileBean.nickName,
"avatar" : TSRouter.fileCheckUrl + bean.profileBean.avatar]
}
}
// 每次第一次进来的时候 将个人信息带给对方
override func sendMessage(_ messageContent: RCMessageContent!, pushContent: String!) {
if let message = messageContent as? RCTextMessage , let map = userMap {
let data : NSData! = try? JSONEncoder().encode(map) as NSData
let string = NSString(data:data as Data,encoding: String.Encoding.utf8.rawValue)
if let newString = string as String? {
message.extra = newString
super.sendMessage(message, pushContent: pushContent)
userMap = nil
return
}
}
super.sendMessage(messageContent, pushContent: pushContent)
}
}
extension ImDetailController{
// 点击头像
override func didTapCellPortrait(_ userId: String!) {
// TODO
}
// 消息点击处理事件
override func didTapMessageCell(_ model: RCMessageModel!) {
super.didTapMessageCell(model)
if conversationType != RCConversationType.ConversationType_SYSTEM {
return
}
if let extra = (model.content as? RCTextMessage)?.extra {
let messageModel = MessageModel(json: JSON(parseJSON: extra))
//落地页 可能是项目中的任何一个页面 具体实现与项目无关 不再赘述
return
}
}
// 这里可以单独设置 文字分颜色点击等功能 这里是融云比较坑的地方 文档不明确
// 翻了源码才找到这个功能 具体写法参考下RCAttributedLabel 融云这个类 项目暂时没用到 不再赘述
}
至此,融云集成的大部分功能基本实现,按步骤复制集成应该没啥问题, 初学iOS 代码规范不一定合理,欢迎指出不合理地方
PS:下篇文章填坑