开篇啰嗦:
好久没在写文章了,最近两个月断断续续的把腾讯云通信移植到工程里面来了。我们以前的工程是采用的混合开发(H5+),即时通讯用的环信那套,现在更换为腾讯云。先把其中的部分代码和经验分享给大家。希望大家能少走点弯路。
开发经费注意:环信的收费版是大概9000块一年,然后腾讯的收费版(超过100个用户):12000块一年。 我们公司采用腾讯云的初衷是看重其稳定,大厂的原因吧...(其实实际的功能需求来看的话,环信还做的更加直白简单些)
1.登录注册部分:
官方文档地址,文档怎么说呢,例子都有,但是还是有些坑。慢慢道来
登录注册的话有两种方式
一种是托管模式,那么就是注册和登录都由开发者调起API来进行注册和登录。账号也是由腾讯帮你管理的
另外一种就是独立模式:所谓独立模式就是需要你的后台也接入TIMSDK,账号的注册由后台来完成,你只需要负责在app端进行登录就好了
托管模式:
我只介绍了最简单的,就是用账号和密码进行注册和登录的情况:
注册:
//注册成功回调协议:
@interface DMRegisterController ()
//注册按钮:
- (IBAction)btnSubmitResponse:(UIButton *)sender {
if(self.tfPassword.text.length < 5){
NSLog(@"请输入8-16位密码");
}else{
[[TLSHelper getInstance] TLSStrAccountReg:self.tfUsername.text andPassword:self.tfPassword.text andTLSStrAccountRegListener:self];
}
//协议回调
//注册成功
- (void)OnStrAccountRegSuccess:(TLSUserInfo *)userInfo{
//注册成功后保存这个用户名在本地:
[userDefaults setObject:userInfo.identifier forKey:UserIdentifier];
[self dismissViewControllerAnimated:YES completion:nil];
}
//注册失败
- (void)OnStrAccountRegFail:(TLSErrInfo *)errInfo{
NSLog(@"注册错误信息--%@",errInfo.sErrorMsg);
}
登录:(登录就有个小坑,得登录两次)
一次是账号密码登录,一次是获得本地签名后的云通讯服务登录
普通登录:
//登录回调协议:
@interface DMLoginController ()
//登录按钮响应
- (IBAction)btnLoginResponse:(UIButton *)sender {
if (![self.tfUsername.text isEqualToString:@""] && ![self.tfPassword.text isEqualToString:@""]) {
[[TLSHelper getInstance] TLSPwdLogin:self.tfUsername.text andPassword:self.tfPassword.text andTLSPwdLoginListener:self];
}
//协议回调
//登录成功:
- (void)OnPwdLoginSuccess:(TLSUserInfo *)userInfo{
[self TLSLoginMethod];
[userDefaults setObject:userInfo.identifier forKey:UserIdentifier];
DMTabBarController *tabBar = [[DMTabBarController alloc]init];
UIWindow *window = [UIApplication sharedApplication].keyWindow;
window.rootViewController = tabBar;
}
//登录失败
- (void)OnPwdLoginFail:(TLSErrInfo *)errInfo{
NSLog(@"登录错误信息--%@",errInfo.sErrorMsg);
}
//登录腾讯云聊天:
- (void)TLSLoginMethod{
TIMLoginParam *param = [[TIMLoginParam alloc]init];
param.identifier = [userDefaults objectForKey:UserIdentifier];
param.appidAt3rd = @"1400136431";
param.userSig = [[TLSHelper getInstance] getTLSUserSig:[userDefaults objectForKey:UserIdentifier]];
[[TIMManager sharedInstance] login:param succ:^{
NSLog(@"登录腾讯云聊天成功");
//获取会话列表
// [self getConversationList];
} fail:^(int code, NSString *msg) {
NSLog(@"登录腾讯云聊天失败:%@",msg);
}];
}
TIMLoginParam 是TIM的登录信息类,放一些登录参数:
/**
* 用户名
*/
@property(nonatomic,strong) NSString* identifier;
/**
* 鉴权Token
*/
@property(nonatomic,strong) NSString* userSig;
/**
* App用户使用OAuth授权体系分配的Appid
*/
@property(nonatomic,strong) NSString* appidAt3rd;
至此,这个托管模式的账号注册和登录就搞定了。这种注册登录是最简单的,腾讯云还提供了手机号加短信验证码等注册登录方式,根据个人需求采用吧。
特别说明一下腾讯云这个用户鉴权Token:userSig. 其实就是一个签名认证Token,为安全性考虑的,不像环信直接账号密码登录的。
独立模式:
所谓独立模式其实就是账号的登录过程交给服务端的伙伴完成,我们只需要拿到服务端返回的鉴权Token进行登录就好了
-(void)loginTIMAsync:(PGMethod *)commands {
NSString *cellnumber = [commands.arguments objectAtIndex:1];
NSString *password = [commands.arguments objectAtIndex:2];
NSString *imAccountSig = [commands.arguments objectAtIndex:3];
//先保存,后面掉线使用
[[NSUserDefaults standardUserDefaults] setValue:cellnumber forKey:@"cellnumber"];
[[NSUserDefaults standardUserDefaults] setValue:password forKey:@"password"];
[[NSUserDefaults standardUserDefaults] synchronize];
//JJTIM-登录腾讯云
NSLog(@"传过来的账号和密码,签名:是:%@,%@,%@",cellnumber,easeMobPassword,imAccountSig);
[userDefaults setObject:cellnumber forKey:UserIdentifier];
[self TLSRealLoginMethod:imAccountSig];
}
loginTIM方法简单说明下,因为我们是混合开发,所以这个账号和签名是H5端登录后写插件传到原生界面的。
- (void)TLSRealLoginMethod:(NSString *)imAccountSig{
TIMLoginParam *param = [[TIMLoginParam alloc]init];
param.identifier = [userDefaults objectForKey:UserIdentifier];
param.appidAt3rd = @"1400136431";
param.userSig = imAccountSig;
[userDefaults setObject:imAccountSig forKey:ImAccountSig];
[[TIMManager sharedInstance] login:param succ:^{
NSLog(@"登录腾讯云聊天成功");
//获取会话列表
[self getConversationList];
//获取自己的昵称和头像:
[self getNickNameAndHeadImg];
} fail:^(int code, NSString *msg) {
NSLog(@"登录腾讯云聊天失败:%@",msg);
}];
}
至此独立模式的登录TIM聊天服务也完成了,下面来说说这个收发消息和群管理方面的了
收发消息
发送消息:
对于我公司项目的聊天需求是有:文字,图片,地理位置,语音,文件,自定义表情这几种,然后加上我的聊天界面也是自己写的,所以我的DMMessageVC就显得相当庞大了。这里只介绍消息的发送用法,不会介绍具体的UI实现和功能实现(实在是写得有点乱,网上有很多写得不错的聊天界面可以参考学习)
/**
发送消息的方法
@param type 单聊/群聊
@param chatId 聊天id
@param messageType 消息类型
@param textMessage 文本消息
@param photoPath 照片消息
@param location 位置信息
@param file 文件信息
*/
- (void)sendMessage:(TIMConversationType)type
chatId:(NSString *)chatId
messageType:(NSInteger)messageType
textMessage:(NSString *)textMessage
photoPath:(NSString *)photoPath
location:(TIMLocationElem *)location
file:(TIMFileElem *)file
sound:(TIMSoundElem *)soundElem
face:(TIMFaceElem *)faceElem{
//获取会话对象:
//单聊
//chatName实际就是identifer
TIMConversation *c2c_conversation = [[TIMManager sharedInstance] getConversation:type receiver:chatId];
WEAKSELF;
if (self.messageType == MessageType_Text) {
//文本消息:
TIMTextElem *text_elem = [[TIMTextElem alloc] init];
[text_elem setText:textMessage];
TIMMessage *msg = [[TIMMessage alloc] init];
[msg addElem:text_elem];
[c2c_conversation sendMessage:msg succ:^{
NSLog(@"消息发送成功");
[weakSelf sendNotiSuccessMessage];
[weakSelf updateLocalMessageList:YES messageType:weakSelf.messageType];
} fail:^(int code, NSString *msg) {
NSLog(@"文本消息发送失败:%@,错误码:%d",msg,code);
[weakSelf sendMessageFailMethod:code];
[weakSelf.dataArr removeLastObject];
}];
}else if (self.messageType == MessageType_Photo){
//图片信息
TIMImageElem *image_elem = [[TIMImageElem alloc]init];
image_elem.path = photoPath;
TIMMessage *msg = [[TIMMessage alloc]init];
[msg addElem:image_elem];
[c2c_conversation sendMessage:msg succ:^{
NSLog(@"图片消息发送成功");
[weakSelf sendNotiSuccessMessage];
[[NSNotificationCenter defaultCenter] postNotificationName:@"photoSuccess" object:nil userInfo:nil];
} fail:^(int code, NSString *msg) {
NSLog(@"图片消息发送失败:%@,错误码:%d",msg,code);
//如果图片消息发送失败,需要删除self.dataArr的最后一个元素
[weakSelf.dataArr removeLastObject];
[weakSelf sendMessageFailMethod:code];
}];
}else if (self.messageType == MessageType_Location){
TIMLocationElem *elem = location;
TIMMessage *msg = [[TIMMessage alloc]init];
[msg addElem:elem];
[c2c_conversation sendMessage:msg succ:^{
NSLog(@"地理位置消息发送成功");
[weakSelf sendNotiSuccessMessage];
[weakSelf updateLocalMessageList:YES messageType:weakSelf.messageType];
} fail:^(int code, NSString *msg) {
NSLog(@"地理位置消息发送失败:%@,错误码:%d",msg,code);
[weakSelf.dataArr removeLastObject];
[weakSelf sendMessageFailMethod:code];
}];
}else if (self.messageType == MessageType_Sound){
//语音消息:
TIMMessage *msg = [[TIMMessage alloc]init];
[msg addElem:soundElem];
[c2c_conversation sendMessage:msg succ:^{
NSLog(@"语音消息发送成功");
[weakSelf updateLocalMessageList:YES messageType:weakSelf.messageType];
[weakSelf sendNotiSuccessMessage];
} fail:^(int code, NSString *msg) {
NSLog(@"语音消息发送失败:%@,错误码:%d",msg,code);
[weakSelf.dataArr removeLastObject];
[weakSelf sendMessageFailMethod:code];
}];
}else if (self.messageType == MessageType_File){
//文件消息
TIMMessage *msg = [[TIMMessage alloc]init];
[msg addElem:file];
[c2c_conversation sendMessage:msg succ:^{
NSLog(@"文件消息发送成功");
[weakSelf sendNotiSuccessMessage];
[weakSelf updateLocalMessageList:YES messageType:weakSelf.messageType];
} fail:^(int code, NSString *msg) {
NSLog(@"文件消息发送失败:%@,错误码:%d",msg,code);
[weakSelf.dataArr removeLastObject];
[weakSelf sendMessageFailMethod:code];
}];
}else if (self.messageType == MessageType_Face){
//专属表情消息
TIMMessage *msg = [[TIMMessage alloc]init];
[msg addElem:faceElem];
[c2c_conversation sendMessage:msg succ:^{
NSLog(@"发送表情消息成功");
[weakSelf sendNotiSuccessMessage];
[weakSelf updateLocalMessageList:YES messageType:weakSelf.messageType];
}fail:^(int code, NSString *msg) {
NSLog(@"发送表情消息失败:%d,%@",code,msg);
[weakSelf.dataArr removeLastObject];
[weakSelf sendMessageFailMethod:code];
}];
}
}
其实发送消息的方法很简单,主要是基于消息基类TIMElem进行各种消息的处理
[weakSelf updateLocalMessageList:YES messageType:weakSelf.messageType];
更新tableView的数据处理就又是一大推了,这个不涉及到TIM的东西
收到消息:
首先需要写个DMMessageListenerlmpl类继承自NSObject,实现TIMMessageListener协议
@interface DMMessageListenerImpl : NSObject
/**收到的消息数组*/
@property (nonatomic, strong) NSMutableArray *receiveArr;
- (void)onNewMessage:(TIMMessage *)msg;
@end
#import "DMMessageListenerImpl.h"
@implementation DMMessageListenerImpl
- (void)onNewMessage:(NSArray *)msgs{
NSLog(@"收到的消息是:%@",msgs);
[self assigmentMessageWithArr:msgs];
}
- (void)assigmentMessageWithArr:(NSArray *)msgs{
self.receiveArr = [NSMutableArray array];
if (msgs.count > 0) {
[self.receiveArr addObject:msgs.firstObject];
}
NSMutableDictionary *dic = [NSMutableDictionary dictionary];
[dic setValue:self.receiveArr forKey:RECEIVE_MSG_DIC];
[[NSNotificationCenter defaultCenter] postNotificationName:RECEIVE_MSG_NOTI object:nil userInfo:dic];
}
其次调用配置方法加入TIMManager
//收到消息回调配置
DMMessageListenerImpl *impl = [[DMMessageListenerImpl alloc] init];
[[TIMManager sharedInstance] addMessageListener:impl];
这个放在appDelegate中就好了
我的收到消息回调处理是通过通知传到聊天界面在其中处理的:
- (void)receiveMessage:(NSNotification *)noti{
NSMutableArray *arr = [[noti userInfo] objectForKey:RECEIVE_MSG_DIC];
TIMMessage *msg = arr.firstObject;
if ([self.currentMessage isEqual:msg]) {
NSLog(@"重复消息");
return;
}else{
self.currentMessage = msg;
}
NSString *sender = self.currentMessage.sender;
int cnt = [msg elemCount];
for (int i = 0; i < cnt; i++) {
TIMElem *elem = [msg getElem:i];
if ([elem isKindOfClass:[TIMTextElem class]]) {
//文本消息
TIMTextElem *text_elem = (TIMTextElem *)elem;
[self updateReceiveTextMessage:text_elem sender:sender];
}
else if ([elem isKindOfClass:[TIMImageElem class]]){
//图片消息
TIMImageElem *image_elem = (TIMImageElem *)elem;
[self updateReceivePhotoMessage:image_elem sender:sender];
}else if ([elem isKindOfClass:[TIMLocationElem class]]){
//地理位置消息
TIMLocationElem *locationElem = (TIMLocationElem *)elem;
[self updateReceiveLocationMessage:locationElem sender:sender];
}else if ([elem isKindOfClass:[TIMSoundElem class]]){
//语音消息:
TIMSoundElem *soundElem = (TIMSoundElem *)elem;
[self updateReceiveVoiceMessage:soundElem sender:sender];
}else if ([elem isKindOfClass:[TIMFileElem class]]){
//文件消息
TIMFileElem *fileElem = (TIMFileElem *)elem;
[self updateReceiveFileMessage:fileElem sender:sender];
}else if ([elem isKindOfClass:[TIMFaceElem class]]){
//专属表情消息
TIMFaceElem *faceElem = (TIMFaceElem *)elem;
[self updateReceiveFaceMessage:faceElem sender:sender];
}
}
}
1.收到消息第一个注意点是消息通过代理的消息回调,有时TIM会回调多次,造成重复接收一样的消息,我处理就是用一个currentMsg来过滤一下。
NSString *sender = self.currentMessage.sender;
这个当前消息的发送者,就是当前发送消息人的账号,我这里获取这个是为群聊天中对方昵称和头像做准备。
至此简单的收发消息就可以了,但是光看我的代码可能会有点蒙,因为还有许多处理是和消息体的准备相关的(语音,文件,地理位置等),其次是各类型的消息cell的展现。所以说如果能找到合适的聊天框架,那么可以在此基础上替换和自定义,不然完全自己实现还是一个不小的工作量了。
用户登录状态监听:
这个状态监听,是我们项目以前就是用环信来监控这个设备二次登录的,所以TIM这边也要用。
首先也是要写个类继承TIMUserStatusListener协议:
@interface DMUserStatusListener : NSObject {
}
- (void)onForceOffline;
- (instancetype)initWithController:(UIViewController *)vc;
@interface DMUserStatusListener ()
/**vc*/
@property (nonatomic, strong) UIViewController *vc;
@end
@implementation DMUserStatusListener
- (instancetype)initWithController:(UIViewController *)vc{
if(self = [super init]){
self.vc = vc;
}
return self;
}
//被踢下线:
- (void)onForceOffline{
UIAlertController *alertVc = [UIAlertController alertControllerWithTitle:@"下线通知" message:@"您的账号在其他设备登录" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *sureAction = [UIAlertAction actionWithTitle:@"重新登录" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
//重新登录TIM:
TIMLoginParam *param = [[TIMLoginParam alloc]init];
param.identifier = [userDefaults objectForKey:UserIdentifier];
param.appidAt3rd = @"1400136431";
param.userSig = [userDefaults objectForKey:ImAccountSig];
[[TIMManager sharedInstance] login:param succ:^{
NSLog(@"重新登录TIM成功");
} fail:^(int code, NSString *msg) {
NSLog(@"登录TIM失败:%@",msg);
}];
}];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"退出" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
NSLog(@"退出应用");
}];
[alertVc addAction:cancelAction];
[alertVc addAction:sureAction];
[self.vc presentViewController:alertVc animated:YES completion:nil];
}
//userSig过期
- (void)onUserSigExpired {
NSLog(@"userSig expired");
}
然后是配置,配置就有点坑了,按照文档中就直接少了一步,加入配置的步骤,不知道是不是在其它地方有写到,耽搁了小半天。
TIMManager *manager = [TIMManager sharedInstance];
TIMSdkConfig *config = [[TIMSdkConfig alloc]init];
config.sdkAppId = 1400136431;
config.accountType = TIMAccountType;
int statusNumber = [manager initSdk:config];
if(0 == statusNumber){
NSLog(@"初始化TIM成功");
//成为腾讯云用户状态监听的代理:
self.userConfig = [[TIMUserConfig alloc]init];
self.lister = [[DMUserStatusListener alloc]initWithController:viewCon];
self.userConfig.userStatusListener = self.lister;
[[TIMManager sharedInstance] setUserConfig:self.userConfig];
}else{
NSLog(@"初始化TIM失败");
}
[[TIMManager sharedInstance] setUserConfig:self.userConfig];
官方文档中没有这一步,要注意。 其次设置的位置也有说明: 在initSdk:后调用,login:前调用