集成环信3.0,入坑指南,看这一篇就够

版权声明:本文为博主原创文章,未经博主允许不得转载。

前言

公司最近要求做即时通讯, 直接用了三方环信了,今天和大家谈谈关于 我做环信集成的过程和坑点,有什么不足的地方,还望大家多多指正

与环信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进行导入,其中有两个版本供大家选择:HyphenateLiteHyphenate 其中后者包含了实时语音
    这里我们就不过多阐述了,在这里附上官方的SDK集成网址供大家参考
    集成iOS SDK前准备工作
    iOS的SDK导入

初始化SDK,以及登录,注册,自动登录,退出登录

*在.pch文件中我们引用 #import *
在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];
    }
}

进行到这里以后,相信大家就能实现简单的登录,注册以及自动登录了,是不是也比较简单呢,接下来简单说一下在登录,注册过程中遇到的问题。

  1. 引用头文件的时候报错出现:Hyphenate/EMSDK.h’ file no found
    解决方法: 换下引用#import
    或者#import

    如果此方法不行, 可以试试选中你的项目中的Pods -> EaseUI->Build Phases->Link Binary With Libraries ,点➕->Add Other ,找到工程里面,Pods里面的Hyphenate文件夹下面的Hyphenate.framework 点击open,重新编译就好了
    集成环信3.0,入坑指南,看这一篇就够_第1张图片
  2. 真机上登录,注册没有效果
    解决方法: 点击工程名进入工程设置 -> BuildSettings -> 搜索bitcode -> 将Enable Bitcode设置为NO
  3. 集成动态库上传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导入中有详细地说明,拿实时音视频版本版本为例 : 执行完以上命令如图所示
    集成环信3.0,入坑指南,看这一篇就够_第2张图片
    运行完毕后得到的Hyphenate.framework就是最后的结果,拖进工程,编译打包上架。
    集成环信3.0,入坑指南,看这一篇就够_第3张图片
    注意 : 最后得到的包必须真机编译运行,并且工程要设置编译二进制文件General->Embedded Bunaries.
    删除i386、x86_64平台后,SDK会无法支持模拟器编译,只需要在上传AppStore时在进行删除,上传后,替换为删除前的SDK,建议先分别把i386、x86_64、arm64、armv7各平台的包拆分到本地,上传App Store时合并arm64、armv7平台,并移入Hyphenate.framework内。上传后,重新把各平台包合并移入动态库
  4. 依旧是打包错误: 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的时候,有两种方法:

  1. 使用cocoapods导入 pod 'EaseUI', :git => 'https://github.com/easemob/easeui-ios-hyphenate-cocoapods.git', :tag => '3.3.2'(这里我推荐使用第一种,比较省事,简单)
  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 ,编译,恩,居然没有报错,看来可以进行下一步了
直接在AppDelegate.m中初始化EaseUI:

[[EaseSDKHelper shareHelper] hyphenateApplication:application
                        didFinishLaunchingWithOptions:launchOptions
                                               appkey:HUANXIN_APPKEY
                                         apnsCertName:nil
                                          otherConfig:@{kSDKConfigEnableConsoleLogger:[NSNumber numberWithBool:YES]}];

这时,当我满怀信心跑起来了工程,纳尼??不能自动登录了,每次必须退出登录以后,再登录一次以后才能实现自动登录,然后当我第二次运行工程的时候发现自动登录又失效了,什么鬼?!

集成环信3.0,入坑指南,看这一篇就够_第4张图片
2C83F9DCCF6D31A0960BCCC31D46A933.jpg

坑点2: 直接登录不能发送消息, 必须自动登录以后才能发送接收,自动登录大部分时候会走失败的回调
最后依靠万能的环信客服人员提供了技术支持,不得不说环信的客服还是很给力的

原来是使用pods导入了两个版本的SDK,使用pods导入的同学们一定要注意这个问题啊,不要重复导入,不然会出现许多未知的bug,
集成环信3.0,入坑指南,看这一篇就够_第6张图片
I27FF4PMBQQCP{PLIQ{5E4B.jpg

接下来我们看一下第二种方法:手动导入EaseUI

  1. 首先我们根据下载好的环信demo中的文件拖入到工程中,
    集成环信3.0,入坑指南,看这一篇就够_第7张图片

    如果要是集成红包功能,就加上RedacketSDK

  2. 把demo中的pch文件 拷贝到自己的pch文件中,并且在自己所有的pch文件的头和尾添加
#ifdef __OBJC__
//
#endif
  1. 编译后,工程会出现如下错误:


    集成环信3.0,入坑指南,看这一篇就够_第9张图片

这个是因为用到了UIKit里的类,但是只导入了Foundation框架,这个错误在其他类里也会出现,我们可以手动修改Founfation为UIKit,但是我不建议这么做,第一这个做法的工程量比较大, 在其他类里面也要导入,二,不利于移植,当以后环信更新的时候我们还是需要做同样的操作,这里我的做法的创建一个pch文件,在pch文件里面导入UIKit。
解决办法:建一个PCH文件在里面添加如下代码:

#ifdef __OBJC__
    #import 
    #import 
#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换成HyphenateLite就可以了,和上面提到的第一点是一样的
到这里以后,应该没有什么问题,编译如果成功的话,那么恭喜你了

至此,我们就导入了EaseUI并在appDelegate.m中初始化了EaseUI,接下来我们就先来完善聊天的页面

聊天页面部分

EaseUI集成应用其实简单很多很多,里面也封装了关于头像昵称的设置,所需要做的只是把代理方法实现,前提是你的聊天页面等都是继承EaseUI里面的相关的类去做的。

这里给大家推荐环信官方论坛的一个快速集成聊天的网址:IOS快速集成环信IM - 基于官方的Demo优化,5分钟集成环信IM功能

集成环信3.0,入坑指南,看这一篇就够_第10张图片
E04D3C27-B372-4D17-B801-A50C1E395589.jpeg

由于环信官方只是通过用户名的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 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 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 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)messageModel
{
    BOOL flag = NO;
    if ([messageModel.message.ext objectForKey:MESSAGE_ATTR_IS_BIG_EXPRESSION]) {
        return YES;
    }
    return flag;
}

//根据消息获取表情信息
- (EaseEmotion*)emotionURLFormessageViewController:(EaseMessageViewController *)viewController
                                      messageModel:(id)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)messageViewController:(EaseMessageViewController *)viewController  
                           modelForMessage:(EMMessage *)message{  
      
    id 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)messageModel {
    return nil;
}

/*!
 @method
 @brief 点击消息头像
 @discussion 获取用户点击头像回调
 @param viewController 当前消息视图
 @param messageModel 消息模型
 */
- (void)messageViewController:(EaseMessageViewController *)viewController
  didSelectAvatarMessageModel:(id)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;
}

截止到这里基本上就已经完成简单的单聊了,至于添加好友联系人列表都比较简单,大家可以到环信官网中自己查看,以后有时间的话会补上群组,聊天室这一块的,最后补上两条不错的文章,大家有相关需求的话可以去看看**
基于环信实现发送/预览文件的功能
基于环信实现实时视频语音通话功能

结束语:本次简单集成环信就算完成了,希望大家能多多指教,多提宝贵意见,有什么不足的地方可以在文章下方留言,希望这篇文章能真正的帮助到大家,如果您觉得还算不错的话,请点赞或打赏!谢谢!

你可能感兴趣的:(集成环信3.0,入坑指南,看这一篇就够)