XMPP实现IM(四):聊天能力的实现

目录

一、发送文本消息
1、发送文本消息的实现
2、文本聊天气泡的绘制
3、键盘输入框的自定义
二、发送图片消息和音频消息
1、发送图片消息和音频消息的实现
2、图片聊天气泡的绘制
3、音频聊天气泡的绘制
4、音频录音器和音频播放器的封装


前言


IM的核心功能是聊天,这一篇我们将使用XMPP实现聊天,不过需要注意的是前面两篇的好友列表和电子名片,我们本地有持久化的数据,openfire服务器上也保存着有数据,所以如果卸载了App,重新下载后也是可以拉取到数据的,但是openfire服务器上并没有帮我们存储聊天记录,只是保存在了手机本地,卸载之后聊天记录就没了。

我们主要实现的是发送文本、图片和音频消息,其它消息可依此扩展,下面会分别实现。

同样,我们先把聊天相关的类和方法先列在这里,也很少。

-----------XMPPMessage相关-----------

XMPPMessage:是XMPPFramework的消息类,我们发送和接收的消息都是这个类型。


// 构建消息方法:我们可以使用该方法构建一个发送给指定联系人的消息
+ (XMPPMessage *)messageWithType:(NSString *)type to:(XMPPJID *)to;

// 添加消息体方法:我们可以使用该方法为XMPPMessage对象添加消息体
- (void)addBody:(NSString *)body;

// 添加附件方法:我们可以使用该方法为XMPPMessage对象添加附件(如图片、音频、视频、文件等)
- (void)addChild:(DDXMLNode *)child;
-----------XMPPStream相关-----------

// 发消息方法:我们可以使用这个方法,把消息(即XMPPMessage对象发送出去)
- (void)sendElement:(NSXMLElement *)element;


#pragma mark - XMPPStreamDelegate

// 消息发送成功的回调
- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message;

// 消息发送失败的回调
- (void)xmppStream:(XMPPStream *)sender didFailToSendMessage:(XMPPMessage *)message error:(NSError *)error;

// 接收到新消息的回调
- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message;


一、发送文本消息


首先我们要做的一个工作就是,在初始化ProjectXMPP单例时初始化聊天记录本地存储的一些东西,这不仅仅是针对文本消息的持久化,图片、音频等消息也需要同一的操作,现在一块儿做好。

-----------ProjectXMPP.m-----------

// 聊天记录
self.messageArchivingCoreDataStorage = [XMPPMessageArchivingCoreDataStorage sharedInstance];
self.messageArchiving = [[XMPPMessageArchiving alloc] initWithMessageArchivingStorage:self.messageArchivingCoreDataStorage dispatchQueue:dispatch_get_main_queue()];
[self.messageArchiving activate:self.stream];
self.messageContext = self.messageArchivingCoreDataStorage.mainThreadManagedObjectContext;

然后我们需要实现一下从本地数据库读取聊天记录,想好友列表和电子名片XMPPFramework已经默认帮我们做好了存储和读取,我们只需要调用API就行了,但是聊天记录只是默认做好了存储,而读取则需要我们自己写方法去数据库里读取。这方法是固定的,复制一下就可以了,读取出来的聊天记录会放在一个数组里给我们返回。

-----------ProjectXMPP.m-----------

- (NSArray *)fecthMessageRecordWithFriendJID:(XMPPJID *)friendJID {
    
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"XMPPMessageArchiving_Message_CoreDataObject" inManagedObjectContext:self.messageContext];
    [fetchRequest setEntity:entity];
    
    // 聊天记录的查询条件:自己的账号和好友的账号
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"streamBareJidStr == %@ AND bareJidStr == %@", self.stream.myJID.bare, friendJID.bare];
    [fetchRequest setPredicate:predicate];
    
    // 查询结果的排序条件:按时间升序排序
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"timestamp"
                                                                   ascending:YES];
    [fetchRequest setSortDescriptors:[NSArray arrayWithObjects:sortDescriptor, nil]];
    
    NSError *error = nil;
    NSArray *fetchedObjects = [self.messageContext executeFetchRequest:fetchRequest error:&error];
    
    return fetchedObjects;
}
1、发送文本消息的实现

然后我们再写一个给指定联系人发送文本消息的方法。

-----------ProjectXMPP.m-----------

- (void)sendTextMessage:(NSString *)text toFriend:(XMPPJID *)friendJID {
    
    // 构建文本消息:消息的类型,消息要发给谁,消息体
    XMPPMessage *message = [XMPPMessage messageWithType:@"chat" to:friendJID];
    [message addBody:text];
    [self.stream sendElement:message];
}

好了,接下来我们就转战到聊天界面ChatViewController干活。

在这个界面里,我们要监听消息发送成功和失败,还有监听是否接收到消息,所以首先要把该界面作为stream的代理,并实现上面列出的三个代理方法,我们现在写做简单点,把功能实现了,后面会慢慢做UI。

-----------ChatViewController.m-----------

// 设置代理
[[ProjectXMPP sharedXMPP].stream addDelegate:self delegateQueue:dispatch_get_main_queue()];


#pragma mark - XMPPStreamDelegate

// 消息发送成功
- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message {
    
    NSLog(@"===========>消息发送成功");
}

// 消息发送失败
- (void)xmppStream:(XMPPStream *)sender didFailToSendMessage:(XMPPMessage *)message error:(NSError *)error {
    
    NSLog(@"===========>消息发送失败:%@", error);
}

// 接收消息成功
- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message {
    
    NSLog(@"===========>接收消息成功:%@", message);
}

然后设置点击rightBarButtonItem发送一条固定的消息。

-----------ChatViewController.m-----------

- (void)sendAction {
    
    [[ProjectXMPP sharedXMPP] sendTextMessage:[NSString stringWithFormat:@"我正在和%@聊天", self.friendUser.vCardTemp.nickname] toFriend:self.friendUser.jid];
}

可得到效果,可以顺利的发送文本消息,也很简单吧。

2、文本聊天气泡的绘制

为了使界面好看些,我们需要分别绘制一个发送方和接收方的聊天气泡,下面会以发送方聊天气泡为例,接收方同理。

绘制文本聊天气泡最主要的要求是:气泡要随着文本内容的多少,自适应宽度和高度。那如何绘制呢?

  • 首先,我们要明确,聊天气泡 = 一张图片 + 一个label
  • 然后,我们会给气泡图片设置一个可拉伸区域,来保证图片能够随着文本的长度做自由的拉伸,换句话说其实图片的拉伸效果其实是依据label自适应的宽度和高度来拉伸的,所以imageView的布局就要依赖于label
// 设置图片不可被拉伸的区域,代表上起10左起10下起10右起10的范围隔出来的四个角(左上、右上、左下、右下四个角)是不可被拉伸的,也就是label以外的部分不要拉伸,其余部分(即放label的部分)可拉伸
UIImage *image = [[UIImage imageNamed:@"chat_out"] resizableImageWithCapInsets:UIEdgeInsetsMake(10, 10, 10, 10) resizingMode:(UIImageResizingModeStretch)];
  • 这样,我们只要做好label的自适应宽度和高度,图片也就是跟着做好了拉伸效果,最终得到的效果就是聊天气泡可以随着文本内容的多少,自适应宽度和高度

好了,那现在来添加约束,其中的关键点就是:

  • 添加好label的上、右、下约束(label的上左右是依赖于气泡imageView的,各缩进10),距左的话可以给一个“大于等于”的约束(这里我们设置为大于等于20),让label自适应宽度,再把label的numberOfLine设置为0,让label自适应高度。
这个约束比较关键
  • 然后imageView的话,设置上、右、下,然后imageView的左要依赖于label的左,这样imageView就可以根据label自由的横向拉伸了,而纵向的话,则是依赖于cell的高度,而cell的高度要会依赖于label自适应的高度(下面一条会说到),这样imageView纵向也可以自由拉伸了。
这个约束比较关键
  • 至于cell想要达到自适应高度,我们可以直接让tableViewCell去自己估算,但是为了更好的控制cell的高度,我们需要自己根据文本内容来计算cell应有的高度
// 可以让tableViewCell去自己估算
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    return UITableViewAutomaticDimension;
}
// 根据文本内容来计算cell应有的高度
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    CGRect rect = [message.body boundingRectWithSize:CGSizeMake(kScreenWidth - 90, 10000) options:(NSStringDrawingUsesLineFragmentOrigin) attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:16]} context:nil];
    CGFloat height = rect.size.height;
    
    if (height > (40 - 20)) {// 文本超过一行时,设置为文本的高度的高度
        
        return height + 20 + (10 + 40 + 10);
    }else {// 文本不足一行时,默认为一行的高度
        
        return 40 + (10 + 40 + 10);
    }
}

好了,按照上面的方式,我们就可以绘制出发送方和接收方的聊天气泡了,我们在聊天界面展示一下效果看看。

现在既然已经是聊天列表了,所以一进界面我们就要拉取聊天记录来展示,并且要把聊天记录滚动到最后一行。

-----------ChatViewController.m-----------

- (void)reloadMessageRecordWithAnimation:(BOOL)flag {
    
    NSArray *fetchedObjects = [[ProjectXMPP sharedXMPP] fecthMessageRecordWithFriendJID:self.friendUser.jid];
    
    // 将聊天记录存进数组
    [self.messageRecordArray removeAllObjects];
    self.messageRecordArray = [NSMutableArray arrayWithArray:fetchedObjects];
    
    [self.tableView reloadData];
    
    if (self.messageRecordArray.count != 0) {
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
            // 滚动到最后一行
            NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.messageRecordArray.count - 1 inSection:0];
            [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:(UITableViewScrollPositionNone) animated:flag];
        });
    }
}

然后cell展示的部分,则是根据每条消息的isOutgoing属性来判断是发送还是接收,来返回相应的气泡来展示。

-----------ChatViewController.m-----------

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    XMPPMessageArchiving_Message_CoreDataObject *message = self.messageRecordArray[indexPath.row];
    
    if (message.isOutgoing) {// 发送

        ChatOutTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellReuseID_out forIndexPath:indexPath];
        cell.backgroundColor = kVCBackgroundColor;
        cell.selectionStyle = UITableViewCellSelectionStyleNone;
        
        // 从本地拉取自己的电子名片,本地没有则会去服务端拉取,并存储在本地
        XMPPvCardTemp *myvCardTemp = [[ProjectXMPP sharedXMPP] fetchvCardTempForAccount:[UserModel currentUser].jid.user];
        cell.headImageView.image = [UIImage imageWithData:myvCardTemp.photo];
        cell.nicknameLabel.text = myvCardTemp.nickname;
        // 拿取消息的时间
        NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
        [dateFormatter setDateFormat:@"MM-dd HH:mm:ss"];
        NSString *dateString = [dateFormatter stringFromDate:message.timestamp];
        cell.timeLabel.text = dateString;
        // 拿取消息的内容
        cell.contentLabel.text = message.body;
        
        return cell;
    }else {
        
        ChatInTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellReuseID_in forIndexPath:indexPath];
        cell.backgroundColor = kVCBackgroundColor;
        cell.selectionStyle = UITableViewCellSelectionStyleNone;
        
        // 拿取好友列表界面传过来的好友电子名片
        cell.headImageView.image = [UIImage imageWithData:self.friendUser.vCardTemp.photo];
        cell.nicknameLabel.text = self.friendUser.vCardTemp.nickname;
        // 拿取消息的时间
        NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
        [dateFormatter setDateFormat:@"MM-dd HH:mm:ss"];
        NSString *dateString = [dateFormatter stringFromDate:message.timestamp];
        cell.timeLabel.text = dateString;
        // 拿取消息的内容
        cell.contentLabel.text = message.body;
        
        return cell;
    }
}

ok,我们还是点击rightBarButtonItem发送一条固定的消息。

我们在消息发送成功的回调里让发送方重新拉取消息并带动画滚动到最新消息处,在消息接收成功的回调里让接收方重新拉取消息并带动画滚动到最新消息处。

-----------ChatViewController.m-----------

#pragma mark - XMPPStreamDelegate

// 消息发送成功
- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message {
    
    NSLog(@"===========>消息发送成功");
    
    [self reloadMessageRecordWithAnimation:YES];
}

// 消息发送失败
- (void)xmppStream:(XMPPStream *)sender didFailToSendMessage:(XMPPMessage *)message error:(NSError *)error {
    
    NSLog(@"===========>消息发送失败:%@", error);
}

// 消息接收成功
- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message {
    
    NSLog(@"===========>消息接收成功");
    
    [self reloadMessageRecordWithAnimation:YES];
}

这样就完事了,看下效果。

3、键盘输入框的自定义

自定义键盘输入框

你看,下面界面空出一部分的白,就是为了自定义这个键盘输入框,这个键盘输入框的封装力度要够:

  • 键盘输入框在内部就要做到跟着键盘的出现与否上升和回落,而不是暴露在聊天界面做这样的事情。
  • 点发送按钮就能获取到要发送的文本内容,直接调用发送文本消息的API发送。
  • 点击更多按钮,暂时的功能是暴露出回调,自定义事件。
  • 所有关于录音的操作都要在键盘输入框在内部完成,只暴露出各种录音状态的回调,并在录音完成的回调就能让聊天界面获取到音频信息,进而直接调用发送音频的API发送音频。
  • 同时要根据输入文本的大小,动态的改变输入框textView的高度,超过指定的高度后就不能再变高,而是开始可以上下滑动。
  • 另外一个比较繁琐的就是模仿微信的那个录音,按住说话、松开结束、上滑取消、超过30s自动发送等等,这个用别的点击加平移手势是实现不了的,而是通过长按手势实现的,各种状态的判断和视图的变换因为是封装在view内部完成的,所以当时比较繁琐,但是一步一步的处理比较好的解决了,录音转码那些倒比较容易些。
  • 还有就是键盘出来时,textView变化大小时,聊天界面跟着变化大小,消息跟着变化位置。

这里就不再详细说明了,可下载代码查看,这里先看下发送文本消息的效果。


二、发送图片消息和音频消息


接下来我们会实现发送图片消息和音频消息,图片和音频消息其实是差不多的,都是给XMPPMessage对象添加了一个附件发送出去。

1、发送图片消息和音频消息的实现

是什么消息是通过消息体来判断的,我们设置图片的消息体为“ image”,音频的消息体为“audio+音频时长”。

-----------ProjectXMPP.m-----------

- (void)sendImageMessage:(NSData *)image toFriend:(XMPPJID *)friendJID {
    
    [self sendMultimediaMessage:image messageType:@"image" toFriend:friendJID];
}

- (void)sendAudioMessage:(NSData *)audio duration:(CGFloat)duration toFriend:(XMPPJID *)friendJID {
    
    [self sendMultimediaMessage:audio messageType:[NSString stringWithFormat:@"audio:%.1f″", duration] toFriend:friendJID];
}

- (void)sendMultimediaMessage:(NSData *)data messageType:(NSString *)type toFriend:(XMPPJID *)friendJID {
    
    // 构建消息:消息的类型,消息要发给谁,消息的内容
    XMPPMessage* message = [XMPPMessage messageWithType:@"chat" to:friendJID];
    
    // 消息体
    [message addBody:type];
    
    // 把图片data转换成base64编码
    NSString *base64String = [data base64EncodedStringWithOptions:0];
    
    // 消息附件
    XMPPElement *attachment = [XMPPElement elementWithName:@"attachment" stringValue:base64String];
    // 添加消息附件
    [message addChild:attachment];
    
    // 发送消息
    [self.stream sendElement:message];
}
2、图片聊天气泡的绘制

同样以绘制发送方图片气泡为例,有一个要求:这里我们图片的宽度值给了一个固定的值220pt,当图片的宽度小于等于220pt就返回原图片,靠右上显示,当图片的宽度大于220pt时,就按宽220等比例缩放,我们不管高度,让高度去自己适应吧。(注意,这里想只靠self.contentImageView.contentMode是没法完全实现的)

图片按比例缩放:

//
//  UIImage+Scale.m
//  XMPPDemo
//
//  Created by 意一yiyi on 2018/7/2.
//  Copyright © 2018年 意一yiyi. All rights reserved.
//

#import "UIImage+Scale.h"

@implementation UIImage (Scale)

// 把图片缩小到指定的宽度范围内为止
- (UIImage *)scaleImageWithWidth:(CGFloat)width {
    
    // 如果图片的宽度小于等于我们指定的宽度,或者我们给了个小于等于0的宽度,就返回原图片
    if (self.size.width <= width || width <= 0) {
        
        return self;
    }
    
    // 否则,把图片按比例缩放到我们指定的大小
    CGFloat scale = self.size.width / width;
    CGFloat height = self.size.height / scale;
    CGRect rect = CGRectMake(0, 0, width, height);
    // 开始上下文,目标大小是这么大
    UIGraphicsBeginImageContext(rect.size);
    // 在指定区域内绘制图像
    [self drawInRect:rect];
    // 从上下文中获得绘制结果
    UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext();
    // 关闭上下文返回结果
    UIGraphicsEndImageContext();
    
    return resultImage;
}

@end

高度自适应:

-----------ChatViewController.m-----------

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    return UITableViewAutomaticDimension;
}

绘制好气泡,我们去发图片消息,看下效果。

3、音频聊天气泡的绘制

音频聊天气泡,也是在一张图片上放了一个imageView来进行播放动画,一个label来显示音频时长,图片的拉伸和文本气泡是一样的,需要注意的一点是:气泡的宽度要根据音频的时长来变化。

这里我们设置音频聊天气泡的宽度为100,然后把宽度这个约束拖成一个属性,然后根据音频时长来计算气泡该有的宽度,气泡最大宽度为200。

-----------ChatOutAudioTableViewCell.m-----------

@property (weak, nonatomic) IBOutlet NSLayoutConstraint *constraint;

// 获取到音频时长后计算
self.constraint.constant = 100 + (200 - 100) * (self.duration / 30.0);

绘制好气泡,我们去发音频消息,看下效果。

4、音频录音器和音频播放器的封装

这个就不说了,可在项目中查看。

至此,我们就算完成了XMPP实现IM。后面可能还会有一些文章来记录XMPP应用于实际项目中出现的问题及解决方案。


上一篇:XMPP实现IM--电子名片、用户在线状态监测
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容