1. 准备工作
1.1 集成SDK
- 复制EaseMobSDK到工程,include和lib下只留一个
- 添加依赖库
- other link flag添加-ObjC
2. Mark
2.1 主界面
2.1.1 登录注册
- 新建Sb,搭建UI,实现登录注册功能
- 自动登录(在登录成功后保存用户信息,然后下次启动程序在AppDelegate里判断)
- 自动连接(网络连接和App连接)
2.1.2 添加好友
- 主动添加好友
- 监听好友申请状态
- 显示好友列表
- 好友列表buddyList需要在(自动)登录成功后才有值
- buddyList的数据是从 本地数据库获取
- 如果当前有添加好友请求,环信的SDK内部会往数据库的buddy表添加好友记录
- 如果程序删除或者用户第一次登录,buddyList表是没记录
- 要从服务器获取好友列表记录
- 用户第一次登录后,自动从服务器获取好友列表
- 好友申请被同意后刷新好友列表
- 在“好友添加请求同意”代理里主动调用从服务器获取好友列表的Api给好友列表数据源重新赋值然后刷新表格
- 接收别人的好友申请
- 删除好友
- 监听被删除好友
2.1.3 退出登录
2.2 聊天界面(发送文字)
2.2.1 定制底部工具条
- 搭建UI
- 点击输入框弹出键盘,修改约束上移toolBarView的高度(用了IQKeyboardManager要先禁用,同时直接在Sb中给中间的tableview设置keyboard→DismissOnDrag
2.2.2 cell的排版
1. 接收方
- Label和背景imageView四条边Edges约束设置
- 背景imageView的拉伸
- Label高度随文字多少变化
- numberOfLines = 0
- 宽度约束改为More than
- Label要显示多行文字只添加上面两个autolayout和numberofline是不够的!!!一定还要设置preferredMaxLayoutWidth这个属性!!!那么这是为什么呢?
- 当label文字很多的时候,会发现文字最右边有的地方空余了好多,这就是问题所在
- 在xib中设置Label的约束,什么距离父控件左边10,距离父控件右边10,但是对于文字或者单词,在末尾不够自己显示的时候,会自动换行
- 自动换行之后,问题就来了!本来在预计第3行就能全部显示完文字,由于自动换行,结果第4行才全部显示完,这就导致实际显示的高度比预计的高度更高,所以在屏幕上看到当Label里文字或单词在末尾出现了自动换行时,每个cell最下面就不能完全显示Label的内容
- 所以说需要设置preferredMaxLayoutWidth来告诉系统Label的首选宽度,而且这个 preferredMaxLayoutWidth的优先级高于于autolayout宽度约束。可以这样理解,autolayout只是设置了Label的约束,而preferredMaxLayoutWidth设置了Label和其内部文字的约束
2. 发送方
和接收方cell是两种cell,但是共用一个m文件
3. 计算cell的高度(这里是用的sb)
- 在chatCell里提供一个计算高度的方法
/** 返回cell的高度*/
-(CGFloat)cellHeghit{
//1.重新布局子控件
[self layoutIfNeeded];
return 5 + 10 + self.messageLabel.bounds.size.height + 10 + 5;
}
- 在控制器里定义一个专门用作计算高度的chatCell
/** 计算高度的cell工具对象 */
@property (nonatomic, strong) XMGChatCell *chatCellTool;
- 在viewDidLoad里给这个chatCellTool赋值
// 给计算高度的cell工具对象,这里ReceiverCell也可以写SenderCell,随便哪个都行,因为二者都是存在的
self.chatCellTool = [self.tableView dequeueReusableCellWithIdentifier:ReceiverCell];
- 在返回高度的代理方法里计算出高度
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
// 设置label的数据
#warning 计算高度与前,一定要给messageLabel.text赋值
self.chatCellTool.messageLabel.text = self.dataSources[indexPath.row];
return [self.chatCellTool cellHeghit];
}
2.2.3 发送聊天消息
1. 以换行符监听textView的发送动作
- 改变textView的returnKeyType为UIReturnKeySend
- 发送前要去掉换行符
2. 监听Segue的跳转,传递buddy
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
//往聊天控制器 传递一个 buddy的值
id destVC = segue.destinationViewController;
if ([destVC isKindOfClass:[ChatController class]]) {
//获取点击的行
NSInteger selectedRow = [self.tableView indexPathForSelectedRow].row;
ChatController *chatVc = destVC;
chatVc.buddy = self.buddyList[selectedRow];
}
}
3. 发送消息
#pragma mark - UITextView代理
-(void)textViewDidChange:(UITextView *)textView{
NSLog(@"%@",textView.text);
// 监听Send事件--判断最后的一个字符是不是换行字符
if ([textView.text hasSuffix:@"\n"]) {
NSLog(@"发送操作");
[self sendMessage:textView.text];
// 清空textView的文字
textView.text = nil;
}
}
-(void)sendMessage:(NSString *)text{
//消息 = 消息头 + 消息体
#warning 每一种消息类型对象不同的消息体
// EMTextMessageBody 文本消息体
// EMVoiceMessageBody 录音消息体
// EMVideoMessageBody 视频消息体
// EMLocationMessageBody 位置消息体
// EMImageMessageBody 图片消息体
NSLog(@"要发送给 %@",self.buddy.username);
// return;
// 创建一个聊天文本对象
EMChatText *chatText = [[EMChatText alloc] initWithText:text];
//创建一个文本消息体
EMTextMessageBody *textBody = [[EMTextMessageBody alloc] initWithChatObject:chatText];
//1.创建一个消息对象
EMMessage *msgObj = [[EMMessage alloc] initWithReceiver:self.buddy.username bodies:@[textBody]];
// 2.发送消息
[[EaseMob sharedInstance].chatManager asyncSendMessage:msgObj progress:nil prepare:^(EMMessage *message, EMError *error) {
NSLog(@"准备发送消息");
} onQueue:nil completion:^(EMMessage *message, EMError *error) {
NSLog(@"完成消息发送 %@",error);
} onQueue:nil];
}
2.2.4 加载本地聊天记录
- 在viewDidload里加载本地数据库聊天记录(MessageV1)
-(void)loadLocalChatRecords{
// 要获取本地聊天记录使用 会话对象
EMConversation *conversation = [[EaseMob sharedInstance].chatManager conversationForChatter:self.buddy.username conversationType:eConversationTypeChat];
// 加载与当前聊天用户所有聊天记录
NSArray *messages = [conversation loadAllMessages];
for (id obj in messages) {
NSLog(@"%@",[obj class]);
}
// 添加到数据源
[self.dataSources addObjectsFromArray:messages];
}
- 在cellforrow方法里根据EMMessage的from属性判断是发送方还是接收方的信息
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
//1.先获取消息模型
EMMessage *message = self.dataSources[indexPath.row];
// EMMessage
/* from:xmgtest1 to:xmgtest7 发送方(自己)
* from:xmgtest7 to:xmgtest1 接收方 (好友)
*/
ChatCell *cell = nil;
if ([message.from isEqualToString:self.buddy.username]) {//接收方
cell = [tableView dequeueReusableCellWithIdentifier:ReceiverCell];
}else{//发送方
cell = [tableView dequeueReusableCellWithIdentifier:SenderCell];
}
//显示内容
cell.message = message;
return cell;
}
- 点击发送消息后刷新聊天记录,把消息显示在顶部
// 3.把消息添加到数据源,然后再刷新表格
[self.dataSources addObject:msgObj];
[self.tableView reloadData];
// 4.把消息显示在顶部
[self scrollToBottom];
- 接受消息并显示
2.2.5 完善输入框
- 让输入框textView高度随文字内容变化,注意最后让光标回到原位的技巧
#pragma mark - UITextView代理
-(void)textViewDidChange:(UITextView *)textView{
NSLog(@"contentOffset %@",NSStringFromCGPoint(textView.contentOffset));
// 1.计算TextView的高度,
CGFloat textViewH = 0;
CGFloat minHeight = 33;//textView最小的高度
CGFloat maxHeight = 68;//textView最大的高度
// 获取contentSize的高度
CGFloat contentHeight = textView.contentSize.height;
if (contentHeight < minHeight) {
textViewH = minHeight;
}else if (contentHeight > maxHeight){
textViewH = maxHeight;
}else{
textViewH = contentHeight;
}
// 2.监听Send事件--判断最后的一个字符是不是换行字符
if ([textView.text hasSuffix:@"\n"]) {
NSLog(@"发送操作");
[self sendMessage:textView.text];
// 清空textView的文字
textView.text = nil;
// 发送时,textViewH的高度为33
textViewH = minHeight;
}
// 3.调整整个InputToolBar 高度
self.inputToolBarHegihtConstraint.constant = 6 + 7 + textViewH;
// 加个动画
[UIView animateWithDuration:0.25 animations:^{
[self.view layoutIfNeeded];
}];
// 4.记光标回到原位
#warning 技巧
[textView setContentOffset:CGPointZero animated:YES];
[textView scrollRangeToVisible:textView.selectedRange];
}
- 给输入框加背景图片
2.3 语音
2.3.1 发送语音
- 搭建UI
- 录音键的三种动作
- 按下touchDown
- 抬起touchUpInside
- 从按钮外面松开取消录音touchUpOutSide
- 拖入两个环信的录音库,import文件
- 写发送API
2.3.2 播放语音
- 显示语音UI(这里是用富文本实现的)
- 播放动画
- 这里在真机播放录音的时候崩了,原来是IOS10的权限问题,在info.plist里添加录音的权限。下面把几种常用权限列出来供以后备用
//相机
NSCameraUsageDescription
cameraDesciption
//相册
NSPhotoLibraryUsageDescription
photoLibraryDescription
//通讯录
NSContactsUsageDescription
contactsDesciption
//麦克风
NSMicrophoneUsageDescription
microphoneDesciption
- 滑动的时候停止播放语音
2.3 图片的发送和显示
- 这里真机调用相册也遇到上面的问题,要在info.plist添加属性
- 【注】这里cell重用时需要先移除图片(懒加载必须用strong)
2.4 时间cell
时间的显示
- 时间显示规则
1.同一分中内的消息,只显示一个时间
/*
15:52
msg1 15:52:10
msg2 15:52:08
msg2 15:52:02
*/
2./*今天:时:分 (HH:mm)
*昨天: 昨天 + 时 + 分 (昨天 HH:mm)
*昨天以前:(前天) 年:月:日 时 分(2015-09-26 15:27)
*/
- 数据源里的每条数据都需要根据显示规则判断要不要先插入一个时间,然后在cellforrow里根据数据源类型判断要不要显示时间
时间的计算
- 先实现今天、昨天和以前的计算,然后每次往数据源数组里添加数据都要先进行判断
- 第一步实现后再实现时间显示规则一就非常简单了。因为本来时间就是只精确到分钟,然后规则一也是同一分钟内时间不显示,所以这时候只需要对第一步的时间过滤,进行判断,在当前controller加个时间字符串属性,当前一个数据源的时间和上个数据源的时间进行比较,不一样就添加,一样就保存
- 示例中判断昨天的算法是错的,需要重新写一个
2.5 会话界面
- 显示历史会话记录
- 未读消息(更新别人发来的消息)
- 添加别人发来的消息
- 设置消息已读