======= 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秒左右,接收消息没有通知栏提示的话,这个情况要配置本地通知,先先注册本地通知(系统方法),然后在看下文档介绍: [[http://docs-im.easemob.com/im/ios/apns/deploy#%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E6%9C%AC%E5%9C%B0%E9%80%9A%E7%9F%A5|如何实现本地通知]] 2).如果不是本地通知,app退到后台超过150秒左右,接收消息没有通知栏提示的话,这个情况属于apns推送。 如果没有配置apns推送,那么先按照文档配置:[[http://docs-im.easemob.com/im/ios/apns/deploy#apns%E6%8E%A8%E9%80%81​​ | 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:[[http://www.easemob.com/download/im | Demo下载链接]] ---- **4、APNS推送收不到怎么办?** 如果是iOS13及以上的系统,那么需要将sdk更新到3.6.4或以上版本。如果更新后还不行那么退出登录、重启app、再登录试下。 如果不是iOS的原因,可根据以下链接中的内容进行排查:[[https://kefu.easemob.com/v1/webimplugin/tenants/9211/robot/article/html/c47c03b5-a437-4533-b1d3-05027b439a2b | 推送排查文档]] 如果以上都确认无误,请将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推送通知栏如何显示消息详情?** 参考文档设置:[[http://docs-im.easemob.com/im/ios/apns/offline#%E8%AE%BE%E7%BD%AE%E6%8E%A8%E9%80%81%E6%98%BE%E7%A4%BA%E8%AF%A6%E6%83%85 | 推送显示详情]] ---- **8、如果没有登录环信,可以收到推送吗?** 1.注册完没有登录过的用户。——— 不会收到推送,因为没有绑定devicetoken,只有登录后才会绑定。 2.登录后,退出登录。 ——— 要看调用环信SDK的退出登录方法传的是NO还YES,传YES是解绑devicetoken,不会收到推送。 传NO是不解绑devicetoken,会收到推送,但不建议传NO,如果多个账号在同一台设备登录过的情况,会出现多个用户绑定同一个devicetoken,这样多个用户的离线消息都会推送到同一台设备上。 3.登录后。直接杀掉app或者app在后台,锁屏超过3分钟。——— 这种情况用户的长连接会环信服务器断开,但devicetoken不会解绑,有离线消息的话就会有推送。 ---- ======= iOS端使用问题 ======= ---- **1、从环信服务器上下载消息中的附件没有后缀怎么办?** 附件类型的消息(图片,语音,视频,文件消息等)发送到环信服务器后是不带后缀的,这个可以在构建消息的时候,给附件消息命名时加上后缀名 比如图片消息的话,可以给displayName设置image.png [[http://docs-im.easemob.com/im/ios/basics/message#%E6%9E%84%E9%80%A0%E6%B6%88%E6%81%AF | 构建图片消息]] {{:faq:im:构造图片消息.jpg|}} 这样接收消息方在接收到消息时,可以从消息的body中通过displayName拿到名称就可以知道附件的格式,地理位置消息的话,可以在发送消息的时候,给消息的ext里面加个参加把附件名称和格式带上,这样接收消息方通过解析消息的ext也可以知道附件消息的类型。 ---- **2、环信id是什么?** 环信id就是指在环信appkey下注册的账号并且是唯一的,用于客户端环信SDK登录以及收发消息使用的。 在注册环信 id 时,建议不要使用有序的 id 进行注册,防止其他人知道注册 id 的顺序,恶意发送大量的垃圾消息。 一般是服务器端给自己用户注册账号的同时,在调用环信的rest接口给用户在注册一个环信id与自己的用户绑定,并返回给客户端,客户端拿到自己服务器用户的账号密码登录后,在拿环信id密码在登录环信服务器。[[http://docs-im.easemob.com/im/server/ready/user#%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86 | 用户管理]] 社区版的appkey只能注册100个环信id,企业版没有注册的限制。​ ---- **3、开启消息漫游之后 为什么不可以进行删除消息的操作了?** 删除聊天记录是删除本地数据库的,不是环信服务器的,开通漫游消息后,如果调用的是从环信服务器获取历史消息的api(asyncFetchHistoryMessagesFromServer),就会出现这种情况。 您可以自己记录下删除了哪条消息,然后在加载历史消息的时候遍历过滤不展示;或者使用从本地数据库获取历史消息的api ---- **4、获取会话列表为空情况有以下几种:** 1)可能是本地数据库中没有会话或者将会话都删除掉了 2)没有登录环信成功,SDK数据库没有打开,取不到会话列表 3)可能是老版本SDK的问题,可以尝试先升级到最新版本的SDK,在测试下:[[http://www.easemob.com/download/im​ | 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端创建会话,插入消息,以难度大的图片消息为例 创建会话:[[http://docs-im.easemob.com/im/ios/basics/message#%E6%96%B0%E5%BB%BA%E8%8E%B7%E5%8F%96%E4%B8%80%E4%B8%AA%E4%BC%9A%E8%AF%9D | 创建会话​]] 构造图片消息:[[http://docs-im.easemob.com/im/ios/basics/message#%E6%9E%84%E9%80%A0%E6%96%87%E5%AD%97%E6%B6%88%E6%81%AF​ | 构建图片消息]] 插入消息:[[http://docs-im.easemob.com/im/ios/basics/message#%E6%96%B9%E6%B3%95%E4%BA%8C | 插入消息​​]] 插入图片消息示例代码:     // 创建会话,以单聊会话为例     // 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的发送消息方法发送文件,参考文档:[[http://docs-im.easemob.com/im/ios/basics/message#%E6%9E%84%E9%80%A0%E6%96%87%E4%BB%B6%E6%B6%88%E6%81%AF​ ​| 构建文件消息]] 聊天页面的工具栏是可以扩展的,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情况用这个回调监听:[[http://docs-im.easemob.com/im/ios/sdk/basic#重连 | 重连]]'' ''5情况用这个回调监听:[[http://docs-im.easemob.com/im/ios/sdk/basic#被动退出登录 | 被动退出登录]]'' ''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”** {{:faq:im:callkit报错.jpg|}} 解决方案: 由于EaseCallKit是动态库,在podfile中必须加入use_frameworks! 参考文档:[[http://docs-im.easemob.com/im/ios/other/easecallkit#导入ui库|导入UI库]] ---- **1、报错信息:“Reason: image not found”** {{:faq:im:image_not_found_1.jpg|}} 解决方案: 在TARGETS → General → Frameworks, Libraries, and Embedded Content中添加Hyphenate.framework依赖库,且依赖库的Status必须是Embed & Sign ---- **2、报错信息:** {{:faq:im:删掉重新拖.jpg|}} 解决方案: 删掉重新添加试一下。添加时先粘贴到项目的finder文件内,再从finder往项目中拖。 ---- **3、报错信息:** {{:faq:im:plugin报错_1.jpg|}} 解决方案: 把plugin文件夹删掉(删掉不会影响到功能)。 {{:faq:im:plugin报错_2.jpg|}} ---- **4、xcode12.3运行报错,报错信息:** {{:faq:im:xcode12.3运行报错.jpg|}} 解决方案: 打开Xcode,左上方点击File—>Workspace Settings—>将Shard Workspace Settings:中的New Build System(Default)修改成Legacy Build System,点击done,再试下。 {{:faq:im:xcode12.3运行报错1.jpg|}} ---- **5、报错信息:i386** {{:faq:im:报错i386.png?|}} 解决方案: {{:faq:im:报错i386方案.jpg?|}} =====打包报错 ===== ---- **1、报错信息:** {{:faq:im:没有剔除.jpg|}} 解决方案:参考官方文档[[http://docs-im.easemob.com/im/ios/sdk/prepare#集成动态库上传appstore|剔除 i386、x86_64 两个平台]] ---- **2、报错信息:** {{:faq:im:bitcode_1.jpg|}} 解决方案:打包时取消勾选bitcode {{:faq:im:bitcode_2.jpg?600|}} ---- =====v1.0版本视频会议Demo报错===== ---- Demo下载地址:[[http://www.easemob.com/download/rtc|环信音视频云下载]] 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 执行完成后运行会报错,报错信息:** {{:faq:im:视频会议demo报错1.jpg|}} 解决方案: 打开xcode,最上方点击File—>Workspace Settings—>将Shard Workspace Settings:中的New Build System(Default)修改成Legacy Build System,点击done,最后运行项目。 **2、报错信息:** {{:faq:im:视频会议demo报错2.jpg|}} 解决方案: {{:faq:im:视频会议demo报错3.jpg|}} **3、xcode12模拟器运行报错,报错信息:** {{:faq:im:xcode12报错.jpg|}} 解决方案: Build Settings里最下面的VALID_ARCHS,添加上x86_64。 {{:faq:im:xcode12报错1.jpg|}} ---- =====使用报错 ===== ---- **iOS登录报303是什么原因?** 报303一般是用户客户端网络不好没有连接上环信服务器,可以切换网络再登录试试(4G网络可以试试),如果开了VPN的话,将VPN关掉。 如果还是登录不了的话,提供下登录不了的环信id和密码,提交工单联系环信技术。 ---- ======功能实现方案====== ---- **1、个人消息免打扰** 自己服务器端维护下好友免打扰列表,发消息前判断下对方有没有将自己设置为免打扰,如果设置了,就用静默消息,文档:[[http://docs-im.easemob.com/im/ios/apns/content#发送静默消息|发送静默消息]],这样在离线时,就不会有这个人发来的消息的离线推送。 在线和后台活跃时,在收到消息的代理方法中( - (void)messagesDidReceive:(NSArray *)aMessages)​判断消息的from在不在自己的免打扰列表内,如果在,就不播放声音。 ---- **2、接收群组消息但不提示(消息免打扰)** (1)先屏蔽这个群组的离线推送,参考文档:[[http://docs-im.easemob.com/im/ios/apns/offline#设置指定群组是否接收_apns|设置指定群组是否接收_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。 注:只能删除本地数据库的,无法删除环信服务器端的。如果使用漫游功能从环信服务器端获取历史消息,还是能把撤回的那条消息拉取下来,这时候还要过滤下再删掉。 构造透传消息文档:[[http://docs-im.easemob.com/im/ios/basics/message#构造透传消息|构造透传消息]] 构造扩展消息文档:[[http://docs-im.easemob.com/im/ios/basics/message#构造扩展消息|构造扩展消息]] 删除消息: {{:faq:im:删除消息.jpg|}} ---- **4、消息提示音(消息提醒、震动、响铃)** 在线消息:可以全局(在根控制器或者AppDelegate​里)​监听收到消息的代理方法(messagesDidReceive​),在这个方法里播放响铃和震动。 离线消息:可以自定义推送提示音,参考文档:[[http://docs-im.easemob.com/im/ios/apns/content#自定义推送提示音|自定义推送提示音]],但这个只能是播放本地音频文件,无法震动。想要震动,可以在发消息时,加"mutable-content":1 字段,参考文档:[[http://docs-im.easemob.com/im/ios/apns/content#开启_apns_通知扩展|开启_apns_通知扩展]],那么环信服务器给苹果apns推送服务器发消息时,也会把这个字段传给苹果那边,苹果那边识别到有"mutable-content":1字段,会假唤醒app,配置NotificationServiceExtension之后,您可以播放响铃和震动​,具体可以看苹果NotificationServiceExtension的文档介绍。 ---- **5、iOS在线与离线未读消息数累加** 在线未读消息数与离线消息数累加的功能,可以按照下面的方案实现 在发消息时,加"mutable-content":1 扩展字段,参考iOS端文档:[[http://docs-im.easemob.com/im/ios/apns/content#%E5%BC%80%E5%90%AF_apns_%E9%80%9A%E7%9F%A5%E6%89%A9%E5%B1%95|开启 APNs 通知扩展]], 那么环信服务器给苹果apns推送服务器发消息时,也会把这个字段传给苹果那边,苹果那边识别到有"mutable-content":1字段,会假唤醒app,配置NotificationServiceExtension之后,您可取到环信的推送角标数,然后自己再修改推送通知栏显示的样式和角标数,具体可以看苹果NotificationServiceExtension的文档介绍。 (1)先新建一个NotificationServiceExtension的Targets (2)主工程打开Remote notifications权限 (3)两个Targets都打开App Groups,并添加共享沙盒 (4)主工程将当前本地未读消息数动态存到共享沙盒 (5)在NotificationService中取出共享沙盒的角标数,与收到的APNs角标数累加重置。 {{:faq:im:在线与离线消息数累加-1.jpg|}} 存角标数使用的是App Groups: {{:faq:im:在线与离线消息数累加-2.jpg|}} ---- **6、不是好友不让发消息** 环信目前的机制是只要知道对方的环信id就可以给对方发消息,环信有好友的机制,对应的功能可以看下文档介绍:[[http://docs-im.easemob.com/im/ios/basics/buddy#%E5%A5%BD%E5%8F%8B%E7%AE%A1%E7%90%86​​ | 好友功能]] 如果使用环信的好友功能,比如A与B不是好友,那么A进入与B的聊天页面时,可以获取A的好友列表看是否存在B,如果不存在可以在UI上做限制,不让A发送消息。 如果使用的是自己的好友体系,那么就换成自己的业务进行判断是不是好友,从而在UI上做是否可以发送消息的限制。 ---- **7、用环信的消息做给自己的用户全量推送功能** 如果想要环信的消息来做给自己用户全量推送功能,还不想让客户端用户看到这条会话,那么可以用环信服务器端rest接口批量发送消息实现,可以使用一个或者几个固定的环信id作为from批量给自己的用户发送消息(推送消息),然后客户端在取会话列表的时候,发现会话id如果是服务器端用于批量发消息的环信id,那么就将这个会话删除掉即可。 文档:[[http://docs-im.easemob.com/im/server/basics/messages#%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF | rest发送消息接口]] [[http://docs-im.easemob.com/im/ios/basics/message#%E5%88%A0%E9%99%A4%E4%BC%9A%E8%AF%9D | iOS端删除会话]] ---- **8、iOS EaseUI如何自定义表情** 参考链接中的demo实现自定义表情即可[[http://www.jianshu.com/p/e92835229566 | 自定义表情]] ---- **9、iOS EaseUI聊天页面怎么显示群组的昵称头像** 因为环信这边是不涉及用户个人信息的,所以通过环信id是获取不到用户昵称头像的,用户的个人信息可以在自己服务器与环信id绑定存储维护,知道环信id就可以到自己服务器下载这个环信id对应的用户信息(注意在注册环信id时传的昵称并不是个人信息的昵称,那个是在设置显示推送通知栏详情时,显示的推送昵称)。​​ 环信UI上的处理,如果使用的是EaseUI的话,我建议是在EaseBaseMessageCell.m​类, - (void)setModel:(id)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相关":[[https://pan.baidu.com/s/1rq8u-TPN_0xl7pnhQ-GouQ | 自定义cell]] ---- **12、用户撤回消息,实时消息回调会怎样处理?** 撤回消息的事件是没有回调的,可以这样,如果想通过实时消息回调知道哪条消息是撤回的消息,那么在客户端撤回消息后,在发条cmd消息,cmd消息的ext里面加上撤回消息的标识然后在加上撤回消息的消息id,这样这条cmd消息也会回调到你们服务器上,这样就可以知道哪条消息是撤回消息了 ---- **13、如何知道对方的环信id是否在线** 可以在客户端先请求自己的服务器,然后让服务器端调用环信获取用户在线状态的接口,将用户是否在线的状态返回给客户端即可:[[http://docs-im.easemob.com/im/server/ready/user#%E5%9C%A8%E7%BA%BF%E4%B8%8E%E7%A6%BB%E7%BA%BF | 用户是否在线]] ---- ====== Demo现存bug ====== ---- **1、新版UI,点击群组语音消息后,未读红点不消失** 在EMChatViewController.m​类, - (void)_audioMessageCellDidSelected:(EMMessageCell *)aCell​方法中,在截图中标注的位置添加以下代码 {{:faq:im:群组语音消息红点.jpg|}} 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​方法中,在截图中标注的位置修改成以下代码​ {{:faq:im:群组语音消息红点-1.jpg|}} 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 {{:faq:im:工具栏遮挡消息.jpg|}} 改成这样 [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、聊天页面发送过消息后,第一次刷新会出现重复的数据** 按照以下来修改: {{:faq:im:刷新数据重复1.jpg|}} {{:faq:im:刷新数据重复2.jpg|}} - (NSMutableArray *)messsagesSource { if (_messsagesSource == nil) { _messsagesSource = [NSMutableArray array]; } return _messsagesSource; } {{:faq:im:刷新数据重复3.jpg|}} [weakself.messsagesSource addObject:msg]; {{:faq:im:刷新数据重复4.jpg|}} [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; {{:faq:im:转发自己发送的图片失败.jpg|}} ---- **5、在聊天页面点击查看过图片、视频消息后,回到会话列表页,再收到消息,未读消息数就不准确了** 由于聊天页面(EMChatViewController)的block里使用了强引用self,造成了循环引用,导致退出聊天页面回到会话列表页面(EMConversationsViewController)时,聊天页面的 dealloc 方法没有执行,所以聊天页面的代理没有移除( [[EMClient sharedClient].chatManager removeDelegate:self]; );然后,用户在会话列表页面收到消息时,聊天页面的 messagesDidReceive 执行了,这个方法里将消息置为已读了,所以,导致会话列表页不显示未读消息数了。 按照截图修改: {{:faq:im:点击图片消息后未读消息数不准确1.jpg|}} {{:faq:im:点击图片消息后未读消息数不准确2.jpg|}} ---- **6、汉字转拼音的工具类(EMChineseToPinyin)对各别汉字不识别** (1)拼音为er的汉字,例如“二”、“儿” 将 if(nCode >= /* DISABLES CODE */ (2288) && nCode <= 2231) 改为 if(nCode >= 2288 && nCode <= 2301) {{:faq:im:汉字转拼音er.jpg|}} (2)汉字“妗” 添加代码 case 7001: {{:faq:im:汉字转拼音妗.jpg|}} ---- **7、EaseUI收到web端发来的视频消息显示“获取缩略图失败”,点击无法播放** 按照截图修改: {{:faq:im:easeui收到web端视频消息显示获取缩略图失败_无法播放1.jpg|}} EMMessageBody *msgBody = aMessage.body; if (msgBody.type == EMMessageBodyTypeVideo) { EMVideoMessageBody *body = (EMVideoMessageBody *)msgBody; if (![body.thumbnailRemotePath isEqualToString:@""]) { [weakSelf showHint:@"获取缩略图失败!"]; } } {{:faq:im:easeui收到web端视频消息显示获取缩略图失败_无法播放2.jpg|}} // 下载视频 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!")];                 }             } ----