版權聲明:本文為博主原創文章,未經博主允許不得轉載。
前言
公司最近要求做即時通訊, 直接用了三方環信了,今天和大家談談關于 我做環信集成的過程和坑點,有什么不足的地方,還望大家多多指正
與環信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
-
編譯后,工程會出現如下錯誤:
WX20170614-171907@2x.png
這個是因為用到了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;
}
截止到這里基本上就已經完成簡單的單聊了,至于添加好友和聯系人列表都比較簡單,大家可以到環信官網中自己查看,以后有時間的話會補上群組,聊天室這一塊的,最后補上兩條不錯的文章,大家有相關需求的話可以去看看**
基于環信實現發送/預覽文件的功能
基于環信實現實時視頻語音通話功能
結束語:本次簡單集成環信就算完成了,希望大家能多多指教,多提寶貴意見,有什么不足的地方可以在文章下方留言,希望這篇文章能真正的幫助到大家,如果您覺得還算不錯的話,請點贊或打賞!謝謝!