这一篇开始,我们实现消息的收发功能。
概述
在现有的实现的中,我们已经实现了xmppStream:didReceiveMessage方法,在收到聊天消息时,会使用UIAlertView显示消息的内容。
- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message
{
DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
// A simple example of inbound message handling.
if ([message isChatMessageWithBody])
{
XMPPUserCoreDataStorageObject *user = [self.xmppRosterStorage userForJID:[message from]
xmppStream:self.xmppStream
managedObjectContext:[self managedObjectContext_roster]];
NSString *body = [[message elementForName:@"body"] stringValue];
NSString *displayName = [user displayName];
if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateActive)
{
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:displayName
message:body
delegate:nil
cancelButtonTitle:@"Ok"
otherButtonTitles:nil];
[alertView show];
}
else
{
// We are not active, so use a local notification instead
UILocalNotification *localNotification = [[UILocalNotification alloc] init];
localNotification.alertAction = @"Ok";
localNotification.alertBody = [NSString stringWithFormat:@"From: %@\n\n%@",displayName,body];
[[UIApplication sharedApplication] presentLocalNotificationNow:localNotification];
}
}
}
但是,我们需要一个单独的ViewController来实现聊天页面。首先,我们看看这个聊天页面需要有哪些功能:
- 发送文本消息
- 发送文件
- 发送消息到多人聊天室
- 接收消息
- 接收文件
- 从多人聊天室接收消息
还需要考虑:
- 是否显示好友正在输入状态?
- 是否在文件接收完毕时显示缩略图?
- 是否想合并多人聊天和普通聊天?
- 是否想程序在后台运行时通知用户收到了新的消息?
接下来,我们实现聊天页面以支持消息的收发。
YDChatOverviewController
当我们在菜单选择Chats时,切换到YDChatOverviewController。它将显示最近的聊天会话。
YDConversationViewController
这个类用于显示聊天信息,发送出去的消息和接收到的消息分别位于界面的两侧。当发送或接收了图片后,通过点击显示在界面上的缩略图可以以实际大小查看图片。
数据存储
应用程序使用Core Data来存储聊天记录。
后台运行
真实的聊天程序需要可以在后台运行并接收消息,选择target的Capabilities标签,激活background模式并勾选Voice over IP:
实现Core Data模型
首先在YDAppDelegate.h中添加以下属性和方法声明:
// Core Data
@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;
- (void)saveContext;
在YDAppDelegate.m中synthesize这些属性并在application:didFinishLaunchingWithOptions:中对managedObjectContext进行初始化:
@synthesize managedObjectContext = _managedObjectContext;
@synthesize managedObjectModel = _managedObjectModel;
@synthesize persistentStoreCoordinator = _persistentStoreCoordinator;
<p class="p1"><span class="s1"> </span><span class="s2">// Setup CoreData System</span></p><p class="p2"><span class="s1"> </span><span class="s2">__managedObjectContext</span><span class="s1"> = </span><span class="s3">self</span><span class="s1">.</span><span class="s2">managedObjectContext</span><span class="s1">;</span></p>
实现CoreData相关的方法:
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Chat COREDATA
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (void)saveContext
{
NSError *error = nil;
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
if (managedObjectContext != nil)
{
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error])
{
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
}
}
- (NSURL *)applicationDocumentsDirectory
{
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}
// 创建Model
- (NSManagedObjectModel *)managedObjectModel
{
if (__managedObjectModel != nil)
{
return __managedObjectModel;
}
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"ChatModel" withExtension:@"momd"];
__managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
return __managedObjectModel;
}
// 创建PSC
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
if (__persistentStoreCoordinator != nil)
{
return __persistentStoreCoordinator;
}
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"YDChat.sqlite"]; NSError *error = nil;
__persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error])
{
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
return __persistentStoreCoordinator;
}
// 创建Context
- (NSManagedObjectContext *)managedObjectContext
{
if (__managedObjectContext != nil)
{
return __managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
__managedObjectContext = [[NSManagedObjectContext alloc] init];
[__managedObjectContext setPersistentStoreCoordinator:coordinator];
[__managedObjectContext setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
// subscribe to change notifications
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_mocDidSaveNotification:) name:NSManagedObjectContextDidSaveNotification object:nil];
}
return __managedObjectContext;
}
//
- (void)_mocDidSaveNotification:(NSNotification *)notification
{
NSManagedObjectContext *savedContext = [notification object];
// ignore change notifications for the main MOC
if (__managedObjectContext == savedContext)
{
return;
}
if (__managedObjectContext.persistentStoreCoordinator != savedContext.persistentStoreCoordinator)
{
// that's another database
return;
}
dispatch_sync(dispatch_get_main_queue(), ^{
[__managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
});
}
创建名为ChatModel的CoreData模型:
这些数据的作用如下表所示:
Attribute | Purpose |
direction | 标示消息消息的方向,是收到的消息还是发送出去的消息 |
filenameAsSent | 要发送的文件名 |
groupNumber | 消息所属的组号 |
hasMedia | 是否涉及多媒体(文件) |
isGroupMessage | 消息是否为组消息 |
isNew | 是否为新收到的消息 |
jidString | 发送方的jid |
localFileName | 设备存储的文件名 |
mediaType | 媒体类型 |
messageBody | 消息的正文 |
messageDate | 消息的日期 |
messageStatus | 消息的状态 |
MimeType | 发送文件的Mime类型 |
roomJID | 聊天室的JID |
roomName | 聊天室的名字 |
创建继承于NSMangedObject的Chat类:
修改YDChat-Prefix.pch,import Chat.h
#import <Availability.h>
#ifndef __IPHONE_5_0
#warning "This project uses features only available in iOS SDK 5.0 and later."
#endif
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import "YDDefinitions.h"
#import "YDHelper.h"
#import "Macros.h"
#import "Chat.h"
#endif
XMPPFramework在收到XMPP 消息时,会调用代理的xmppStream:didReceiveMessage:方法,此时,我们就将收到的消息存入到Core Data中。
updateCoreDataWithIncomingMessage:方法的实现如下,通过JID查询发送方的信息,构造Chat对象并将构造好的对象存入到Core Data中,最后发送Notification通知应用程序的其它关心此信息的模块。
-(void)updateCoreDataWithIncomingMessage:(XMPPMessage *)message
{
//determine the sender
XMPPUserCoreDataStorageObject *user = [self.xmppRosterStorage userForJID:[message from]
xmppStream:self.xmppStream
managedObjectContext:[self managedObjectContext_roster]];
Chat *chat = [NSEntityDescription
insertNewObjectForEntityForName:@"Chat"
inManagedObjectContext:self.managedObjectContext];
chat.messageBody = [[message elementForName:@"body"] stringValue];
chat.messageDate = [NSDate date];
chat.messageStatus=@"received";
chat.direction = @"IN";
chat.groupNumber=@"";
chat.isNew = [NSNumber numberWithBool:YES];
chat.hasMedia=[NSNumber numberWithBool:NO];
chat.isGroupMessage=[NSNumber numberWithBool:NO];
chat.jidString = user.jidStr;
NSError *error = nil;
if (![self.managedObjectContext save:&error])
{
NSLog(@"error saving");
}
[[NSNotificationCenter defaultCenter] postNotificationName:kNewMessage object:self userInfo:nil];
}
消息类型分为两大类,一类是完整的消息拥有消息正文,另一类没有消息正文,但是从这种消息我们可以知道,对方是否正在输入,是否处于离开状态等等。
当用户在输入信息时,xmppStream会发送一个状态消息,标示自己正处于录入状态,这样我们就可以在应用程序上显示类似“Peter is typing”的状态信息。
修改YDDefinitions.h在其中添加两个通知的定义:
//Notifications
#define kChatStatus @"kChatStatus"
#define kNewMessage @"kNewMessage"
修改didReceiveMessage方法,根据消息的种类,生成不同类型的通知
- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message
{
DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
// A simple example of inbound message handling.
if ([message isChatMessageWithBody])
{
DDLogInfo(@"Save message in CoreData: %@", message);
[self updateCoreDataWithIncomingMessage:message];
}
else if ([message isChatMessage])
{
NSArray *elements = [message elementsForXmlns:@"http://jabber.org/protocol/chatstates"];
if ([elements count] >0)
{
for (NSXMLElement *element in elements)
{
NSString *statusString = @" ";
NSString *cleanStatus = [element.name stringByReplacingOccurrencesOfString:@"cha:" withString:@""];
if ([cleanStatus isEqualToString:@"composing"])
statusString = @" is typing";
else if ([cleanStatus isEqualToString:@"active"])
statusString = @" is ready";
else if ([cleanStatus isEqualToString:@"paused"])
statusString = @" is pausing";
else if ([cleanStatus isEqualToString:@"inactive"])
statusString = @" is not active";
else if ([cleanStatus isEqualToString:@"gone"])
statusString = @" left this chat";
NSMutableDictionary *m = [[NSMutableDictionary alloc] init];
[m setObject:statusString forKey:@"msg"];
[[NSNotificationCenter defaultCenter] postNotificationName:@"kChatStatus" object:self userInfo:m];
DDLogInfo(@"PETER %@", statusString);
}
}
}
}
下一节继续实现YDChatOverviewController