XMPPFramework iOS开发(七)即时通讯(文字和图片)
一、程序目标
聊天界面如图,需要实现的功能有:发送文字消息、发送图片消息。
二、使用准备
2.1 开启消息模块
和电子名片、花名册模块一样,要想使用XMPP框架下的聊天功能,需要开启消息模块。
//消息模块
#import "XMPPMessageArchiving.h"
#import "XMPPMessageArchivingCoreDataStorage.h"
//消息模块
@property (nonatomic, strong, readonly) XMPPMessageArchiving *msgArchiving;
//消息数据存储模块
@property (nonatomic, strong, readonly) XMPPMessageArchivingCoreDataStorage *msgArchivingStorage;
//4.添加消息模块
if (_msgArchivingStorage == nil) {
_msgArchivingStorage = [[XMPPMessageArchivingCoreDataStorage alloc] init];
_msgArchiving = [[XMPPMessageArchiving alloc] initWithMessageArchivingStorage:_msgArchivingStorage];
[_msgArchiving activate:_xmppStream];
}
要记得在之前说过的teardown方法里面,释放花名册、消息模块等资源。此外,XMPP还提供了断线重连的功能,很简单,声明后激活即可。
//断网自动连接
XMPPReconnect *_reconnect;
//5.自动连接模块
_reconnect = [[XMPPReconnect alloc] init];
[_reconnect activate:_xmppStream];
2.2 声明成员变量
@interface WCChatViewController : UIViewController
/**
* 正在聊天的好友的Jid
*/
@property (nonatomic, strong) XMPPJID *friendJid;
@end
@interface WCChatViewController () <NSFetchedResultsControllerDelegate, UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate>{
NSFetchedResultsController *_resultContr;
}
@property (weak, nonatomic) IBOutlet UITableView *tableView;
/**
* 输入框距离底部的约束
*/
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *bottomConstraint;
@property (weak, nonatomic) IBOutlet UITextField *textField;
- (IBAction)addBtnClick:(id)sender;
@end
2.3 聊天数据格式
可以在程序中打印message的内容,其格式如下:
其中,接受到的message中:
1. from、to指通信双方的jid
2. body指通信内容
发送给对方的message中:
1. type的值是自己定义的
2. body同样指通信内容
2.4 监听键盘弹出情况
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kbWillShow:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kbWillHide:) name:UIKeyboardWillHideNotification object:nil];
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark 键盘即将显示
- (void)kbWillShow:(NSNotification *)noti
{
CGFloat kbHeight = [noti.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height;
self.bottomConstraint.constant = kbHeight;
}
#pragma mark 键盘即将隐藏
- (void)kbWillHide:(NSNotification *)noti
{
self.bottomConstraint.constant = 0;
}
#pragma mark 退出键盘
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
[self.view endEditing:YES];
}
三、发送/接收消息
3.1 获取聊天数据
和花名册模块一样,XMPP会先把聊天数据上传到服务器,再缓存到本地并生成sqlite文件。因此,获取的方法也是一样的:
//加载数据库的聊天数据
//1.上下文
NSManagedObjectContext *msgContext = [[WCXMPPTool sharedWCXMPPTool].msgArchivingStorage mainThreadManagedObjectContext];
//2.请求查询哪张表
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"XMPPMessageArchiving_Message_CoreDataObject"];
//设置过滤条件,只要自己和当前好友的聊天数据
NSString *loginUserJid = [WCXMPPTool sharedWCXMPPTool].xmppStream.myJID.bare;
//数据库表的属性名可在XMPP/Extensions/XEP-0136/CoreDataStorage/XMPPMessageArchiving.xcdatamodeld/中查看
//streamBareJidStr指自己的jid,bareJidStr指和自己通信的用户的jid
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"streamBareJidStr = %@ AND bareJidStr = %@", loginUserJid, self.friendJid.bare];
request.predicate = predicate;
//如果以时间排序,timestamp是固定写法
NSSortDescriptor *timeSort = [NSSortDescriptor sortDescriptorWithKey:@"timestamp" ascending:YES];
request.sortDescriptors = @[timeSort];
//3.执行请求
//3.1创建结果控制器
_resultContr = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:msgContext sectionNameKeyPath:nil cacheName:nil];
_resultContr.delegate = self;
//3.2执行
NSError *error = nil;
[_resultContr performFetch:&error];
//列表下拉到最底部
if (_resultContr.fetchedObjects.count > 0) {
NSIndexPath *lastIndex = [NSIndexPath indexPathForRow:_resultContr.fetchedObjects.count-1 inSection:0];
[self.tableView scrollToRowAtIndexPath:lastIndex atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}
3.2 发送文本消息
#pragma mark 按下键盘的发送键(遵守了UITextFieldDelegate协议)
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
NSString *text = self.textField.text;
//发送聊天数据
//上面说过,chat是定义给message的xml数据中type节点的值,之后获取聊天内容就根据这个节点的值获取
XMPPMessage *msg = [XMPPMessage messageWithType:@"chat" to:self.friendJid];
[msg addBody:text];
//sendElement在发送在线状态的时候也用过
[[WCXMPPTool sharedWCXMPPTool].xmppStream sendElement:msg];
//清空输入框
self.textField.text = nil;
return YES;
}
3.3 发送图片消息
#pragma mark 选择附件按钮
- (IBAction)addBtnClick:(id)sender {
//选择图库中的图片
UIImagePickerController *imagePc = [[UIImagePickerController alloc] init];
imagePc.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
imagePc.delegate = self;
//进入图库选择图片
[self presentViewController:imagePc animated:YES completion:nil];
}
#pragma mark 图片选择器代理方法
#pragma mark 图片选择完成
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
ZHLog(@"%@", info);
//获取到图片
UIImage *image = info[UIImagePickerControllerOriginalImage];
//把图片转化为data类型并发送
[self sendAttchmentWithData:UIImagePNGRepresentation(image) bodyType:@"image"];
//退出图片选择器
[self dismissViewControllerAnimated:YES completion:nil];
}
#pragma mark 发送附件
- (void)sendAttchmentWithData:(NSData *)data bodyType:(NSString *)bodyType
{
XMPPMessage *msg = [XMPPMessage messageWithType:@"chat" to:self.friendJid];
//新增属性节点,存放发送的消息数据的类型
[msg addAttributeWithName:@"bodyType" stringValue:bodyType];
//发送图片其实不需要body了,但没有body就发送不出去,获取图片的时候需要另外处理body的内容,不要让它显示
[msg addBody:bodyType];
//图片的data数据经过base64编码 转换为 字符串类型
NSString *base64Str = [data base64EncodedStringWithOptions:0];
//添加自定义节点,body存放文本消息/消息类型,自定义节点存放图片的数据内容
XMPPElement *attachment = [XMPPElement elementWithName:@"attachment" stringValue:base64Str];
[msg addChild:attachment];
//发送
[[WCXMPPTool sharedWCXMPPTool].xmppStream sendElement:msg];
}
3.4 显示消息内容
#pragma mark 返回列表行数
- (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return _resultContr.fetchedObjects.count;
}
#pragma mark 设置列表数据,显示聊天内容
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *ID = @"message";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:ID];
}
XMPPMessageArchiving_Message_CoreDataObject *msgObj = _resultContr.fetchedObjects[indexPath.row];
//1.获取原始的xml数据
XMPPMessage *message = msgObj.message;
//2.获取消息的类型
NSString *bodyType = [message attributeStringValueForName:@"bodyType"];
if ([bodyType isEqualToString:@"image"]) { //图片
//遍历message的子节点
NSArray *children = message.children;
for (XMPPElement *note in children) {
//获取节点的名字,attachment是刚刚定义的存放图片数据的节点
if ([[note name] isEqualToString:@"attachment"]) {
//获取附件字符串,转换为data,再转为图片
NSString *imgBase64Str = [note stringValue];
NSData *imgData = [[NSData alloc] initWithBase64EncodedString:imgBase64Str options:0];
UIImage *image = [UIImage imageWithData:imgData];
cell.imageView.image = image;
cell.textLabel.text = nil;
}
}
}else { //纯文本
cell.textLabel.text = msgObj.body;
cell.imageView.image = nil;
}
return cell;
}
3.5 刷新列表数据
#pragma mark 列表内容改变
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
//刷新列表
[self.tableView reloadData];
//列表下拉到最底部
NSIndexPath *lastIndex = [NSIndexPath indexPathForRow:_resultContr.fetchedObjects.count-1 inSection:0];
[self.tableView scrollToRowAtIndexPath:lastIndex atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}
四、问题
上面的代码是可以运行的,但同时还有几个地方没做好:
聊天数据的排版。上面的代码没有对聊天数据进行排版,因此可以显示,但不能知道某条消息是谁发送的,是什么时候发送的。要解决这个问题,需要自定义cell,这里不细讲,以后写UITableView相关的博客的时候再说。
发送图片耗能过大。一般来说,图片数据不应该直接发送,开发这种应用应该配备一台文件服务器。正确的发送图片的方式应该是这样的:
a) 发送图片时首先把图片上传到文件服务器
b) 服务器返回文件路径给用户
c) openfire服务器只保存这个文件路劲
d) 对方接收图片的时候,只获取到文件路径
e) 之后再根据这个路径去文件服务器下载图片
解决这个问题需要自己编写文件服务器,笔者不具备这方面的知识,所以略过不谈。
五、小结
以上。