iOS端集成问题


1、EaseIMKit如何设置昵称、头像

参考文章:https://www.jianshu.com/p/3de3b7c5b203


iOS端推送问题


1、环信管理后台有的用户显示证书名称有的不显示证书名称?

1)显示证书名称:用户是不是绑定证书要看是不是在app端登录过,只有登录环信服务器登录成功之后才会绑定证书,也才会显示证书名称,用户离线时接收消息才有推送通知。

2)不显示证书名称:如果用户没在app端登录过,就不会显示证书名称,还有在app端登录之后,调用环信SDK方法退出登录(如果方法内传的是YES,解绑devicetoken和证书),这样也不会显示证书名称。


2、iOS没有通知栏提示?

通知栏分本地通知和apns推送通知

1).如果指的是app进入后台没有超过150秒左右,接收消息没有通知栏提示的话,这个情况要配置本地通知,先先注册本地通知(系统方法),然后在看下文档介绍: 如何实现本地通知

2).如果不是本地通知,app退到后台超过150秒左右,接收消息没有通知栏提示的话,这个情况属于apns推送。

如果没有配置apns推送,那么先按照文档配置: APNS推送配置

如果配置过了apns推送,那么在初始化SDK方法之后调用下这个方法试下  ​

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    [[EMClient sharedClient] applicationDidEnterBackground:application];
}

- (void)applicationWillEnterForeground:(UIApplication *)application
{
    [[EMClient sharedClient] applicationWillEnterForeground:application];
}

3、iOS本地通知栏如何展示?

本地通知栏的显示属于本地自己设置的,这个可以参考下环信demo的做法,判断apns推送通知栏是不是显示详情的,是的话,那就将本地通知栏也显示成‘昵称:消息内容’,如果是默认的就显示‘您有一条新消息’。

在EMRemindManager.m类
- (void)_localNotification:(EMMessage *)message needInfo:(BOOL)isNeed 方法中

环信Demo: Demo下载链接


4、APNS推送收不到怎么办?

如果是iOS13及以上的系统,那么需要将sdk更新到3.6.4或以上版本。如果更新后还不行那么退出登录、重启app、再登录试下。

如果不是iOS的原因,可根据以下链接中的内容进行排查: 推送排查文档

如果以上都确认无误,请将appkey、devicetoken、bundle id、证书的.p12文件(附件中上传给我,若无法上传,则把后缀改成.txt试下)、证书名称、证书密码、收不到推送的环信id、测试的环境(development or production)、推送消息的内容、发送的时间以及消息id(请勿遗漏,以免工单反复询问耽误您的时间) 这些信息提交工单,环信技术支持团队将会对您反馈的问题进行排查。 工单入口:登录IM环信管理后台–应用列表–应用基本信息–右下角技术支持按钮


5、环信推送的角标怎么和极光推送的角标合并?

可以通过 NotificationServiceExtension 功能实现。(此方案需要极光那边也支持mutable-content)

在发消息时,加“mutable-content”:1 字段,参考iOS端文档:http://docs-im.easemob.com/im/ios/apns/content#开启_apns_通知扩展 那么环信服务器给苹果apns推送服务器发消息时,也会把这个字段传给苹果那边,苹果那边识别到有“mutable-content”:1字段,会假唤醒app,配置NotificationServiceExtension之后,你可取到环信和极光的推送角标数进行加法,然后自己再修改推送通知栏显示的样式和角标数,具体可以看苹果NotificationServiceExtension的文档介绍。


6、离线推送角标数跳跃

问:

杀死app后测试离线推送,角标计数有时候会不准。

例如:我们连续发了4条消息,但是APP的角标数有时候会从4闪一下变成2。;还有时候比如现在发了8条消息。然后APP的角标是4 但是当我们再发送一条消息,角标又变成9了。

答:

您好,发送第一条消息,这条消息携带的角标数是1,发送第二条消息,这条消息携带的角标数是2,发送第n条消息,这条消息携带的角标数是n,有可能是某条消息的离线推送有些延迟,导致角标数跳跃。


7、iOS推送通知栏如何显示消息详情?

参考文档设置: 推送显示详情


8、如果没有登录环信,可以收到推送吗?

1.注册完没有登录过的用户。——— 不会收到推送,因为没有绑定devicetoken,只有登录后才会绑定。

2.登录后,退出登录。 ——— 要看调用环信SDK的退出登录方法传的是NO还YES,传YES是解绑devicetoken,不会收到推送。 传NO是不解绑devicetoken,会收到推送,但不建议传NO,如果多个账号在同一台设备登录过的情况,会出现多个用户绑定同一个devicetoken,这样多个用户的离线消息都会推送到同一台设备上。

3.登录后。直接杀掉app或者app在后台,锁屏超过3分钟。——— 这种情况用户的长连接会环信服务器断开,但devicetoken不会解绑,有离线消息的话就会有推送。


iOS端使用问题


1、从环信服务器上下载消息中的附件没有后缀怎么办?

附件类型的消息(图片,语音,视频,文件消息等)发送到环信服务器后是不带后缀的,这个可以在构建消息的时候,给附件消息命名时加上后缀名

比如图片消息的话,可以给displayName设置image.png 构建图片消息

这样接收消息方在接收到消息时,可以从消息的body中通过displayName拿到名称就可以知道附件的格式,地理位置消息的话,可以在发送消息的时候,给消息的ext里面加个参加把附件名称和格式带上,这样接收消息方通过解析消息的ext也可以知道附件消息的类型。


2、环信id是什么?

环信id就是指在环信appkey下注册的账号并且是唯一的,用于客户端环信SDK登录以及收发消息使用的。

在注册环信 id 时,建议不要使用有序的 id 进行注册,防止其他人知道注册 id 的顺序,恶意发送大量的垃圾消息。

一般是服务器端给自己用户注册账号的同时,在调用环信的rest接口给用户在注册一个环信id与自己的用户绑定,并返回给客户端,客户端拿到自己服务器用户的账号密码登录后,在拿环信id密码在登录环信服务器。 用户管理

社区版的appkey只能注册100个环信id,企业版没有注册的限制。​


3、开启消息漫游之后 为什么不可以进行删除消息的操作了?

删除聊天记录是删除本地数据库的,不是环信服务器的,开通漫游消息后,如果调用的是从环信服务器获取历史消息的api(asyncFetchHistoryMessagesFromServer),就会出现这种情况。 您可以自己记录下删除了哪条消息,然后在加载历史消息的时候遍历过滤不展示;或者使用从本地数据库获取历史消息的api


4、获取会话列表为空情况有以下几种:

1)可能是本地数据库中没有会话或者将会话都删除掉了

2)没有登录环信成功,SDK数据库没有打开,取不到会话列表

3)可能是老版本SDK的问题,可以尝试先升级到最新版本的SDK,在测试下: SDK下载

4)换新设备登录,因为会话列表是存设备本地数据库的,如果用户换新设备登录,就获取不到之前设备上的会话列表

5)卸载app,因为会话列表是存设备本地数据库的,如果卸载app本地数据库文件会被清理掉,导致会话列表获取不到


5、消息漫游功能,比如账号在A设备上登录接收了3条没有已读,未读消息数是3,然后在B设备上登录这个账号,也想显示这个会话并把会话的这3条未读消息数也获取到,如何处理?

先调用SDK方法拉取漫游的消息,然后在获取到这个会话,在取会话的未读消息数。不能创建会话后在进入聊天页面,如果用的环信的聊天页面UI,进入聊天页面时就会把会话中的所有未读消息标记已读,这样就获取不到会话的未读消息数了。 按照下面的方法登录成功后测试,u2换成对方的环信id。

[EMClient.sharedClient.chatManager asyncFetchHistoryMessagesFromServer:@"u2" conversationType:EMConversationTypeChat startMessageId:nil pageSize:50 completion:^(EMCursorResult *aResult, EMError *aError) {
if (!aError) {
} else {
}
}];

EMConversation *conversation = [[EMClient sharedClient].chatManager getConversation:@"u2" type:EMConversationTypeChat createIfNotExist:YES];
NSLog(@"未读消息消息数---%d", [conversation unreadMessagesCount]);

(因为消息接收都是存设备本地数据库的,这个获取会话的方法是从本地数据库取的,并不是到环信服务器取,所以在换设备登录的时候,正常是不会把另外一台的设备上的会话和消息主动同步过来。 因为开了消息漫游,可以同步消息,但是并不能将其他设备上的会话给同步过来,得需要主动调用SDK消息漫游方法到服务器上拉取消息,SDK在将消息存到本地数据库,所以得先拉取消息,本地数据库中才会有会话和消息,才能通过会话取到未读消息数。)


6、用户个人信息如何存储获取?

环信这边是不涉及用户个人信息的,所以获取好友列表的方法返回的都是环信id,用户的个人信息可以在自己服务器与环信id绑定存储维护,知道环信id就可以到自己服务器下载这个环信id对应的用户信息(注意在注册环信id时传的昵称并不是个人信息的昵称,那个是在设置显示推送通知栏详情时,显示的推送昵称)。


7、如何自己实现漫游消息功能?

根据自己服务器端的返回给app端的消息内容,在app端创建会话,插入消息,以难度大的图片消息为例

创建会话: 创建会话​

构造图片消息: 构建图片消息

插入消息: 插入消息​​

插入图片消息示例代码:

    // 创建会话,以单聊会话为例
    // fdh333就是对方的环信id,EMConversationTypeChat单聊会话类型
    EMConversation *conversation = [[EMClient sharedClient].chatManager getConversation:@"fdh3333" type:EMConversationTypeChat createIfNotExist:YES];
    // 取到登录方的环信id
    NSString *from = [[EMClient sharedClient] currentUsername];
    // 创建图片消息body,Data要传nil,因为都是从服务器上取的图片url,没有本地图片转data传,所以传nil
    EMImageMessageBody *newBody = [[EMImageMessageBody alloc] initWithData:nil displayName:@"image"];
    // 缩略图在环信服务器上的url,替换成自己的图片消息中的url
    newBody.thumbnailRemotePath = @"https://a1.easemob.com/easemob-demo/chatdemoui/chatfiles/50742590-96ef-11e9-9b3e-9b44203ccce8";
    // 缩略图的密钥,替换成自己的图片消息中的SecretKey
    newBody.thumbnailSecretKey = @"UHQlmpbvEemzBC-5fkLNP8hQJlbKYOQNxcesOSeP-cMg9KAH";
    
    // 缩略图的本地路径,沙盒路径下,fdh8888换成自己登录的环信id,fdh3333换成对方的环信id,也是会话id,thumb_50742590-96ef-11e9-9b3e-9b44203ccce8中的"50742590-96ef-11e9-9b3e-9b44203ccce8"就是图片的uuid
    newBody.thumbnailLocalPath = @"/Users/easemob-dn0164/Library/Developer/CoreSimulator/Devices/8B048B3E-61DB-4A28-BA20-98564F4ABAB2/data/Containers/Data/Application/4E796EDA-E478-4834-9927-BFA3AFDCAE5A/Library/Application Support/HyphenateSDK/appdata/fdh8888/fdh3333/thumb_50742590-96ef-11e9-9b3e-9b44203ccce8";
    // 缩略图名称
    newBody.thumbnailDisplayName = @"thumbnail.png";
    // 缩略图下载状态
    newBody.thumbnailDownloadStatus = 0;
    // 缩略图大小
    newBody.thumbnailSize = CGSizeMake(0, 0);

    // remotePath,secretKey与thumbnailRemotePath,thumbnailSecretKey的参数相同
    newBody.remotePath = @"https://a1.easemob.com/easemob-demo/chatdemoui/chatfiles/50742590-96ef-11e9-9b3e-9b44203ccce8";
    newBody.secretKey = @"UHQlmpbvEemzBC-5fkLNP8hQJlbKYOQNxcesOSeP-cMg9KAH";
    // 大图的本地路径与缩略图本地路径的唯一差别就是图片的uuid前面不加thumb_前缀
    newBody.localPath = @"/Users/easemob-dn0164/Library/Developer/CoreSimulator/Devices/8B048B3E-61DB-4A28-BA20-98564F4ABAB2/data/Containers/Data/Application/4E796EDA-E478-4834-9927-BFA3AFDCAE5A/Library/Application Support/HyphenateSDK/appdata/fdh8888/fdh3333/50742590-96ef-11e9-9b3e-9b44203ccce8";
    // 大图的尺寸,替换成自己的图片消息中的size
    newBody.size = CGSizeMake(1800, 1202);
    // 大图的下载状态,
    newBody.downloadStatus = 3;
    // 大图的大小,替换成自己的图片消息中的fileLength
    newBody.fileLength = 539787;
    // 创建消息
    EMMessage *newMsg = [[EMMessage alloc] initWithConversationID:self.conversationModel.emModel.conversationId from:from to:self.conversationModel.emModel.conversationId body:newBody ext:nil];
    // 消息类型
    newMsg.chatType = EMChatTypeChat;
    // 控制消息的显示方向
    newMsg.direction = 1;

    EMError *error;
    // 向会话中插入消息
    [conversation insertMessage:newMsg error:&error];
    NSLog(@"insertMessageError---%@", error.errorDescription);

8、iOS如何发送文件消息?

环信demo中没有发送文件消息的功能,不过SDK是提供了构建文件消息方法,把文件的本地路径传进去,然后调用SDK的发送消息方法发送文件,参考文档: 构建文件消息

聊天页面的工具栏是可以扩展的,UI代码都是开源的,需要自己看下代码进行修改,在EMChatBar.m​类中


9、iOS如何将消息的扩展内容自定义cell?

向会话中插入一条文本消息或者发送一条消息,将需要展示到cell上的内容加到消息的ext中。 可以给插入消息的ext加个字段标识,然后根据这个标识显示自己自定义的cell展示,大致就是这个实现思路。

以发送消息为例的参考demo:

链接:https://pan.baidu.com/s/1YXqdbvp8tKtLyHXhl1ULXA 密码:vuys


10、iOS本地通知栏的展示

本地通知栏的显示属于本地自己设置的,这个可以参考下环信demo的做法,在接收消息的回调中,判断apns推送通知栏是不是显示详情的,是的话,那就将本地通知栏也显示成‘昵称:消息内容’,如果是默认的就显示‘您有一条新消息’。

EMRemindManager.m

- (void)remindMessage:(EMMessage *)aMessage方法中


11、群组管理员将用户禁言后,用户仍然可以在群内发消息?

需要确认下在调用禁言方法时,传的禁言时长是多久,单位是毫秒,如果传的时间较短,那用户禁言效果会很快消失,将时间戳参数传大一些在进行禁言测试。


12、iOS端关于环信账号离线常见的几种情况:

1.主动调用环信SDK退出登录方法 (不会触发环信SDK的重连机制)

2.app退到后台超过150秒左右

前提是调用将app进入后台的状态传给环信SDK:

- void)applicationDidEnterBackground:(UIApplication *)application {
    [[EMClient sharedClient] applicationDidEnterBackground:application];
}

app返回前台时,SDK自动重连,需要将app返回前台的状态传给环信SDK

- (void)applicationWillEnterForeground:(UIApplication *)application {
    [[EMClient sharedClient] applicationWillEnterForeground:application];
}

3.主动kill掉app,或者app在后台挂起被系统kill掉

4.断网或者网络不好的情况下,长连接断开,当网络恢复时SDK会自动重连

5.在线情况下,账号在其他设备登录被踢下线

2、4情况用这个回调监听: 重连

5情况用这个回调监听: 被动退出登录

1、3没监听


13、单点登录:A手机登录后杀掉app,然后B手机登录(A杀死后是离线状态,不存在被踢),然后A手机再打开应用,此时会自动登录上(如果开启了自动登录功能的话),所以B手机会被踢掉​。


14、background task有问题,输出: Can't end BackgroundTask: no background task exists with identifier 1 (0x1), or it may have already been ended. Break in UIApplicationEndBackgroundTaskError() to debug.

这个是ios13的bug,不是环信SDK的问题 :https://developer.apple.com/forums/thread/121990


iOS端常见报错



0、EaseCallKit报错信息:“Reason: image not found” 解决方案: 由于EaseCallKit是动态库,在podfile中必须加入use_frameworks! 参考文档:导入UI库


1、报错信息:“Reason: image not found”

解决方案: 在TARGETS → General → Frameworks, Libraries, and Embedded Content中添加Hyphenate.framework依赖库,且依赖库的Status必须是Embed & Sign


2、报错信息:

解决方案: 删掉重新添加试一下。添加时先粘贴到项目的finder文件内,再从finder往项目中拖。


3、报错信息:

解决方案: 把plugin文件夹删掉(删掉不会影响到功能)。


4、xcode12.3运行报错,报错信息:

解决方案: 打开Xcode,左上方点击File—>Workspace Settings—>将Shard Workspace Settings:中的New Build System(Default)修改成Legacy Build System,点击done,再试下。


1、报错信息:

解决方案:参考官方文档剔除 i386、x86_64 两个平台


2、报错信息:

解决方案:打包时取消勾选bitcode



Demo下载地址:环信音视频云下载

1.视频会议demo下载下来 podfile 里面的 Hyphenate 是注释的,需要先打开

2.视频会议demo只支持3.6.6以上版本的sdk。 您在 pod install 前先 pod search Hyphenate​看下能不能搜到3.6.6版本,如果搜不到,可以执行下 pod repo update 更新本地cocoapods的spec资源配置信息后再试下,如果还搜不到,那么需要您自己再看下是什么原因。

1、pod install 执行完成后运行会报错,报错信息:

解决方案:

打开xcode,最上方点击File—>Workspace Settings—>将Shard Workspace Settings:中的New Build System(Default)修改成Legacy Build System,点击done,最后运行项目。

2、报错信息:

解决方案:

3、xcode12模拟器运行报错,报错信息:

解决方案:

Build Settings里最下面的VALID_ARCHS,添加上x86_64。



iOS登录报303是什么原因?

报303一般是用户客户端网络不好没有连接上环信服务器,可以切换网络再登录试试(4G网络可以试试),如果开了VPN的话,将VPN关掉。 如果还是登录不了的话,提供下登录不了的环信id和密码,提交工单联系环信技术。


功能实现方案


1、个人消息免打扰

自己服务器端维护下好友免打扰列表,发消息前判断下对方有没有将自己设置为免打扰,如果设置了,就用静默消息,文档:发送静默消息,这样在离线时,就不会有这个人发来的消息的离线推送。 在线和后台活跃时,在收到消息的代理方法中( - (void)messagesDidReceive:(NSArray *)aMessages)​判断消息的from在不在自己的免打扰列表内,如果在,就不播放声音。


2、接收群组消息但不提示(消息免打扰)

(1)先屏蔽这个群组的离线推送,参考文档:设置指定群组是否接收_apns

(2)登录之后先调用“从服务器获取推送属性”方法:

//调用这个方法后sdk会自动更新本地的“屏蔽了推送的群组ID列表”
[[EMClient sharedClient] getPushNotificationOptionsFromServerWithCompletion:^(EMPushOptions *aOptions, EMError *aError) {
    if (!aError) {
        NSLog(@"从服务器获取推送属性成功");
    } else {
        NSLog(@"从服务器获取推送属性失败的原因 --- %@", aError.errorDescription);
    } 
}];

(3)在收到消息的代理方法中( - (void)messagesDidReceive:(NSArray *)aMessages),判断如果app处于后台活跃状态(正常是需要发送本地通知,提醒用户有消息的),就调用下面这个方法取到屏蔽了离线推送的群组ID列表,判断下消息是否来自于被屏蔽了离线推送的群组,是的话就不发本地通知。

NSArray *igGroupIds = [[EMClient sharedClient].groupManager getGroupsWithoutPushNotification:nil];

3、群主撤回群成员的消息

群主进行撤回操作时,给所有群成员发送一条cmd消息,cmd添加扩展,把消息id携带过去,群成员收到这条cmd消息时,解析出消息id,然后从本地删除这条消息,然后刷新UI。

注:只能删除本地数据库的,无法删除环信服务器端的。如果使用漫游功能从环信服务器端获取历史消息,还是能把撤回的那条消息拉取下来,这时候还要过滤下再删掉。

构造透传消息文档:构造透传消息

构造扩展消息文档:构造扩展消息

删除消息:


4、消息提示音(消息提醒、震动、响铃)

在线消息:可以全局(在根控制器或者AppDelegate​里)​监听收到消息的代理方法(messagesDidReceive​),在这个方法里播放响铃和震动。

离线消息:可以自定义推送提示音,参考文档:自定义推送提示音,但这个只能是播放本地音频文件,无法震动。想要震动,可以在发消息时,加“mutable-content”:1 字段,参考文档:开启_apns_通知扩展,那么环信服务器给苹果apns推送服务器发消息时,也会把这个字段传给苹果那边,苹果那边识别到有“mutable-content”:1字段,会假唤醒app,配置NotificationServiceExtension之后,您可以播放响铃和震动​,具体可以看苹果NotificationServiceExtension的文档介绍。


5、iOS在线与离线未读消息数累加

在线未读消息数与离线消息数累加的功能,可以按照下面的方案实现

在发消息时,加“mutable-content”:1 扩展字段,参考iOS端文档:开启 APNs 通知扩展, 那么环信服务器给苹果apns推送服务器发消息时,也会把这个字段传给苹果那边,苹果那边识别到有“mutable-content”:1字段,会假唤醒app,配置NotificationServiceExtension之后,您可取到环信的推送角标数,然后自己再修改推送通知栏显示的样式和角标数,具体可以看苹果NotificationServiceExtension的文档介绍。

(1)先新建一个NotificationServiceExtension的Targets

(2)主工程打开Remote notifications权限

(3)两个Targets都打开App Groups,并添加共享沙盒

(4)主工程将当前本地未读消息数动态存到共享沙盒

(5)在NotificationService中取出共享沙盒的角标数,与收到的APNs角标数累加重置。

存角标数使用的是App Groups:


6、不是好友不让发消息

环信目前的机制是只要知道对方的环信id就可以给对方发消息,环信有好友的机制,对应的功能可以看下文档介绍: 好友功能 如果使用环信的好友功能,比如A与B不是好友,那么A进入与B的聊天页面时,可以获取A的好友列表看是否存在B,如果不存在可以在UI上做限制,不让A发送消息。 如果使用的是自己的好友体系,那么就换成自己的业务进行判断是不是好友,从而在UI上做是否可以发送消息的限制。


7、用环信的消息做给自己的用户全量推送功能

如果想要环信的消息来做给自己用户全量推送功能,还不想让客户端用户看到这条会话,那么可以用环信服务器端rest接口批量发送消息实现,可以使用一个或者几个固定的环信id作为from批量给自己的用户发送消息(推送消息),然后客户端在取会话列表的时候,发现会话id如果是服务器端用于批量发消息的环信id,那么就将这个会话删除掉即可。

文档: rest发送消息接口 iOS端删除会话


8、iOS EaseUI如何自定义表情

参考链接中的demo实现自定义表情即可 自定义表情


9、iOS EaseUI聊天页面怎么显示群组的昵称头像

因为环信这边是不涉及用户个人信息的,所以通过环信id是获取不到用户昵称头像的,用户的个人信息可以在自己服务器与环信id绑定存储维护,知道环信id就可以到自己服务器下载这个环信id对应的用户信息(注意在注册环信id时传的昵称并不是个人信息的昵称,那个是在设置显示推送通知栏详情时,显示的推送昵称)。​​

环信UI上的处理,如果使用的是EaseUI的话,我建议是在EaseBaseMessageCell.m​类, - (void)setModel:(id<IMessageModel>)model​方法中,通过 self.model.message.chatType​ 判断出来是群组类型消息的话,在取到消息的from就是在群内发送消息的环信id,再根据环信id到自己服务器上取到用户的昵称,头像,在赋值给 _nameLabel.text​和 self.avatarView​ (注意单聊和群聊显示昵称,头像的逻辑要区分开)


10、iOS 漫游消息能否拉取会话列表?

这个漫游只能是拉取聊天记录,不能拉取聊天会话列表。

比如在A设备上,登录环信id:user1,然后有3个会话,那user1在设备B上登录时拉取不到A设备上的3个会话,只能是先通过漫游消息的方法,传A设备上的3个会话id,本地创建会话后,再从环信服务器上拉取下拉消息,这样本地就生成会话列表,也能看到会话中的消息。

如果你们有好友体系的话,账号在多个设备,或者多端登录时,可以先遍历自己的好友列表,然后分别调用一次漫游消息的方法,如果跟某个好友收发过消息,那么就会拉取下拉漫游消息,本地也就自动生成会话了,间接的实现了拉取会话列表的功能。

// 创建会话
EMConversation *conversation = [[EMClient sharedClient].chatManager getConversation:@"username" type:EMConversationTypeChat createIfNotExist:YES];
// 漫游消息,漫游下来的消息会自动添加到会话中
[EMClient.sharedClient.chatManager asyncFetchHistoryMessagesFromServer:@"username" conversationType:EMConversationTypeChat startMessageId:nil pageSize:5 completion:^(EMCursorResult *aResult, EMError *aError) {
if (!aError) {
} else {
}
}];


11、iOS新版UI如何自定义cell

下载这个demo参考,新版UI的自定义cell实现,工程中搜索 “自定义cell相关”: 自定义cell


12、用户撤回消息,实时消息回调会怎样处理?

撤回消息的事件是没有回调的,可以这样,如果想通过实时消息回调知道哪条消息是撤回的消息,那么在客户端撤回消息后,在发条cmd消息,cmd消息的ext里面加上撤回消息的标识然后在加上撤回消息的消息id,这样这条cmd消息也会回调到你们服务器上,这样就可以知道哪条消息是撤回消息了


13、如何知道对方的环信id是否在线

可以在客户端先请求自己的服务器,然后让服务器端调用环信获取用户在线状态的接口,将用户是否在线的状态返回给客户端即可: 用户是否在线


Demo现存bug


1、新版UI,点击群组语音消息后,未读红点不消失

在EMChatViewController.m​类, - (void)_audioMessageCellDidSelected:(EMMessageCell *)aCell​方法中,在截图中标注的位置添加以下代码

EMMessage *chatMessage = aCell.model.emModel;
NSMutableDictionary *dict = [chatMessage.ext mutableCopy];
if (chatMessage.ext) {
if (![[dict objectForKey:@"isPlayed"] boolValue]) {
[dict setObject:@YES forKey:@"isPlayed"];
chatMessage.ext = [dict copy];
}
} else {
chatMessage.ext = @{@"isPlayed":@YES};
}
[[EMClient sharedClient].chatManager updateMessage:chatMessage completion:nil];

然后在EMMessageCell.m​类, - (void)setModel:(EMMessageModel *)model​方法中,在截图中标注的位置修改成以下代码​

if (model.emModel.chatType == EMChatTypeChat) {
self.statusView.hidden = model.emModel.isReadAcked;
} else {
self.statusView.hidden = [[model.emModel.ext objectForKey:@"isPlayed"] boolValue];
}

2、iOS 3.6.0demo,聊天页面弹出的工具栏遮挡消息怎么处理?

EMChatViewController.m类 这个方法中- (void)keyBoardWillHide:(NSNotification *)note

改成这样

[UIView animateWithDuration:animationTime animations:animation completion:^(BOOL finished) {
    if (self.dataArray.count > 0) {
        [self.tableView reloadData];
        NSIndexPath *lastIndex = [NSIndexPath indexPathForRow:self.dataArray.count - 1 inSection:0];
        [self.tableView scrollToRowAtIndexPath:lastIndex atScrollPosition:UITableViewScrollPositionBottom animated:YES];
    }
}];

3、聊天页面发送过消息后,第一次刷新会出现重复的数据

按照以下来修改:

- (NSMutableArray *)messsagesSource
{
    if (_messsagesSource == nil) {
        _messsagesSource = [NSMutableArray array];
    }
    return _messsagesSource;
}

[weakself.messsagesSource addObject:msg];

[self.messsagesSource addObject:message];

然后将 tableViewDidTriggerHeaderRefresh 替换为下面的:
- (void)tableViewDidTriggerHeaderRefresh
{
    
    NSString *messageId = nil;
    if ([self.messsagesSource count] > 0) {
        messageId = [(EMMessage *)self.messsagesSource.firstObject messageId];
    }
    else {
        messageId = nil;
    }
    [self _loadMessagesBefore:messageId];

}

- (void)_loadMessagesBefore:(NSString*)messageId
{
    __weak typeof(self) weakself = self;
    void (^block)(NSArray *aMessages, EMError *aError) = ^(NSArray *aMessages, EMError *aError) {
        if (!aError && [aMessages count]) {
//            EMMessage *msg = aMessages[0];
//            weakself.moreMsgId = msg.messageId;
            
            dispatch_async(self.msgQueue, ^{
                NSArray *formated = [weakself _formatMessages:aMessages];
                
                NSInteger scrollToIndex = 0;
                [weakself.messsagesSource insertObjects:aMessages atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [aMessages count])]];
                
                id object = [weakself.dataArray firstObject];
                if ([object isKindOfClass:[NSString class]]) {
                    NSString *timestamp = object;
                    [formated enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id model, NSUInteger idx, BOOL *stop) {
                        if ([model isKindOfClass:[NSString class]] && [timestamp isEqualToString:model]) {
                            [weakself.dataArray removeObjectAtIndex:0];
                            *stop = YES;
                        }
                    }];
                }
                scrollToIndex = [weakself.dataArray count];
                [weakself.dataArray insertObjects:formated atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [formated count])]];
                
                dispatch_async(dispatch_get_main_queue(), ^{
                    [weakself.tableView reloadData];
                    
                    if (weakself.isFirstLoadMsg) {
                        weakself.isFirstLoadMsg = NO;
                        [weakself _scrollToBottomRow];
                    }
                });
            });
        }
        
        [weakself tableViewDidFinishTriggerHeader:YES reload:NO];
    };
    
    if ([EMDemoOptions sharedOptions].isPriorityGetMsgFromServer) {
        EMConversation *conversation = self.conversationModel.emModel;
        [EMClient.sharedClient.chatManager asyncFetchHistoryMessagesFromServer:conversation.conversationId conversationType:conversation.type startMessageId:messageId pageSize:5 completion:^(EMCursorResult *aResult, EMError *aError) {
            block(aResult.list, aError);
        }];
    } else {
        [self.conversationModel.emModel loadMessagesStartFromId:messageId count:5 searchDirection:EMMessageSearchDirectionUp completion:block];
    }
}

4、转发自己发送出去的图片失败

参考截图添加代码:

body.thumbnailLocalPath = imgPath;


5、在聊天页面点击查看过图片、视频消息后,回到会话列表页,再收到消息,未读消息数就不准确了

由于聊天页面(EMChatViewController)的block里使用了强引用self,造成了循环引用,导致退出聊天页面回到会话列表页面(EMConversationsViewController)时,聊天页面的 dealloc 方法没有执行,所以聊天页面的代理没有移除( [[EMClient sharedClient].chatManager removeDelegate:self]; );然后,用户在会话列表页面收到消息时,聊天页面的 messagesDidReceive 执行了,这个方法里将消息置为已读了,所以,导致会话列表页不显示未读消息数了。

按照截图修改:


6、汉字转拼音的工具类(EMChineseToPinyin)对各别汉字不识别

(1)拼音为er的汉字,例如“二”、“儿”

将 if(nCode >= /* DISABLES CODE */ (2288) && nCode ⇐ 2231)

改为 if(nCode >= 2288 && nCode ⇐ 2301)

(2)汉字“妗”

添加代码 case 7001:


7、EaseUI收到web端发来的视频消息显示“获取缩略图失败”,点击无法播放

按照截图修改:

EMMessageBody *msgBody = aMessage.body;
if (msgBody.type == EMMessageBodyTypeVideo) {
  EMVideoMessageBody *body = (EMVideoMessageBody *)msgBody;
  if (![body.thumbnailRemotePath isEqualToString:@""]) {
    [weakSelf showHint:@"获取缩略图失败!"];
  }
}

// 下载视频
 if (videoBody.type == EMMessageBodyTypeVideo) {
                EMVideoMessageBody *body = (EMVideoMessageBody *)videoBody;
                if ([body.thumbnailRemotePath isEqualToString:@""]) {
                    NSString *localPath = [model.fileLocalPath length] > 0 ? model.fileLocalPath : videoBody.localPath;
                    if ([localPath length] == 0) {
                        [[EMClient sharedClient].chatManager downloadMessageAttachment:model.message progress:nil completion:^(EMMessage *message, EMError *error) {
                            if (!error) {
                                NSLog(@"x下载成功!");
                                dispatch_async(dispatch_get_main_queue(), ^{
                                    NSURL *videoURL = [NSURL fileURLWithPath:body.localPath];
                                    AVPlayerViewController *playerViewController = [[AVPlayerViewController alloc] init];
                                    playerViewController.player = [AVPlayer playerWithURL:videoURL];
                                    playerViewController.videoGravity = AVLayerVideoGravityResizeAspect;
                                    playerViewController.showsPlaybackControls = YES;
                                    [self presentViewController:playerViewController animated:YES completion:^{
                                        [playerViewController.player play];
                                    }];
                                });
                            } else {
                                NSLog(@"下载失败---%@", error.errorDescription);
                            }
                        }];
                    } else {
                        NSURL *videoURL = [NSURL fileURLWithPath:localPath];
                        AVPlayerViewController *playerViewController = [[AVPlayerViewController alloc] init];
                        playerViewController.player = [AVPlayer playerWithURL:videoURL];
                        playerViewController.videoGravity = AVLayerVideoGravityResizeAspect;
                        playerViewController.showsPlaybackControls = YES;
                        [self presentViewController:playerViewController animated:YES completion:^{
                            [playerViewController.player play];
                        }];
                    }
                } else {
                    [weakSelf showHint:NSEaseLocalizedString(@"message.thumImageFail", @"thumbnail for failure!")];
                }
            }