2016年03月07日更新
已经重新提交了Githup,现在项目可以正常使用
首先附上前两篇的链接
LeanCloud实时通信模块iOS端快速接入指南(一)
LeanCloud实时通信模块iOS端快速接入指南(二)
上一节我们已经实现了一个相对完整的聊天,今天我们来定制我们聊天里一些细节功能。
怎么定制
说是定制,其实就是对LeanChatLib写好的代码进行改造,我这里列出几项项目基本都会用到的改造,照着做就能用。如果你有时间的话,阅读LeanChatLib的代码并搞懂它的逻辑,你几乎可以进行任意的改造。
准备工作
上次的代码有一些小BUG,我们先修复一下,原因我已经更新到上一篇的开头
我们把『登陆』按钮点击事件改成如下代码
- (IBAction)loginBtnClicked:(id)sender {
[[CDChatManager manager]closeWithCallback:^(BOOL succeeded, NSError *error) {
[[CDChatManager manager]openWithClientId:_textField.text callback:^(BOOL succeeded, NSError *error) {
ChatListViewController * chatList = [[ChatListViewController alloc]init];
[self.navigationController pushViewController:chatList animated:YES];
}];
}];
}
真正的准备工作
我们继续使用我们在第二篇创建的工程
在这个工程里,我们的ChatListViewController继承了CDChatListVC,但是我们的很多改造并不是重写方法,而是修改方法中的部分逻辑,所以,为了方便,我们把CDChatListVC中除了ViewDidLoad外其他的代码全都拷贝过来。
- 别忘了那些头文件和成员属性
而对于聊天界面我们需要做的界面方面的改动很少,关于业务逻辑方面的东西CDChatRoomVC提供了足够的接口,所以不用做代码的修改。
完成后,command+B 编译一下,没有问题之后我们开始改造工作
解决警告
身为一个强迫症患者,看见警告总是要想办法去掉
代码拷贝完之后CDChatListVC会有两个警告
第一条是因为我们在IOS 9之后AlertView以及被废弃,建议使用的是UIAlertController,因此我们定位到这个警告,把 -(BOOL) filterError:(NSError *)error{}
方法写成这样
- (BOOL)filterError:(NSError *)error {
if (error) {
UIAlertController * alert = [UIAlertController alertControllerWithTitle:nil message:[NSString stringWithFormat:@"%@", error] preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
return NO;
}
return YES;
}
第二条警告是告诉我们把一个将一个可变数组类型的指针指向了不可变数组
定位到错误,将self.conversations = conversations
改成self.conversations.array = conversations
即可
开始改造
不接受/显示某人消息(黑名单)
之前我们说过,当第一次进入或手动刷新聊天界面时,会拉取最近的聊天,然后刷新TableView。这里调用的就是这个 - (void)refresh
方法,其中有这么两句
这两句的意思大家一眼就能看出来,就是把从服务器获取到的会话数组赋值给TableView的数据源,然后刷新TableView。因此,我们在这里做个手脚,把要屏蔽的人从数组里剔除就可以。
比如我们要屏蔽id为79或78的用户,那么我们把这两句替换为如下代码
NSMutableArray * array = [[NSMutableArray alloc]initWithArray:conversations];
//将id为"78"或"79"的用户聊天从最近聊天移除
for (AVIMConversation * conver in conversations) {
for (id member in conver.members) {
if ([member isEqual:@"78"] || [member isEqual:@"79"]) {
[array removeObject:conver];
}
}
}
self.conversations= array;
[self.tableView reloadData];
运行项目,发起和78或者79的聊天,然后再返回,大功告成
设置静音
设置静音是对会话的muted
属性做操作,设置静音后,当该会话有新消息时将不会推送(如果你设置了推送的话),并且角标会显示一个小红点,而不是具体数字。
其实就是微信的免打扰功能。
具体做法为在任何一个能够获取到conversation的地方调用
[conver muteWithCallback:^(BOOL succeeded, NSError *error) {
}];
取消静音这么写
[conver unmuteWithCallback:^(BOOL succeeded, NSError *error) {
}];
设置静音后的效果如下
标签栏显示未读消息数量
想在标签栏显示未读消息数,得先知道未读消息的数目,我们继续看这个refresh
方法,发现这么几句
在回调方法中已经返回了未读消息的总数,并且CDChatListDelegate也提供了相应的代理方法。
之前我们将ChatListViewController作为自身的代理,因此我们实现这个代理方法即可。我们给ChatListViewController添加如下方法
- (void)setBadgeWithTotalUnreadCount:(NSInteger)totalUnreadCount
{
if (totalUnreadCount == 0) {
[self.navigationController.tabBarItem setBadgeValue:nil];
}
else {
[self.navigationController.tabBarItem setBadgeValue:[NSString stringWithFormat:@"%ld",(long)totalUnreadCount]];
}
}
- 之前有人问过我怎么给标签栏加这个数字角标,网上也有各种定制方法,但其实tabBarItem自身就有一个
setBageVaule:
的方法用来设置角标
头像切圆角、移动角标位置
为什么要把这两项放在这里呢?
一来是头像圆角显示是现在的主流,二来默认的角标是显示在头像右上角的,大家都知道图片切圆角无非是对UIImageView的layer做剪裁,但是因为它把角标直接加在了头像上作为子控件,这样的话角标就无法显示,因此我们还需要把角标移个位置。
LeanChatLib在这里用的是叫做LZConversationCell的类,但是这里我们不再选择使用它,因为它创建控件的方式还蛮奇怪的,继承改造很麻烦。再加上在TableView返回cell采用的策略也并不是很好,因此这里我们选择重新创建自己的cell。
首先是创建带Xib的UITableViewCell
然后在cell中完成布局(不一定非要按照LeanChatLib的来)
然后把xib中的控件和代码关联起来,这里注意把最好把控件名字起的和LZConversationCell一样,这样一会可以省很大力气。
- 这里解释一下littleBadgeView和BadgeView,之前说过如何设置会话的静音,当新消息来了之后,如果会话是静音的,那么就显示littleBadgeView,也就是小红点,如果非静音,那么就显示BadgeView,也就是角标。LeanChatLib把他们两个的位置设置的一样,但是我们自己做的时候是可以随意安排它的位置的。我这里是跟QQ一样放在了cell的右侧居中位置。
引入头文件
#import <JSBadgeView>
然后添加
@property (nonatomic, strong) JSBadgeView *badgeView;
,
我们一会在代码中处理它
再次引入头文件
#import <AVIMConversation.h>
再添加一条成员属性
@property(nonatomic,strong)AVIMConversation * conversation;
最后头文件中是这样的
实现ConversationCell.m
的awakeFromNib
方法
- (void)awakeFromNib {
// Initialization code
self.avatarImageView.layer.cornerRadius = 22.5;
self.avatarImageView.clipsToBounds = YES;
self.litteBadgeView.hidden = YES;
_badgeView = [[JSBadgeView alloc]initWithParentView:self.timestampLabel alignment:JSBadgeViewAlignmentBottomCenter];
}
然后重写conversation
的set方法
将ChatListViewController中返回cell的代理方法中的图中框起来的内容剪切(没有错,就是剪切)过来,并把 cell. 都改成 self. 。
并添加以下头文件
#import <AVIMConversation+Custom.h>
#import <CDChatManager.h>
#import <UIView+XHRemoteImage.h>
#import <CDMessageHelper.h>
#import <NSDate+DateTools.h>
- 到了这一步在ChatListViewController中对应的头文件已经可以删除了,而且你会发现CDChatListVC引用了两次
UIView+XHRemoteImage.h
,虽然没啥影响,但看来LeanCloud的程序员也蛮粗心的....
set方法中的内容
- (void)setConversation:(AVIMConversation *)conversation
{
if (conversation.type == CDConvTypeSingle) {
id <CDUserModel> user = [[CDChatManager manager].userDelegate getUserById:conversation.otherId];
self.nameLabel.text = user.username;
[self.avatarImageView setImageWithURL:[NSURL URLWithString:user.avatarUrl]];
}
else {
[self.avatarImageView setImage:conversation.icon];
self.nameLabel.text = conversation.displayName;
}
if (conversation.lastMessage) {
self.messageTextLabel.attributedText = [[CDMessageHelper helper] attributedStringWithMessage:conversation.lastMessage conversation:conversation];
self.timestampLabel.text = [[NSDate dateWithTimeIntervalSince1970:conversation.lastMessage.sendTimestamp / 1000] timeAgoSinceNow];
}
if (conversation.unreadCount > 0) {
if (conversation.muted) {
self.litteBadgeView.hidden = NO;
} else {
self.badgeView.badgeText = [NSString stringWithFormat:@"%ld", conversation.unreadCount];
}
}
}
然后在.h添加返回cell的类方法并在.m实现,代码如下
+ (instancetype)cellForRowWithTableView:(UITableView *)tableView
{
NSString * className = NSStringFromClass([self class]);
[tableView registerNib:[UINib nibWithNibName:className bundle:nil] forCellReuseIdentifier:className];
return [tableView dequeueReusableCellWithIdentifier:className];
}
最后一步,在ChatViewController中引入我们写好的cell
然后在返回cell的代理方法中这么写
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
ConversationCell *cell = [ConversationCell cellForRowWithTableView:tableView];
AVIMConversation *conversation = [self.conversations objectAtIndex:indexPath.row];
cell.conversation = conversation;
if ([self.chatListDelegate respondsToSelector:@selector(configureCell:atIndexPath:withConversation:)]) {
[self.chatListDelegate configureCell:cell atIndexPath:indexPath withConversation:conversation];
}
return cell;
}
- 这里回报一个警告说类型不正确,是因为LeanChatLib把参数类型写死写成了LZConversationCell,但是没关系,这个代理方法依然可以正常使用..
现在运行项目,查看效果
图片切成了圆角(我这个图片边角都是白色所以不显眼。。。),角标也移到了右侧
同时我们还整理了一下返回cell的代码,让结构看起来更好一点
- 这里我们花了很大力气自己写了一个Cell用来展示最近会话,其实聪明的同学已经想到了,这个conversation其实就是一个数据模型嘛,因此如果你愿意,conversation中的任何属性你都可以以各种方式展现,不必拘泥于原有的形式。这样就实现了最近消息列表的自定制
烦人的状态栏
LeanCloud目前的连接状态我还没有搞的很明白,总之如果你长时间没有活动或程序处于后台一段时间,就会和服务器断开连接,如果你又有了刷新或者发消息的操作它就会重新连接。但是,这都不是关键,关键是它那个状态栏是作为TableView的HeaderView存在的,因此如果你在HeaderView上加了其它控件会被这个状态栏覆盖掉。另外,在我看来,一个聊天动不动就显示断线对用户体验也不友好,因此我们要想办法干掉这个状态栏。
其实方法很简单,我们只要在ChatViewController中重写updateStatusView
方法,然后啥也不做就行了。就是这么简单!
解决线程阻塞问题
之前说过,聊天列表中的头像昵称等信息是通过CDChatManager的代理方法返回一个user对象获取的,一般在项目中我们都会返回自己后台保存的昵称和头像地址,但是LeanChatLib这里是这么获取User的
- 这句话现在已经被移到了我们刚才自定义的ConversationCell的SetConversation中
那么在UserFactory中我们就要写一个网络访问请求来返回,不管你写的是NSSURLSession还是NSURLConnect,如果网络环境不好的话,就有可能阻塞线程。因此这里我们需要做一点处理,让它去异步获取User。我们使用最简答的方法,利用NSOperationQueue来实现
首先给Cell添加NSOperationQueue
@property(nonatomic,strong)NSOperationQueue * operationQueue;
在从xib加载cell的时候将它初始化
- (void)awakeFromNib {
.....原来的代码
_operationQueue = [[NSOperationQueue alloc]init];
}
然后把上面截图里的几句话改成这样
if (conversation.type == CDConvTypeSingle) {
[self.operationQueue addOperationWithBlock:^{
id <CDUserModel> user = [[CDChatManager manager].userDelegate getUserById:conversation.otherId];
self.nameLabel.text = user.username;
[self.avatarImageView setImageWithURL:[NSURL URLWithString:user.avatarUrl]];
}];
}
然后运行代码,看不出什么差别。但是在你最近聊天列表内容比较多或者网络状况不好的时候,不会出现卡住不动的情况。
为聊天室添加功能
之前说过,聊天室的界面其实没有什么要改动的地方,我们要做的无非就是点击头像查看对方详细信息,点击消息或长按消息出现触发相应事件。对于这些操作,LeanChatLib提供的各种代理方法基本足够了。各种代理方法如下图所示,红框框起来的是经常用到的。
举个例子,我们在ChatRoomViewController中实现点击头像的代理方法如下
- (void)didSelectedAvatorOnMessage:(id<XHMessageModel>)message atIndexPath:(NSIndexPath *)indexPath
{
NSLog(@"sender is %@",message.sender);
}
可以看到我们打印出了发送者的id,这样我们就可以根据这个id跳转到对应用户的详情界面去,是不是很简单?
其它的代理方法大家可以一个一个去试,这里就不在赘述
到了这里,LeanCloud即时通讯模块在iOS端的基本应用和改造方法已经都教给大家了。为了能够比较简单的接入IM功能,我们大量使用或继承了LeanChatLib中提供的类,但是LeanCloud的清晰的后台业务逻辑和前台提供的丰富的接口才是我认为它最吸引人的地方,有兴趣的同学可以自己研究研究。
这篇的代码在这里[https://github.com/RunningChicken-K/LeanCloudDemo_02]
- 如果我的文章对您有帮助,请点赞或评论
- 如有不对,欢迎指正
- 我们下篇文章见