版权声明:本文为博主原创文章,未经博主允许不得转载。
前言
公司最近要求做即时通讯, 直接用了三方环信了,今天和大家谈谈关于 我做环信集成的过程和坑点,有什么不足的地方,还望大家多多指正
与环信V2.0的区别
既然要使用三方环信,第一步当然是下载官方demo了,在这里我用的版本是环信V3.3.2 , 通过查看官方文档我们不难发现, 相比于之前的环信2.0, 环信3.0 中的核心类为 EMClient 类,通过 EMClient 类可以获取到 chatManagergroupManager、contactManager、roomManager对象。原来 2.0 版本的 SDK 很多方法提供了同步、异步回调、异步(block)三种方法,3.0 版只提供同步方法(async开头的方法为异步方法)
** 我们只需要知道 2.0版本 [EaseMob shareInstance] → 3.0 版本 [EMClient sharedClient] **
大家可以根据不同的需求选择不同的模块
- EMClient: 是 SDK 的入口,主要完成登录、退出、连接管理等功能。也是获取其他模块的入口。
- EMChatManager: 管理消息的收发,完成会话管理等功能。
- EMContactManager: 负责好友的添加删除,黑名单的管理。
- EMGroupManager: 负责群组的管理,创建、删除群组,管理群组成员等功能。
- EMChatroomManager: 负责聊天室的管理。
准备工作
- 注册环信开发者账号并创建后台应用
- 制作并上传推送证书来实现离线推送功能
- 导入SDK,这里推荐使用CocoaPods进行导入,其中有两个版本供大家选择:HyphenateLite 和 Hyphenate 其中后者包含了实时语音
这里我们就不过多阐述了,在这里附上官方的SDK集成网址供大家参考
集成iOS SDK前准备工作
iOS的SDK导入
初始化SDK,以及登录,注册,自动登录,退出登录
*在.pch文件中我们引用 #import <Hyphenate/Hyphenate.h> *
在AppDelegate.m中:
//1.初始化SDK
//NSLog(@"环信做自动登录时沙盒路径%@",NSHomeDirectory());
//AppKey:注册的AppKey,详细见下面注释。
//apnsCertName:推送证书名(不需要加后缀),详细见下面注释。
EMOptions *options = [EMOptions optionsWithAppkey:HUANXIN_APPKEY];
// options.apnsCertName = @"istore_dev";
EMError *error = [[EMClient sharedClient] initializeSDKWithOptions:options];
if (!error) {
NSLog(@"环信初始化成功");
}
在登录页面LoginViewController.m中:
//因为设置了自动登录模式,所以登录之前要注销之前的用户,否则重复登录会抛出异常
EMError *error1 = [[EMClient sharedClient] logout:YES];
if (!error1) {
NSLog(@"退出之前的用户成功");
}
[[EMClient sharedClient] loginWithUsername:_userTextField.text password:_passTextField.text completion:^(NSString *aUsername, EMError *aError){
if (!aError) {
kSetLogin(YES);
NSLog(@"登陆成功,用户名为:%@",aUsername);
// 添加菊花 [custom showWaitView:@"登录中..." byView:self.view completion:^{
// 设置自动登录
[EMClient sharedClient].options.isAutoLogin = YES;
// }];
} else {
NSLog(@"登陆失败%d",aError.code); //这里可以通过EMError这个类,去查看登录失败的原因
}
}];
在注册页面RegisterViewController.m中:
//如果注册不成功,需要去环信官网切换注册模式为开放注册,而不是授权注册
EMError *error = [[EMClient sharedClient] registerWithUsername:_userTextField.text password:_passTextField.text];
if (error == nil) {
NSLog(@"注册成功");
kSetLogin(YES);
//这里是注册的时候在调用登录方法, 让其登录一次,只有这样下次才能自动登录,只设置自动登录的Boll值是不行的
//也就是说这里的逻辑是一旦让用户注册,如果注册成功直接跳转到我的页面,并设置下次自动登录,并不是注册完成后回到登录页面
[[EMClient sharedClient] loginWithUsername:_userTextField.text password:_passTextField.text completion:^(NSString *aUsername, EMError *aError) {
[EMClient sharedClient].options.isAutoLogin = YES;
}];
MineViewController *mineVC = [MineViewController new];
mineVC.hidesBottomBarWhenPushed = YES;
for (UIViewController *vc in self.navigationController.viewControllers) {
if ([vc isKindOfClass:[MineViewController class]]) {
[self.navigationController popToViewController:vc animated:YES];
}
}
}else{
NSLog(@"注册失败%d",error.code);
}
设置自动登录的代理,以及实现逻辑,在AppDelegate.m中:
//2.监听自动登录的状态
//设置代理
[[EMClient sharedClient] addDelegate:self delegateQueue:nil];
//3.如果登录过,直接来到主界面
BOOL isAutoLogin = [EMClient sharedClient].options.isAutoLogin;
jLog(@"登录状态为:%d",isAutoLogin);
if (isAutoLogin == YES) {
self.window.rootViewController = [BaseTabBarController new];
}else{
//部分APP这里就是返回登录页面, 这里就不做操作了
NSLog(@"环信自动登录失败,或者是没有登陆过");
}
需要注意的是:添加代理一定不要忘了移除代理,这个暂且算一个小小的注意点
//移除代理, 因为这里是多播机制
- (void)dealloc {
[[EMClient sharedClient] removeDelegate:self];
}
//自动登录的回调
- (void)autoLoginDidCompleteWithError:(EMError *)aError{
if (!aError) {
NSLog(@"自动登录成功");
[CustomView alertMessage:@"环信自动登录成功" view:self.window];
}else{
NSLog(@"自动登录失败%d",aError.code);
}
}
/**
环信 监听网络状态(重连)
1.登录成功后,手机无法上网时
2.登录成功后,网络状态变化时
aConnectionState:当前状态
*/
- (void)didConnectionStateChanged:(EMConnectionState)aConnectionState{
if (aConnectionState == EMConnectionConnected) {
NSLog(@"网络连接成功");
}else{
NSLog(@"网络断开");
//监听网络状态(这里通知的目地是检测到如果没网络的情况下,修改Navigation.title的值)
[[NSNotificationCenter defaultCenter] postNotificationName:
AFNetworkingReachabilityDidChangeNotification object:nil];
}
}
/*!
* 重连
* 有以下几种情况,会引起该方法的调用:
* 1. 登录成功后,手机无法上网时,会调用该回调
* 2. 登录成功后,网络状态变化时,会调用该回调
*/
- (void)connectionStateDidChange:(EMConnectionState)aConnectionState{
NSLog(@"断线重连不需要其他操作%u",aConnectionState);
}
//APP进入后台
- (void)applicationDidEnterBackground:(UIApplication *)application {
[[EMClient sharedClient] applicationDidEnterBackground:application];
}
//APP将要从后台返回
- (void)applicationWillEnterForeground:(UIApplication *)application {
[[EMClient sharedClient] applicationWillEnterForeground:application];
}
最后是退出登录:
- (void)quitLogin:(UIButton *)button {
custom = [CustomView new];
if (LOGIN) {
[self alertWithTitle:nil message:@"是否确定退出登录?" actionATitle:@"确定" actionAHandler:^(UIAlertAction *action) {
[UserInfoClass clearAllInfo];
[UserInfoClass printAllInfo];
NSLog(@"%@",[NSThread currentThread]);
//退出登录
[[CustomView new] showWaitView:@"退出登录成功" byView:self.view completion:^{
[[EMClient sharedClient] logout:YES completion:^(EMError *aError) {
if (!aError) {
NSLog(@"退出环信登录成功");
}else{
NSLog(@"退出环信登录失败,%u",aError.code);
}
}];
[self.navigationController popViewControllerAnimated:YES];
}];
} actionBTitle:@"取消" actionBHandler:nil totalCompletion:nil];
} else {
[custom showAlertView:@"您尚未登录" byView:self.view completion:nil];
}
}
进行到这里以后,相信大家就能实现简单的登录,注册以及自动登录了,是不是也比较简单呢,接下来简单说一下在登录,注册过程中遇到的问题。
-
引用头文件的时候报错出现:Hyphenate/EMSDK.h’ file no found
解决方法: 换下引用#import <HyphenateLite/HyphenateLite.h>
或者#import <Hyphenate/Hyphenate.h>
如果此方法不行, 可以试试选中你的项目中的Pods -> EaseUI->Build Phases->Link Binary With Libraries ,点➕->Add Other ,找到工程里面,Pods里面的Hyphenate文件夹下面的Hyphenate.framework 点击open,重新编译就好了
-
真机上登录,注册没有效果
解决方法: 点击工程名进入工程设置 -> BuildSettings -> 搜索bitcode -> 将Enable Bitcode设置为NO -
集成动态库上传AppStore出现问题, 打包上线时报错
ERROR ITMS-90087: "Unsupported Architectures. The executable for xiantaiApp.app/Frameworks/Hyphenate.framework contains unsupported architectures '[x86_64, i386]'."
解决方法: 环信:由于 iOS 编译的特殊性,为了方便开发者使用,我们将 i386 x86_64 armv7 arm64 几个平台都合并到了一起,所以使用动态库上传appstore时需要将i386 x86_64两个平台删除后,才能正常提交审核
在SDK当前路径下执行以下命令删除i386 x86_64两个平台
iOS的SDK导入中有详细地说明,拿实时音视频版本版本为例 : 执行完以上命令如图所示
删除i386、x86_64平台后,SDK会无法支持模拟器编译,只需要在上传AppStore时在进行删除,上传后,替换为删除前的SDK,建议先分别把i386、x86_64、arm64、armv7各平台的包拆分到本地,上传App Store时合并arm64、armv7平台,并移入Hyphenate.framework内。上传后,重新把各平台包合并移入动态库 -
依旧是打包错误: ERROR ITMS-90535: "Unexpected CFBundleExecutable Key. 。。。。。。 consider contacting the developer of the framework for an update to address this issue."
解决方法: 从EaseUIResource.bundle中找到info.plist删掉CFBundleExecutable,或者整个info.plist删掉
接下来我们说一下,会话聊天部分和会话列表的两个部分
这里用到的是EaseUI ,它封装了 IM 功能常用的控件(如聊天会话、会话列表、联系人列表)
集成EaseUI
请戳这里查看 → EaseUI使用指南
在这里集成EaseUI的时候,有两种方法:
- 使用cocoapods导入 pod 'EaseUI', :git => 'https://github.com/easemob/easeui-ios-hyphenate-cocoapods.git', :tag => '3.3.2'(这里我推荐使用第一种,比较省事,简单)
- 手动导入文件直接将EaseUI拖入已经集成SDK的项目中(注意: 由于EaseUI中有几个常用的第三方库 MJRefresh SDWebImage MBProgressHUD。这会跟自己项目中的冲突。)
我们先来看看使用第一种方法集成时候的过程和遇到的坑点:
坑点1: 使用cocoaPods时候,出现了报错的信息,发现无法将环信的EaseUI导入。
这时候我们跟随提示的指令进行更新pods就可以了,主要是pod 问题 本地仓库太旧了, 终端执行pod repo update, 之后在pod search 'Hyphenate' 如果可以找到3.3.0版本, 就可以下载了 podfile 里面 platform 要指定8.0
在导入完成以后,在.pch文件中引用了#import <EaseUI/EaseUI.h>,编译,恩,居然没有报错,看来可以进行下一步了
直接在AppDelegate.m中初始化EaseUI:
[[EaseSDKHelper shareHelper] hyphenateApplication:application
didFinishLaunchingWithOptions:launchOptions
appkey:HUANXIN_APPKEY
apnsCertName:nil
otherConfig:@{kSDKConfigEnableConsoleLogger:[NSNumber numberWithBool:YES]}];
这时,当我满怀信心跑起来了工程,纳尼??不能自动登录了,每次必须退出登录以后,再登录一次以后才能实现自动登录,然后当我第二次运行工程的时候发现自动登录又失效了,什么鬼?!
坑点2: 直接登录不能发送消息, 必须自动登录以后才能发送接收,自动登录大部分时候会走失败的回调
最后依靠万能的环信客服人员提供了技术支持,不得不说环信的客服还是很给力的
原来是使用pods导入了两个版本的SDK,使用pods导入的同学们一定要注意这个问题啊,不要重复导入,不然会出现许多未知的bug,
接下来我们看一下第二种方法:手动导入EaseUI
-
首先我们根据下载好的环信demo中的文件拖入到工程中,
如果要是集成红包功能,就加上RedacketSDK
- 把demo中的pch文件 拷贝到自己的pch文件中,并且在自己所有的pch文件的头和尾添加
#ifdef __OBJC__
//
#endif
-
编译后,工程会出现如下错误:
这个是因为用到了UIKit里的类,但是只导入了Foundation框架,这个错误在其他类里也会出现,我们可以手动修改Founfation为UIKit,但是我不建议这么做,第一这个做法的工程量比较大, 在其他类里面也要导入,二,不利于移植,当以后环信更新的时候我们还是需要做同样的操作,这里我的做法的创建一个pch文件,在pch文件里面导入UIKit。
解决办法:建一个PCH文件在里面添加如下代码:
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#define NSEaseLocalizedString(key, comment) [[NSBundle bundleWithURL:[[NSBundle mainBundle] URLForResource:@"EaseUIResource" withExtension:@"bundle"]] localizedStringForKey:(key) value:@"" table:nil]
#endif
这里需要注意一定要加入--OBJC --,不然可能会报NSObjcRunTime的错误
4.环信内部集成的MBProgressHUD SDWebImage MJRefresh 与我们工程中集成的这几个第三方库发生冲突!
解决方法:删掉工程中自己集成的这些第三方库,或者删除环信EaseUI 里面的这些第三方库!
需要注意的是:如果删除的是环信集成的第三方库!由于环信在集成的第三方库中加了EM前缀! 记得删掉EaseUI 中使用方法的前缀,不然会报错!
如果集成的是不包含实时音视频的SDK , 手动导入EaseUI的话 , 那么此时还会报Hyphenate/EMSDK.h’ file no found
这时需要把 #import <Hyphenate/Hyphenate.h>注释掉,然后把报错地方的Hyphenate换成HyphenateLite就可以了,和上面提到的第一点是一样的
到这里以后,应该没有什么问题,编译如果成功的话,那么恭喜你了
至此,我们就导入了EaseUI并在appDelegate.m中初始化了EaseUI,接下来我们就先来完善聊天的页面
聊天页面部分
EaseUI集成应用其实简单很多很多,里面也封装了关于头像昵称的设置,所需要做的只是把代理方法实现,前提是你的聊天页面等都是继承EaseUI里面的相关的类去做的。
这里给大家推荐环信官方论坛的一个快速集成聊天的网址:IOS快速集成环信IM - 基于官方的Demo优化,5分钟集成环信IM功能
由于环信官方只是通过用户名的id进行会话,所以不是好友也可以进行聊天,我们先做一个简单的单聊页面,如图 (PS:用户头像环信并不进行存储,所以我们后期实现代理方法进行处理就可以了)
首先我们创建一个ChatViewController类并继承于EaseMessageViewController
在ChatViewController.m中:
@interface ChatViewController ()
<
UIAlertViewDelegate,
EaseMessageViewControllerDelegate,
EaseMessageViewControllerDataSource,
EMClientDelegate,
UIImagePickerControllerDelegate
>
{
UIMenuItem *_copyMenuItem;
UIMenuItem *_deleteMenuItem;
UIMenuItem *_transpondMenuItem;
}
@property (nonatomic) BOOL isPlayingAudio;
@property (nonatomic) NSMutableDictionary *emotionDic; //表情
@end
在ViewDidLoad的方法中:我们修改环信的一些设置,让他更符合我们的开发需求
- (void)viewDidLoad {
[super viewDidLoad];
self.showRefreshHeader = YES;
self.delegate = self;
self.dataSource = self;
if ([[DeviceInfo SystemVersion] floatValue] >= 7.0) {
self.edgesForExtendedLayout = UIRectEdgeNone;
}
self.tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
//修改聊天界面的颜色
// self.view.backgroundColor = [UIColor colorWithString:@"#f8f8f8"];
//自定义气泡
[[EaseBaseMessageCell appearance] setSendBubbleBackgroundImage:[[UIImage imageNamed:@"右气泡"] stretchableImageWithLeftCapWidth:5 topCapHeight:35]];
[[EaseBaseMessageCell appearance] setRecvBubbleBackgroundImage:[[UIImage imageNamed:@"左气泡"] stretchableImageWithLeftCapWidth:35 topCapHeight:35]];
//设置头像圆角
[[EaseBaseMessageCell appearance] setAvatarSize:40.f];
[[EaseBaseMessageCell appearance] setAvatarCornerRadius:20.f];
//隐藏对话时的昵称
[EaseBaseMessageCell appearance].messageNameIsHidden = YES;
//修改字体高度,这样在隐藏昵称的时候,可以让气泡对齐
[EaseBaseMessageCell appearance].messageNameHeight = 10;
//修改发送图片,定位,等的所在的View的颜色...
[[EaseChatBarMoreView appearance] setMoreViewBackgroundColor:[UIColor colorWithRed:240 / 255.0 green:242 / 255.0 blue:247 / 255.0 alpha:1.0]];
// [[EaseChatBarMoreView appearance] setMoreViewBackgroundColor:[UIColor colorWithString:@"#0a0a0a"]];
//删除功能模块中的实时通话
[self.chatBarMoreView removeItematIndex:3];
//删除功能模块中的录制视频(注意:删除通话以后,视频的索引变成了3,所以这里还是3哦)
[self.chatBarMoreView removeItematIndex:3];
//更改功能模块中的图片和文字
[self.chatBarMoreView updateItemWithImage:[UIImage imageNamed:@"information_photo"] highlightedImage:[UIImage imageNamed:@"information_photo_hl"] title:@"照片" atIndex:0];
[self.chatBarMoreView updateItemWithImage:[UIImage imageNamed:@"information_location"] highlightedImage:[UIImage imageNamed:@"information_location_hl"] title:@"位置" atIndex:1];
[self.chatBarMoreView updateItemWithImage:[UIImage imageNamed:@"information_photograph"] highlightedImage:[UIImage imageNamed:@"information_photograph_hl"] title:@"拍摄" atIndex:2];
//设置按住说话的图片数组
// NSArray *arr = @[@"information_voice_one",@"information_voice_two",@"information_voice_three",@"information_voice_four",@"information_voice_five",kDefaultUserHeadImage];
// [self.recordView setVoiceMessageAnimationImages:arr];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(deleteAllMessages:) name:KNOTIFICATIONNAME_DELETEALLMESSAGE object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(exitChat) name:@"ExitGroup" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(insertCallMessage:) name:@"insertCallMessage" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleCallNotification:) name:@"callOutWithChatter" object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleCallNotification:) name:@"callControllerClose" object:nil];
//通过会话管理者获取已收发消息 (bug:会话列表已经调用了刷新,如果继续调用的话会出现消息重复的现象)
// [self tableViewDidTriggerHeaderRefresh];
//处理表情崩溃
// EaseEmotionManager *manager = [[EaseEmotionManager alloc] initWithType:(EMEmotionDefault) emotionRow:3 emotionCol:7 emotions:[EaseEmoji allEmoji]];
// [self.faceView setEmotionManagers:@[manager]];
//语音动态图片数组
/* NSArray *array = [[NSArray alloc]initWithObjects:
[UIImage imageNamed:@"chat_sender_audio_playing_full"],
[UIImage imageNamed:@"chat_sender_audio_playing_000"],
[UIImage imageNamed:@"chat_sender_audio_playing_001"],
[UIImage imageNamed:@"chat_sender_audio_playing_002"],
[UIImage imageNamed:@"chat_sender_audio_playing_003"],
nil];
*/
// [[EaseBaseMessageCell appearance] setSendMessageVoiceAnimationImages:array];
/* NSArray * array1 = [[NSArray alloc] initWithObjects:
[UIImage imageNamed:@"chat_receiver_audio_playing_full"],
[UIImage imageNamed:@"chat_receiver_audio_playing000"],
[UIImage imageNamed:@"chat_receiver_audio_playing001"],
[UIImage imageNamed:@"chat_receiver_audio_playing002"],
[UIImage imageNamed:@"chat_receiver_audio_playing003"],nil];
*/
// [[EaseBaseMessageCell appearance] setRecvMessageVoiceAnimationImages:array1];
}
这里要注意的是更改功能模块中的图片和文字的时候,文字是没有效果的,源码中没有添加Label的代码,需要我们自己去写,可以添加分类,也可以直接在源码上改,我这里由于只是多了Label而已,所以是直接在源码上改的
在EaseChatBarMoreView.m中,下面的方法中添加Label即可
- (void)updateItemWithImage:(UIImage *)image highlightedImage:(UIImage *)highLightedImage title:(NSString *)title atIndex:(NSInteger)index {
对了,如果要修改ChatBarMoreView的高度的话,在第220行
if (_maxIndex >=5) {
frame.size.height = 150;
} else {
// 修改高度
frame.size.height = 120;
}
在ChatViewController.m中,我们继续添加:
注意:这里可能会出现发现重复消息。[self tableViewDidTriggerHeaderRefresh]; 检查一下这个方法是不是在chatViewController 和EaseMessageViewCOntroller 的ViewDidLoad 里面都调用了,看如果都有,随便删除一个这个方法。就ok了!
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (self.conversation.type == EMConversationTypeGroupChat) {
if ([[self.conversation.ext objectForKey:@"subject"] length])
{
self.title = [self.conversation.ext objectForKey:@"subject"];
}
}
}
实现收到消息以后播放音频以及震动
//收到消息的回调
- (void)messagesDidReceive:(NSArray *)aMessages {
//收到消息时,播放音频
[[EMCDDeviceManager sharedInstance] playNewMessageSound];
//收到消息时, 震动
[[EMCDDeviceManager sharedInstance] playVibration];
}
根据遵循EaseMessageViewControllerDelegate的代理,实现长按手势的功能,转发,复制,删除如下:
//是否允许长按
- (BOOL)messageViewController:(EaseMessageViewController *)viewController
canLongPressRowAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
//触发长按手势
- (BOOL)messageViewController:(EaseMessageViewController *)viewController
didLongPressRowAtIndexPath:(NSIndexPath *)indexPath
{
id object = [self.dataArray objectAtIndex:indexPath.row];
if (![object isKindOfClass:[NSString class]]) {
EaseMessageCell *cell = (EaseMessageCell *)[self.tableView cellForRowAtIndexPath:indexPath];
[cell becomeFirstResponder];
self.menuIndexPath = indexPath;
[self _showMenuViewController:cell.bubbleView andIndexPath:indexPath messageType:cell.model.bodyType];
}
return YES;
}
- (void)_showMenuViewController:(UIView *)showInView
andIndexPath:(NSIndexPath *)indexPath
messageType:(EMMessageBodyType)messageType
{
if (self.menuController == nil) {
self.menuController = [UIMenuController sharedMenuController];
}
if (_deleteMenuItem == nil) {
_deleteMenuItem = [[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"删除", @"Delete") action:@selector(deleteMenuAction:)];
}
if (_copyMenuItem == nil) {
_copyMenuItem = [[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"复制", @"Copy") action:@selector(copyMenuAction:)];
}
if (_transpondMenuItem == nil) {
_transpondMenuItem = [[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"转发", @"Transpond") action:@selector(transpondMenuAction:)];
}
if (messageType == EMMessageBodyTypeText) {
[self.menuController setMenuItems:@[_copyMenuItem, _deleteMenuItem,_transpondMenuItem]];
} else if (messageType == EMMessageBodyTypeImage){
[self.menuController setMenuItems:@[_deleteMenuItem,_transpondMenuItem]];
} else {
[self.menuController setMenuItems:@[_deleteMenuItem]];
}
[self.menuController setTargetRect:showInView.frame inView:showInView.superview];
[self.menuController setMenuVisible:YES animated:YES];
}
- (void)transpondMenuAction:(id)sender
{
if (self.menuIndexPath && self.menuIndexPath.row > 0) {
id<IMessageModel> model = [self.dataArray objectAtIndex:self.menuIndexPath.row];
// ContactListSelectViewController *listViewController = [[ContactListSelectViewController alloc] initWithNibName:nil bundle:nil];
// listViewController.messageModel = model;
// [listViewController tableViewDidTriggerHeaderRefresh];
// [self.navigationController pushViewController:listViewController animated:YES];
}
self.menuIndexPath = nil;
}
- (void)copyMenuAction:(id)sender
{
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
if (self.menuIndexPath && self.menuIndexPath.row > 0) {
id<IMessageModel> model = [self.dataArray objectAtIndex:self.menuIndexPath.row];
pasteboard.string = model.text;
}
self.menuIndexPath = nil;
}
- (void)deleteMenuAction:(id)sender
{
if (self.menuIndexPath && self.menuIndexPath.row > 0) {
id<IMessageModel> model = [self.dataArray objectAtIndex:self.menuIndexPath.row];
NSMutableIndexSet *indexs = [NSMutableIndexSet indexSetWithIndex:self.menuIndexPath.row];
NSMutableArray *indexPaths = [NSMutableArray arrayWithObjects:self.menuIndexPath, nil];
[self.conversation deleteMessageWithId:model.message.messageId error:nil];
[self.messsagesSource removeObject:model.message];
if (self.menuIndexPath.row - 1 >= 0) {
id nextMessage = nil;
id prevMessage = [self.dataArray objectAtIndex:(self.menuIndexPath.row - 1)];
if (self.menuIndexPath.row + 1 < [self.dataArray count]) {
nextMessage = [self.dataArray objectAtIndex:(self.menuIndexPath.row + 1)];
}
if ((!nextMessage || [nextMessage isKindOfClass:[NSString class]]) && [prevMessage isKindOfClass:[NSString class]]) {
[indexs addIndex:self.menuIndexPath.row - 1];
[indexPaths addObject:[NSIndexPath indexPathForRow:(self.menuIndexPath.row - 1) inSection:0]];
}
}
[self.dataArray removeObjectsAtIndexes:indexs];
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
if ([self.dataArray count] == 0) {
self.messageTimeIntervalTag = -1;
}
}
self.menuIndexPath = nil;
}
添加表情,并发送,这里我并没有遇到其他同学说的表情发送崩溃的问题,不过还是将解决方法贴出来,在ViewDidLoad中,大家可以看一下
//获取表情列表
- (NSArray*)emotionFormessageViewController:(EaseMessageViewController *)viewController
{
NSMutableArray *emotions = [NSMutableArray array];
for (NSString *name in [EaseEmoji allEmoji]) {
EaseEmotion *emotion = [[EaseEmotion alloc] initWithName:@"" emotionId:name emotionThumbnail:name emotionOriginal:name emotionOriginalURL:@"" emotionType:EMEmotionDefault];
[emotions addObject:emotion];
}
EaseEmotion *temp = [emotions objectAtIndex:0];
EaseEmotionManager *managerDefault = [[EaseEmotionManager alloc] initWithType:EMEmotionDefault emotionRow:3 emotionCol:7 emotions:emotions tagImage:[UIImage imageNamed:temp.emotionId]];
NSMutableArray *emotionGifs = [NSMutableArray array];
_emotionDic = [NSMutableDictionary dictionary];
NSArray *names = @[@"icon_002",@"icon_007",@"icon_010",@"icon_012",@"icon_013",@"icon_018",@"icon_019",@"icon_020",@"icon_021",@"icon_022",@"icon_024",@"icon_027",@"icon_029",@"icon_030",@"icon_035",@"icon_040"];
int index = 0;
for (NSString *name in names) {
index++;
EaseEmotion *emotion = [[EaseEmotion alloc] initWithName:[NSString stringWithFormat:@"[表情%d]",index] emotionId:[NSString stringWithFormat:@"em%d",(1000 + index)] emotionThumbnail:[NSString stringWithFormat:@"%@_cover",name] emotionOriginal:[NSString stringWithFormat:@"%@",name] emotionOriginalURL:@"" emotionType:EMEmotionGif];
[emotionGifs addObject:emotion];
[_emotionDic setObject:emotion forKey:[NSString stringWithFormat:@"em%d",(1000 + index)]];
}
EaseEmotionManager *managerGif= [[EaseEmotionManager alloc] initWithType:EMEmotionGif emotionRow:2 emotionCol:4 emotions:emotionGifs tagImage:[UIImage imageNamed:@"icon_002_cover"]];
return @[managerDefault,managerGif];
}
//判断消息是否为表情消息
- (BOOL)isEmotionMessageFormessageViewController:(EaseMessageViewController *)viewController
messageModel:(id<IMessageModel>)messageModel
{
BOOL flag = NO;
if ([messageModel.message.ext objectForKey:MESSAGE_ATTR_IS_BIG_EXPRESSION]) {
return YES;
}
return flag;
}
//根据消息获取表情信息
- (EaseEmotion*)emotionURLFormessageViewController:(EaseMessageViewController *)viewController
messageModel:(id<IMessageModel>)messageModel
{
NSString *emotionId = [messageModel.message.ext objectForKey:MESSAGE_ATTR_EXPRESSION_ID];
EaseEmotion *emotion = [_emotionDic objectForKey:emotionId];
if (emotion == nil) {
emotion = [[EaseEmotion alloc] initWithName:@"" emotionId:emotionId emotionThumbnail:@"" emotionOriginal:@"" emotionOriginalURL:@"" emotionType:EMEmotionGif];
}
return emotion;
}
//获取发送表情消息的扩展字段
- (NSDictionary*)emotionExtFormessageViewController:(EaseMessageViewController *)viewController
easeEmotion:(EaseEmotion*)easeEmotion
{
return @{MESSAGE_ATTR_EXPRESSION_ID:easeEmotion.emotionId,MESSAGE_ATTR_IS_BIG_EXPRESSION:@(YES)};
}
//view标记已读
- (void)messageViewControllerMarkAllMessagesAsRead:(EaseMessageViewController *)viewController
{
[[NSNotificationCenter defaultCenter] postNotificationName:@"setupUnreadMessageCount" object:nil];
}
最后就是实现ViewDidLoad中的通知了,这里的通知是删除所有会话,以及对于实时语音的一些实现,没有这些需求的同学们可以略过
#pragma mark - EMClientDelegate
//当前登录账号在其它设备登录时会接收到此回调
- (void)userAccountDidLoginFromOtherDevice
{
if ([self.imagePicker.mediaTypes count] > 0 && [[self.imagePicker.mediaTypes objectAtIndex:0] isEqualToString:(NSString *)kUTTypeMovie]) {
[self.imagePicker stopVideoCapture];
}
}
//当前登录账号已经被从服务器端删除时会收到该回调
- (void)userAccountDidRemoveFromServer
{
if ([self.imagePicker.mediaTypes count] > 0 && [[self.imagePicker.mediaTypes objectAtIndex:0] isEqualToString:(NSString *)kUTTypeMovie]) {
[self.imagePicker stopVideoCapture];
}
}
//服务被禁用
- (void)userDidForbidByServer
{
if ([self.imagePicker.mediaTypes count] > 0 && [[self.imagePicker.mediaTypes objectAtIndex:0] isEqualToString:(NSString *)kUTTypeMovie]) {
[self.imagePicker stopVideoCapture];
}
}
- (void)showGroupDetailAction
{
[self.view endEditing:YES];
// if (self.conversation.type == EMConversationTypeGroupChat) {
// EMGroupInfoViewController *infoController = [[EMGroupInfoViewController alloc] initWithGroupId:self.conversation.conversationId];
// [self.navigationController pushViewController:infoController animated:YES];
// }
// else if (self.conversation.type == EMConversationTypeChatRoom)
// {
// ChatroomDetailViewController *detailController = [[ChatroomDetailViewController alloc] initWithChatroomId:self.conversation.conversationId];
// [self.navigationController pushViewController:detailController animated:YES];
// }
}
- (void)deleteAllMessages:(id)sender
{
if (self.dataArray.count == 0) {
[self showHint:NSLocalizedString(@"message.noMessage", @"no messages")];
return;
}
if ([sender isKindOfClass:[NSNotification class]]) {
NSString *groupId = (NSString *)[(NSNotification *)sender object];
BOOL isDelete = [groupId isEqualToString:self.conversation.conversationId];
if (self.conversation.type != EMConversationTypeChat && isDelete) {
self.messageTimeIntervalTag = -1;
[self.conversation deleteAllMessages:nil];
[self.messsagesSource removeAllObjects];
[self.dataArray removeAllObjects];
[self.tableView reloadData];
[self showHint:NSLocalizedString(@"message.noMessage", @"no messages")];
}
}
else if ([sender isKindOfClass:[UIButton class]]){
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"prompt", @"Prompt") message:NSLocalizedString(@"sureToDelete", @"please make sure to delete") delegate:self cancelButtonTitle:NSLocalizedString(@"cancel", @"Cancel") otherButtonTitles:NSLocalizedString(@"ok", @"OK"), nil];
[alertView show];
}
}
- (void)exitChat
{
[self.navigationController popToViewController:self animated:NO];
[self.navigationController popViewControllerAnimated:YES];
}
- (void)insertCallMessage:(NSNotification *)notification
{
id object = notification.object;
if (object) {
EMMessage *message = (EMMessage *)object;
[self addMessageToDataSource:message progress:nil];
[[EMClient sharedClient].chatManager importMessages:@[message] completion:nil];
}
}
- (void)handleCallNotification:(NSNotification *)notification
{
id object = notification.object;
if ([object isKindOfClass:[NSDictionary class]]) {
//开始call
self.isViewDidAppear = NO;
} else {
//结束call
self.isViewDidAppear = YES;
}
}
截止到目前为止,聊天页面基本上就差不多了,这里需要重点说明的是聊天页面头像的数据处理
在这里环信给出了2种处理头像的方法,让我们一起来看一下,昵称和头像的显示与更新
方法一:从APP服务器获取昵称和头像
昵称和头像的获取:当收到一条消息(群消息)时,得到发送者的用户ID,然后查找手机本地数据库是否有此用户ID的昵称和头像,如没有则调用APP服务器接口通过用户ID查询出昵称和头像,然后保存到本地数据库和缓存,下次此用户发来信息即可直接查询缓存或者本地数据库,不需要再次向APP服务器发起请求。
昵称和头像的更新:当点击发送者头像时加载用户详情时从APP服务器查询此用户的具体信息然后更新本地数据库和缓存。当用户自己更新昵称或头像时,也可以发送一条透传消息到其他用户和用户所在的群,来更新该用户的昵称和头像。
方法二:从消息扩展中获取昵称和头像
昵称和头像的获取:把用户基本的昵称和头像的URL放到消息的扩展中,通过消息传递给接收方,当收到一条消息时,则能通过消息的扩展得到发送者的昵称和头像URL,然后保存到本地数据库和缓存。当显示昵称和头像时,请从本地或者缓存中读取,不要直接从消息中把赋值拿给界面(否则当用户昵称改变后,同一个人会显示不同的昵称)。
昵称和头像的更新:当扩展消息中的昵称和头像 URI 与当前本地数据库和缓存中的相应数据不同的时候,需要把新的昵称保存到本地数据库和缓存,并下载新的头像并保存到本地数据库和缓存。
这里我们选择使用方案二,首先我们要实现存储的功能,通过FMDB实现对用户model的存储,这里大家可以根据自己的需求进行存储相关信息,在登录成功之后你得先把自己的信息存储起来,在更改了个人资料之后,你要更新这里的存储信息。这样就可以做到更新头像后历史的头像也会更新**
简单来说:流程是这样的,存储用户的model信息 → 把用户信息扩展附加到要发送的消息中去 → 接收到消息以后通过数据源方法赋值到头像上去
#pragma mark - EaseMessageViewControllerDataSource
// 数据源方法
- (id<IMessageModel>)messageViewController:(EaseMessageViewController *)viewController
modelForMessage:(EMMessage *)message{
id<IMessageModel> model = nil;
// 根据聊天消息生成一个数据源Model
//NSLog(@"-======%@",message.from);
//debugObj(message.ext);
model = [[EaseMessageModel alloc] initWithMessage:message];
NSDictionary * messageDic = message.ext;
UserInfoModel * userinfoModel = [ChatUserDataManagerHelper queryByuserEaseMobId:messageDic[CHATUSERID]];
if (userinfoModel != nil) {
model.nickname = userinfoModel.usernickName;
model.avatarURLPath = userinfoModel.userHeaderImageUrl;
}
// 默认头像
//model.avatarImage = [UIImage imageNamed:@"EaseUIResource.bundle/user"];
//Placeholder image for network error
//项目图片取出错误的时候就用这张代替
model.failImageName = @"icon_Default-Avatar";
return model;
}
这里在贴两个代理方法,供大家查看
/*!
@method
@brief 获取消息自定义cell
@discussion 用户根据messageModel判断是否显示自定义cell。返回nil显示默认cell,否则显示用户自定义cell
@param tableView 当前消息视图的tableView
@param messageModel 消息模型
@result 返回用户自定义cell
*/
- (UITableViewCell *)messageViewController:(UITableView *)tableView
cellForMessageModel:(id<IMessageModel>)messageModel {
return nil;
}
/*!
@method
@brief 点击消息头像
@discussion 获取用户点击头像回调
@param viewController 当前消息视图
@param messageModel 消息模型
*/
- (void)messageViewController:(EaseMessageViewController *)viewController
didSelectAvatarMessageModel:(id<IMessageModel>)messageModel
{
NSLog(@"点击头像回调");
// UserProfileViewController *userprofile = [[UserProfileViewController alloc] initWithUsername:messageModel.message.from];
// [self.navigationController pushViewController:userprofile animated:YES];
}
会话列表部分
接下来,我们一起来看看会话列表的实现,同样的,我们也是创建一个类并继承于EaseConversationListViewController
废话不多说,上Code,在MessageViewController.m中
在ViewDidLoad中,我们加入如下代码:
//首次进入刷新数据,加载会话列表
[self tableViewDidTriggerHeaderRefresh];
[[EMClient sharedClient].chatManager addDelegate:self delegateQueue:nil];
//获取当前所有会话
self.datalistArray = (NSMutableArray *) [[EMClient sharedClient].chatManager getAllConversations];
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
[self tableViewDidTriggerHeaderRefresh];
[self refreshAndSortView];
self.datalistArray = (NSMutableArray *) [[EMClient sharedClient].chatManager getAllConversations]; //获取当前所有会话
[_messageTableView reloadData];
}
/**
* 收到消息回调
*/
- (void)didReceiveMessages:(NSArray *)aMessages
{
[self tableViewDidTriggerHeaderRefresh];
[self refreshAndSortView]; //刷新内存中的消息
//加载新的会话
self.datalistArray = (NSMutableArray *) [[EMClient sharedClient].chatManager getAllConversations];
//这里需要的话可以加入时间排序(别忘了刷新数据源)
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *funcIdentifier = @"funcIdentifier";
if (indexPath.section == 0) {
MsgFuncTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:funcIdentifier];
if (!cell) {
cell = [[MsgFuncTableViewCell alloc] initWithStyle:(UITableViewCellStyleDefault) reuseIdentifier:funcIdentifier];
}
UIView *lineView = [UIView new];
lineView.backgroundColor = [UIColor colorWithNumber:kLineColor];
[cell addSubview:lineView];
[lineView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.bottom.equalTo(cell);
make.height.equalTo(@0.7);
}];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.imageV.image = [UIImage imageNamed:[NSString stringWithFormat:@"%@",[_funcArray objectAtIndex:0][indexPath.row]]];
cell.label.text = [_funcArray objectAtIndex:1][indexPath.row];
return cell;
}
else if (indexPath.section == 1) {
// MessageChatTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
// if (!cell) {
// cell = [[MessageChatTableViewCell alloc] initWithStyle:(UITableViewCellStyleDefault) reuseIdentifier:cellIdentifier];
// }
// 这里开始我们使用环信提供的一种cell
EaseConversationCell * cell = [tableView dequeueReusableCellWithIdentifier:@"reuseID"];
if (!cell) {
cell = [[EaseConversationCell alloc] initWithStyle:(UITableViewCellStyleDefault) reuseIdentifier:@"reuseID"];
}
EMConversation *conversation = [_datalistArray objectAtIndex:indexPath.row];
// EMConversationTypeChat = 0, 单聊会话
// EMConversationTypeGroupChat, 群聊会话
// EMConversationTypeChatRoom 聊天室会话
switch (conversation.type) {
//单聊会话
case EMConversationTypeChat:
{
//这里有个小坑,刚开始不知道怎么获取到对方的昵称,就用了下面的方法去获取,根据当前的会话是接收方还是发送方来获取发送的对象,或接收的对象,结果有些能获取到,有些返回的Null,
// cell.textLabel.text = [conversation lastReceivedMessage].direction == EMMessageDirectionSend? [conversation lastReceivedMessage].to : [conversation lastReceivedMessage].from;
cell.titleLabel.text = conversation.conversationId;
NSLog(@"发送方%@------接收方%@",[conversation lastReceivedMessage].from,[conversation lastReceivedMessage].to);
//头像,我这里用固定的头像
cell.avatarView.image = [UIImage imageNamed:kDefaultUserHeadImage];
//设置头像圆角
cell.avatarView.imageCornerRadius = 20;
//是否显示角标
cell.avatarView.showBadge = YES;
//未读消息数量
cell.avatarView.badge = conversation.unreadMessagesCount;
break;
}
default:
break;
}
//这里是将会话的最后一条消息装换成具体内容展示
cell.detailLabel.text = [self subTitleMessageByConversation:conversation];
//显示最后一条消息的时间
cell.timeLabel.text = [NSString stringWithFormat:@"%@",[self lastMessageDateByConversation:conversation]];
//添加分割线
UIView *lineView = [UIView new];
lineView.backgroundColor = [UIColor colorWithNumber:kLineColor];
[cell addSubview:lineView];
[lineView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.bottom.equalTo(cell);
make.height.equalTo(@0.7);
}];
return cell;
}
else {
return [UITableViewCell new];
}
}
在UITableView的didSelect中,代码如下:
EMConversation *msgConversation = _datalistArray[indexPath.row];
ChatViewController *chatVC = [[ChatViewController alloc] initWithConversationChatter:msgConversation.conversationId conversationType:EMConversationTypeChat];
chatVC.hidesBottomBarWhenPushed = YES;
chatVC.title = msgConversation.conversationId;
[self.navigationController pushViewController:chatVC animated:YES];
接下来就是获取最后消息的文字或者类型,以及获得最后一条消息显示的时间
//得到最后消息文字或者类型
-(NSString *)subTitleMessageByConversation:(EMConversation *)conversation
{
NSString *ret = @"";
EMMessage *lastMessage = [conversation latestMessage];
EMMessageBody * messageBody = lastMessage.body;
if (lastMessage) {
EMMessageBodyType messageBodytype = lastMessage.body.type;
switch (messageBodytype) {
// EMMessageBodyTypeText = 1, /*! \~chinese 文本类型 \~english Text */
// EMMessageBodyTypeImage, /*! \~chinese 图片类型 \~english Image */
// EMMessageBodyTypeVideo, /*! \~chinese 视频类型 \~english Video */
// EMMessageBodyTypeLocation, /*! \~chinese 位置类型 \~english Location */
// EMMessageBodyTypeVoice, /*! \~chinese 语音类型 \~english Voice */
// EMMessageBodyTypeFile, /*! \~chinese 文件类型 \~english File */
// EMMessageBodyTypeCmd, /*! \~chinese 命令类型 \~english Command */
//图像类型
case EMMessageBodyTypeImage:
{
ret = NSLocalizedString(@"[图片消息]", @"[image]");
} break;
//文本类型
case EMMessageBodyTypeText:
{
NSString *didReceiveText = [EaseConvertToCommonEmoticonsHelper
convertToSystemEmoticons:((EMTextMessageBody *)messageBody).text]; //表情映射
ret = didReceiveText;
} break;
//语音类型
case EMMessageBodyTypeVoice:
{
ret = NSLocalizedString(@"[语音消息]", @"[voice]");
} break;
//位置类型
case EMMessageBodyTypeLocation:
{
ret = NSLocalizedString(@"[地理位置信息]", @"[location]");
} break;
//视频类型
case EMMessageBodyTypeVideo:
{
ret = NSLocalizedString(@"[视频消息]", @"[video]");
} break;
default:
break;
}
}
return ret;
}
//获得最后一条消息显示的时间
- (NSString *)lastMessageDateByConversation:(EMConversation *)conversation {
NSString *latestMessageTime = @"";
EMMessage *lastMessage = [conversation latestMessage];;
if (lastMessage) {
latestMessageTime = [NSDate formattedTimeFromTimeInterval:lastMessage.timestamp];
}
return latestMessageTime;
}
//给加载会话列表添加下拉刷新方法
- (void)tableViewDidTriggerHeaderRefresh {
[super tableViewDidTriggerHeaderRefresh]; //这里必须写super,完全继承
__weak MessageViewController *weakSelf = self;
self.messageTableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
[weakSelf.messageTableView reloadData];
[weakSelf tableViewDidFinishTriggerHeader:YES reload:NO];
// [weakSelf.messageTableView reloadData]; //刷新数据源
// [weakSelf refreshAndSortView]; //刷新内存页面
[weakSelf.messageTableView.mj_header endRefreshing]; //结束刷新
}];
self.messageTableView.mj_header.accessibilityIdentifier = @"refresh_header";
// header.updatedTimeHidden = YES;
}
截止到这里基本上就已经完成简单的单聊了,至于添加好友和联系人列表都比较简单,大家可以到环信官网中自己查看,以后有时间的话会补上群组,聊天室这一块的,最后补上两条不错的文章,大家有相关需求的话可以去看看**
基于环信实现发送/预览文件的功能
基于环信实现实时视频语音通话功能
结束语:本次简单集成环信就算完成了,希望大家能多多指教,多提宝贵意见,有什么不足的地方可以在文章下方留言,希望这篇文章能真正的帮助到大家,如果您觉得还算不错的话,请点赞或打赏!谢谢!