1. 前言
之前做过一个项目的聊天是是基于 XMPP 协议,所有的东西都是自己写的,工程量大,而且会出现各种各样得问题,丢失消息的问题,所以新进入一家公司之后重新做一个项目给老板推荐聊天使用第三方的,最后权衡选择了融云即时通讯。
2. 融云聊天的实现
关于在融云上创建自己的应用、集成 SDK、初始化等一些基本的我们在这就不多说了,按照他的教程可以很容易的完成。
①会话列表的实现
会话列表的实现其实也很简单,就是创建一个继承于RCConversationListViewController的控制器,然后重写 init 的方法,这个里边我们需要设定需要显示的会话类型。
-(id)init{ self = [super init]; /** <设置会话列表头像为圆形> **/ [self setConversationAvatarStyle:RC_USER_AVATAR_CYCLE]; if (self) { //设置需要显示哪些类型的会话 [self setDisplayConversationTypes:@[@(ConversationType_PRIVATE), @(ConversationType_DISCUSSION), @(ConversationType_CHATROOM), @(ConversationType_GROUP), @(ConversationType_APPSERVICE), @(ConversationType_SYSTEM)]]; //设置需要将哪些类型的会话在会话列表中聚合显示 [self setCollectionConversationType:@[@(ConversationType_DISCUSSION), @(ConversationType_SYSTEM)]]; } return self; }
然后就是在- (void)viewWillAppear:(BOOL)animated写一些界面将要显示的时候的一些逻辑操作,比如提示新消息的小圆点或者消息数等。
- (void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:animated]; /*更新底部红点状态 点击cell读取消息后 要刷新底部红点*/ [[TJNotification sharedTJNotification]refreshNotificationPrompt]; }
之后再viewDidLoad里边设定一些会话列表界面的一些属性,设定刷新菊花、没有消息时候的背景视图、需要置顶的会话等等
- (void)viewDidLoad { [super viewDidLoad]; /*设置默认属性*/ self.conversationListTableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine; self.conversationListTableView.backgroundColor = [UIColor tj_mainColor]; self.edgesForExtendedLayout = UIRectEdgeNone; UIView *footerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, 150)]; footerView.backgroundColor = [UIColor tj_mainColor]; self.conversationListTableView.tableFooterView = footerView; self.emptyConversationView = self.noMessage; //置顶特定UID 的会话 [[RCIMClient sharedRCIMClient] setConversationToTop:6 targetId:@"tanjie_user_1.0_2017" isTop:YES]; }
然后就是融云自己的一些方法根据我们的功能需要需要我们自己重写,下面我就列举两个比较重要的,如果自己需要这些逻辑的话就可以重写这些方法,
/*! 即将加载列表数据源的回调 @param dataSource 即将加载的列表数据源(元素为RCConversationModel对象) @return 修改后的数据源(元素为RCConversationModel对象) @discussion 您可以在回调中修改、添加、删除数据源的元素来定制显示的内容,会话列表会根据您返回的修改后的数据源进行显示。 数据源中存放的元素为会话Cell的数据模型,即RCConversationModel对象。 */- (NSMutableArray *)willReloadTableData:(NSMutableArray *)dataSource;/*! 即将显示Cell的回调 @param cell 即将显示的Cell @param indexPath 该Cell对应的会话Cell数据模型在数据源中的索引值 @discussion 您可以在此回调中修改Cell的一些显示属性。 */- (void)willDisplayConversationTableCell:(RCConversationBaseCell *)cell atIndexPath:(NSIndexPath *)indexPath;
紧接着我们需要点击会话跳转到会话界面,我们需要重写这个方法。
- (void)onSelectedTableRow:(RCConversationModelType)conversationModelType conversationModel:(RCConversationModel *)model atIndexPath:(NSIndexPath *)indexPath {//我们把我们自己设定的两个客服消息聚合到了一个会话里边,所以这里根据消息类型跳转不同的界面 if (conversationModelType == RC_CONVERSATION_MODEL_TYPE_COLLECTION){ TJChatListViewController *chatListVC = [[TJChatListViewController alloc]init]; NSArray *array = [NSArray arrayWithObject:[NSNumber numberWithInt:model.conversationType]]; [chatListVC setDisplayConversationTypes:array]; [chatListVC setCollectionConversationType:nil]; chatListVC.isEnteredToCollectionViewController = YES; [self.navigationController pushViewController:chatListVC animated:YES]; } else if (model.conversationModelType == ConversationType_PRIVATE){ TJConversationViewController *conversationVC = [[TJConversationViewController alloc]init]; conversationVC.conversationType =model.conversationType; conversationVC.targetId = model.targetId; conversationVC.title = model.conversationTitle; [self.navigationController pushViewController:conversationVC animated:YES];//解决键盘遮挡消息问题 [IQKeyboardManager sharedManager].enable = NO; } }
接着就是我们希望能删除会话列表的某个会话,这时候我们需要重写下面的方法
- (void)didDeleteConversationCell:(RCConversationModel *)model{ [[RCIMClient sharedRCIMClient] clearMessages:ConversationType_PRIVATE targetId:model.targetId]; RCIMClient *rcim = [[RCIMClient alloc]init]; //获取未读消息数 NSUInteger a = [rcim getUnreadCount:@[@(ConversationType_PRIVATE),@(ConversationType_SYSTEM)]]; //把未读消息数赋值给单例,改变未读消息数小红点提示数 [TJMessageCount sharedTJMessageCount].messageCount = a; }
最后如果你们自己的项目需要一些自定义的功能,比如自定义消息列表什么的,或者设置没有消息的时候的占位图等,那就根据自己的逻辑写。
②会话界面的实现
会话界面的实现我们需要写一个继承于RCConversationViewController的控制器,因为为了知道我们是跟谁聊天的,所以我们要暴露一个UID给其他需要聊天的界面使用,到时候把 UID 传到这个界面,如果没有其他的逻辑,就简单的聊天,写到这里这个界面就可以了,但是如果你需要其他的功能操作,这个控制器我们还需要重写其他的一些方法。由于融云提供的功能太多,我就不一一写了,总之点进去RCConversationViewController这个控制器挑选你需要的功能。
/*! 点击Cell中头像的回调 @param userId 点击头像对应的用户ID */- (void)didTapCellPortrait:(NSString *)userId;/*! 长按Cell中头像的回调 @param userId 头像对应的用户ID */- (void)didLongPressCellPortrait:(NSString *)userId;
这里说一下下边的功能面板上添加的东西,我们可以自定义我们需要的功能图标,但是自定义功能的按钮的 tag 值尽量别选1开头的,因为融云的功能按钮的 tag 值都是以1开头的,避免重复
// 在功能面板上插入一个Item,并标记tag,方便区分 [self.pluginBoardView insertItemWithImage:[UIImage imageNamed:@"tj_home_share_commodity_icon"] title:@"文件" tag:201];
下边我们要重写自定义扩展面板的回调方法
- (void)pluginBoardView:(RCPluginBoardView *)pluginBoardView clickedItemWithTag:(NSInteger)tag{ switch (tag) { case 201: { [super pluginBoardView:pluginBoardView clickedItemWithTag:tag];//记得调用super父类的方法· NSLog(@"shipin"); } break; default: [super pluginBoardView:pluginBoardView clickedItemWithTag:tag]; NSLog(@"%ld",(long)pluginBoardView.tag); break; } }
3. 自定义消息类型和消息cell
这个功能的实现,我们需要做些什么工作呢?
第一步
肯定的是我们需要写一个继承与融云消息类RCMessageContent的类,具体怎么写下面详细说
第二步
我们需要写一个继承与融云消息cell的RCMessageCell的类,这个和我们平时 tableView 的 cell 自定义的书写差不多
第三步
我们需要做的工作就是注册一个消息类型(就是我们第一步写的那个自定义的消息类),这个注册的消息类型最好注册在我们专门写的融云 delegate 的分类里边,每次启动 APP 都会走一遍,如果考虑到启动时间,性能问题也可以在发送消息的时候注册,这个看自己的取舍来决定注册的位置
第四步
在会话界面注册一个我们第二步写的消息卡片 cell,到这里基本上自定义消息类型和消息 cell 已经完成了
继承融云消息类RCMessageContent
继承这个类,融云怎么实现聊天的我们都要重写,包括消息的编码和解码,以及包含在消息里边的信息呈现在自定义的消息 cell 上所以我们要继承NSCoding和RCMessageContentView协议,
自定义消息类 .h
#import <RongIMLib/RongIMLib.h>#import <RongIMLib/RCMessageContentView.h>//这个是自定义消息的标识符,和安卓端对接用的#define RCLocalMessageTypeIdentifier @"App:SimpleMsg"@interface TJShopingDetailMessageContent : RCMessageContent<NSCoding,RCMessageContentView>//下面这两个是我们发送消息对应我们需要的键@property(nonatomic,strong)NSString *content;@property(nonatomic, strong) NSString* extra; +(instancetype)messageWithContent:(NSString *)content;@end
自定义消息类 .m
首先就是我们需要判定接收到的和发送的消息是不是我们自定义的消息,然后需要编码还是解码,需要把消息转换成可读的或者可以发送的格式,比如接收到的我们要解码然后转换成 json 类型的;
#import "TJShopingDetailMessageContent.h"@implementation TJShopingDetailMessageContent+(instancetype)messageWithContent:(NSString *)content { TJShopingDetailMessageContent *msg = [[TJShopingDetailMessageContent alloc] init]; if (msg) { msg.content = content; } return msg; }//存储状态和是否计入未读数+(RCMessagePersistent)persistentFlag { //存储并计入未读数 return (MessagePersistent_ISCOUNTED); }#pragma mark – NSCoding protocol methods#define KEY_TXTMSG_CONTENT @"content"#define KEY_TXTMSG_EXTRA @"extra"- (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super init]; if (self) { self.content = [aDecoder decodeObjectForKey:KEY_TXTMSG_CONTENT]; self.extra = [aDecoder decodeObjectForKey:KEY_TXTMSG_EXTRA]; } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self.content forKey:KEY_TXTMSG_CONTENT]; [aCoder encodeObject:self.extra forKey:KEY_TXTMSG_EXTRA]; }#pragma mark – RCMessageCoding delegate methods///将消息内容编码成json-(NSData *)encode { NSMutableDictionary *dataDict=[NSMutableDictionary dictionary]; [dataDict setObject:self.content forKey:@"content"]; if (self.extra) { [dataDict setObject:self.extra forKey:@"extra"]; } NSData *data = [NSJSONSerialization dataWithJSONObject:dataDict options:kNilOptions error:nil]; return data; }//将json解码生成消息内容-(void)decodeWithData:(NSData *)data { __autoreleasing NSError* __error = nil; if (!data) { return; } NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&__error]; NSLog(@"dictionary == %@",dictionary); if ([dictionary objectForKey:@"content"]) { self.content = dictionary[@"content"]; NSLog(@"dictionary1111 == %@",dictionary[@"content"]); self.extra = dictionary[@"extra"]; }else{ NSError *error; NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:kNilOptions error:nil]; NSString *content ; if (!data) { NSLog(@"%@",error); }else{ content = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]; } self.content = content; } }//您定义的消息类型名,需要在各个平台上保持一致,以保证消息互通,别以 RC 开头,以免和融云系统冲突+(NSString *)getObjectName { return RCLocalMessageTypeIdentifier; }//最后一条消息是自定义消息的时候,可以更改在会话列表显示的类型,为了区分消息类型- (NSString *)conversationDigest { NSString *contentStr = @"【商品信息】"; return contentStr; }
这样我们自定义的消息类型已经完成了,不过你还想完善一下就在这几个方法里添加你想要的逻辑代码。
自定义消息cell .h
这个和平时自定义 cell 一样,只不过 model 不用我们创建,融云的就行,但是融云的 cell你要去他官方文档看,看了才能为所欲为的去写,其实和平常 自定义cell不一样的就只有高度问题,他有一个extraHeight高度,就是是Cell根据界面上下文,需要额外显示的高度(比如时间、用户名的高度等),一般Cell的高度应该是内容显示的高度再加上extraHeight的高度。
#import <RongIMKit/RongIMKit.h>@interface TJShopingDetailMessageCell : RCMessageCell@property(nonatomic, strong) UIImageView *bubbleBackgroundView;@property(nonatomic,strong)UIImageView *contentImageView;@property (nonatomic , strong)UILabel *titleNameLabel;@property (nonatomic , strong)UILabel *priceLabel; - (void)setDataModel:(RCMessageModel *)model; - (void)initialize;@end
自定义消息 cell .m
//当应用自定义消息时,必须实现该方法来返回cell的Size+ (CGSize)sizeForMessageModel:(RCMessageModel *)model withCollectionViewWidth:(CGFloat)collectionViewWidth referenceExtraHeight:(CGFloat)extraHeight { //这里我们设定的高度是120,所以加上extraHeight return CGSizeMake(ScreenWidth , 120*ADAPTERWIDTH + extraHeight); }//model 的 set 方法- (void)setDataModel:(RCMessageModel *)model { [super setDataModel:model]; [self setAutoLayout:model]; }//布局我们可以写在这个里边- (void)setAutoLayout:(RCMessageModel *)model{//接收到的消息我们需要拿到需要的数据,赋值到自定义 cell 的控件上 TJShopingDetailMessageContent *shopingDetailContent = (TJShopingDetailMessageContent *)self.model.content; if (shopingDetailContent) { NSData *jsonData = [shopingDetailContent.content dataUsingEncoding:NSUTF8StringEncoding]; NSError *err; NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&err]; NSLog(@"dic == %@",dic); /******************自定义消息 cell 的布局和赋值************/}
注册消息类型和消息 cell
自定义的消息类型我是注册在融云 delegate 的分类里,自定义消息的 cell 我注册在会话控制器里
// 注册自定义测试消息 [[RCIM sharedRCIM] registerMessageType:[TJShopingDetailMessageContent class]];//注册自定义消息Cell [self registerClass:[TJShopingDetailMessageCell class] forMessageClass:[TJShopingDetailMessageContent class]];
差不多到这所有的都完成了,但是当我们和安卓端对接的时候问题出来了,找了好久找到了问题的所在,希望大家不要犯同样的错误,当时我们安卓端发送的消息数据和我们这边解析的数据格式不一样,导致拿不到数据,最后我们重新设置了消息的格式解决了这个问题。
4. 融云使用过程中出现的问题
估计使用融云的过程中会出现很多问题,我来列举几个,头像不显示问题、聊天时键盘遮挡消息内容问题、头像不能及时更新问题、用户名显示成 UID 问题、发送位置界面导航栏颜色问题等等。(太多了想不起来了,等到遇到问题的老哥问到了再说)
先说头像和用户名问题
都知道融云的头像显示有两种方法,一种是发送消息体,打开就行了,但是有一个很大的弊端,就是如果你给一个聊天对象发送消息,他不回复你你是看不到他的头像和用户名的,而且发送消息体里边带有用户信息,对发送消息的速度也会有一定的影响,而且还会浪费用户的流量,一般这种都会用到自定义消息类型上,比如我们上边的发送卡片,可以附带一些我们需要展示的数据内容,所以我们一般头像显示都会用另一种“用户信息代理函数”,这种方法的原理就是,当我们改变头像的时候,上传头像和用户名到融云的 SDK,当我们发送消息的时候,他会去SDK 找看是否有这个用户的头像和用户名,这样是不是很方便。下面我们具体说一下怎么实现。
当时我用了第一种方法,因为简单,一句代码就搞定了,但是出现了对方没有回复的时候,头像显示的是蓝色的融云默认没有头像的图片,用户名显示的是对方用户名的 UID,一大串看着都难受。我就想到了用[self refreshConversationTableViewIfNeeded]这句代码强刷界面,(但是这个方法尽量别用,融云已经使用过这个方法刷新过界面了,使用不当特别消耗性能)你肯定想想不到成功了,但是出现了另一个问题,在那个聊天界面同级的界面都变得特别卡,打印一下数据,能看到一直在刷新,很可怕,所以这种方法肯定不行。
说实在的,融云的官方文档写的很乱,看了一遍又一遍都没找到头绪,就直接找融云的客服,他给我说头像显示要“用户信息代理函数”,然后我才解决了所以的头像问题,下面贴上具体的实现。
在融云 delegate 的分类里遵循这个数据源RCIMUserInfoDataSource,然后实现他的数据源方法,因为我们的用户的身份比较多,所以根据传过来的 UID进行了判断
//数据信息代理方法- (void)getUserInfoWithUserId:(NSString *)userId completion:(void (^)(RCUserInfo *userInfo))completion{ NSString *uid = [TJAccountHelp sharedTJAccountHelp]._id; NSString *head = [TJAccountHelp sharedTJAccountHelp].head; if ([userId isEqualToString:uid]) { RCUserInfo *userInfo = [[RCUserInfo alloc]init]; userInfo.userId = userId; userInfo.name = [TJAccountHelp sharedTJAccountHelp].name; userInfo.portraitUri = head; return completion(userInfo); }else if([userId isEqualToString:@"tanjie_user_1.0_2017"]) { RCUserInfo *otherUser = [[RCUserInfo alloc]init]; otherUser.userId = userId; otherUser.portraitUri = @"https://static.tanjie.shop/tanjie/tanjie.jpg"; otherUser.name = @"探街小弟"; return completion(otherUser); }else if ([userId isEqualToString:@"tanjie_kefu_1"]) { RCUserInfo *otherUser = [[RCUserInfo alloc]init]; otherUser.userId = userId; otherUser.portraitUri = @"https://static.tanjie.shop/tanjie/tanjie.jpg"; otherUser.name = @"客服1"; return completion(otherUser); }else if ([userId isEqualToString:@"tanjie_kefu_2"]) { RCUserInfo *otherUser = [[RCUserInfo alloc]init]; otherUser.userId = userId; otherUser.portraitUri = @"https://static.tanjie.shop/tanjie/tanjie.jpg"; otherUser.name = @"客服2"; return completion(otherUser); }else{ RCUserInfo *otherUser = [[RCUserInfo alloc]init]; otherUser.userId = userId; NSDictionary *dic = [[NSDictionary alloc]init]; dic = @{@"userid":userId}; [TJNetRequest postDictRequestWithURL:TJ_User_FindHeadName_URl parameters:dic success:^(id response) { @try { if ([self tj_requestSuccess:response]) { NSDictionary *dict = [[NSDictionary alloc]init]; dict = response[@"obj"]; if ([dict[@"head"] isEqual:[NSNull null]]) { otherUser.portraitUri = @"https://static.tanjie.shop/tanjie/defualt.jpg"; }else{ otherUser.portraitUri = dict[@"head"]; } otherUser.name = dict[@"name"]; // 解决异步请求数据第一次不显示头像用户名问题 completion(otherUser); } } @catch (NSException *exception) { } @finally { } } failure:^(NSError *error) { }]; } }
做到这还没完呢,如果你要是请求的服务器数据,一定要注意网络请求,如果用的你们封装好的请求方式,大多都是异步请求,就会出现第一次进入会话列表界面的时候还是不显示头像和用户名,再次进入才显示,所以具体怎么显示你自己处理。大概到这里头像和用户名问题就解决了。
键盘问题
键盘问题其实很简单,一句代码就行了,如果你使用了第三方库,在会话界面添加一句代码
//解决键盘遮挡消息内容[[IQKeyboardManager sharedManager] setEnable:NO];/*解决点击系统键盘的语音按钮,导致输入工具栏被遮挡*/- (void)keyboardWillShowNotification:(NSNotification *)notification { if(!self.chatSessionInputBarControl.inputTextView.isFirstResponder) { [self.chatSessionInputBarControl.inputTextView becomeFirstResponder]; } }
发送位置界面导航栏颜色问题
在融云 delegate 的分类里边加一句代码
[[UINavigationBar appearance] setBarTintColor:[UIColor tj_navBgColor]];
5. 结语
以前特别喜欢过年,回来家里人一块聊聊天,打打牌,但是今年我特别讨厌过年,可能年龄大了,不是被逼婚就是在我面前说一些周围的孩子考了什么公务员,什么事业单位啦,听着就难受,自己不喜欢那种工作状态才选择了技术类的工作,跟他们说不下去了,就自己待着逛一下论坛,写一下这一年因为工作忙欠下的文章。就这样一年过去了。有喜欢的可以点个喜欢,不管有什么问题都可以留言问我,能帮忙的肯定帮忙。