2014年8月12日 星期二

[ iOS XMPP ] 即時聊天APP開發 - Part 1



最近在寫一支與即時訊息有關的社交APP,需要使用 XMPP 來完成即時訊息聊天的功能,因此在這裡我做個簡單筆記記錄一下最近的XMPP開發心得,不過我也是自己慢慢摸索,算不上什麼厲害的人就是了,所以不介意的話就參考看看吧。

<前言>

此篇主要是做一些最最最基本前置的設定,與程式碼都還無關,但是這些設定沒做好,後面可就頭大拉!

<正文開始>

首先,若是要開發與XMPP的APP,需要先架一台 XMPPServer,可以使用ejabber或是openfire伺服器,有關XMPP SERVER的細節可以參考: 這裡。因此有關伺服器的部分我就不多贅述了。在完成架設伺服器之後,接著就要來完成XMPP iOS Client端的部分嘍。




一開始,我們要先到 這裡 下載XMPP Framework,可以直接點選Download ZIP。下載完成後,開啓Xcode,建立一個新的專案(我將新專案取名為:XMPPTry),把剛剛在下載的XMPPFramework-master資料夾中的Authentication, Categories, Core, Extensions, Utilities, Vendor 匯入到新專案XMPPTry內(新專案的資料夾內應該要有這些檔案)。




其中,特別注意資料夾內有個Sample_XMPPFramework.h ,把它複製一份到你的APP資料夾中,並把名稱改為:XMPPFramework.h,這樣一來之後你就可以只要include這個header就可以了。至於裡面內容的設定細節後面會再討論,完成後在我的新專案資料夾XMPPTry內應如下圖。





然後打開Xcode把這些東西都匯入進去。



接下來,在Xcode中將一些你所需要的Framework加到專案內,如下圖匯入這些Framework。



然後將Build Setting裡的Header Search Path加上 /libxml2



到此為止,我們算是將XMPP一開始最基本的匯入工作完成了(當然,還沒正式開工寫程式呢)。
我們接著打開剛才曾經說過的XMPPFramework.h檔案,將你即將會需要用到的檔案import進來(基本上在Sample_XMPPFramework.h中除了XMPP.h外所有的module都被Comment掉了,你可以自己選擇接下來要使用哪些標頭檔),下面的是我所使用的,我把它們uncomment之後,將XMPPFramework.h加入我們的專案中。



其中,

XMPPReconnect.h
負責重新連線

XMPPMessageArchivingCoreDataStorage.h
負責處理訊息的資料儲存 (透過Core Data)

XMPPRoster.h
XMPPRosterCoreDataStorage.h
負責處理好友清單與資料儲存

XMPPvCardTemp.h
XMPPvCardTempModule.h
XMPPvCardAvatarModule.h
XMPPvCardCoreDataStorage.h
負責處理個人資料的內容,包含姓名、暱稱、大頭照、地址等等

XMPPCapabilitiesCoreDataStorage.h
XMPPCapabilities.h
負責支持Capabilities

XMPPMUC.h
XMPPRoomCoreDataStorage.h
負責處理聊天室與相關的資料儲存,多人聊天等等



現在,如果都沒有問題,就即將進入設定APP Delegate的部分拉!!

我們把一些XMPP的主要方法寫在APPDelegate中,基本上我這裡是參考剛剛所下載下來的XMPP Framework裡面的範例程式加以修改而成。

在 AppDelegate.h 中寫入以下程式碼。


//
//  JTAppDelegate.h
//  XMPPTry
//
//  Created by MacBook Pro on 2014/8/12.
//  Copyright (c) 2014 Jacky Tsai. All rights reserved.
//

#import <UIKit/UIKit.h>

#import "XMPPFramework.h"
#import <CoreData/CoreData.h>


@interface JTAppDelegate : UIResponder <UIApplicationDelegate, XMPPRosterDelegate>
{
XMPPStream *xmppStream;
XMPPReconnect *xmppReconnect;
        XMPPRoster *xmppRoster;
XMPPRosterCoreDataStorage *xmppRosterStorage;
        XMPPvCardCoreDataStorage *xmppvCardStorage;
XMPPvCardTempModule *xmppvCardTempModule;
XMPPvCardAvatarModule *xmppvCardAvatarModule;
XMPPCapabilities *xmppCapabilities;
XMPPCapabilitiesCoreDataStorage *xmppCapabilitiesStorage;
        XMPPMessageArchivingCoreDataStorage *xmppMessageArchivingCoreDataStorage;
        XMPPMessageArchiving *xmppMessageArchivingModule;
    
        UINavigationController *navigationController;
        LoginViewController *loginViewController;
NSString *password;
BOOL customCertEvaluation;
BOOL isXmppConnected;
}

@property (nonatomic, strong, readonly) XMPPStream *xmppStream;
@property (nonatomic, strong, readonly) XMPPReconnect *xmppReconnect;
@property (nonatomic, strong, readonly) XMPPRoster *xmppRoster;
@property (nonatomic, strong, readonly) XMPPRosterCoreDataStorage *xmppRosterStorage;
@property (nonatomic, strong, readonly) XMPPvCardTempModule *xmppvCardTempModule;
@property (nonatomic, strong, readonly) XMPPvCardAvatarModule *xmppvCardAvatarModule;
@property (nonatomic, strong, readonly) XMPPCapabilities *xmppCapabilities;
@property (nonatomic, strong, readonly) XMPPCapabilitiesCoreDataStorage *xmppCapabilitiesStorage;
@property (nonatomic, strong) XMPPMessageArchivingCoreDataStorage *xmppMessageArchivingCoreDataStorage;
@property (nonatomic, strong) XMPPMessageArchiving *xmppMessageArchivingModule;

- (NSManagedObjectContext *)managedObjectContext_roster;
- (NSManagedObjectContext *)managedObjectContext_capabilities;

- (BOOL)connect;
- (void)disconnect;

- (void)setupStream;
- (void)teardownStream;

- (void)goOnline;
- (void)goOffline;


@end


AppDelegate.h 中的程式碼,我們宣告了一些會使用到關於XMPP的變數,接下來,在AppDelegate.m 中 import 這些檔案。

#import "GCDAsyncSocket.h"
#import "XMPP.h"
#import "XMPPLogging.h"
#import "XMPPReconnect.h"
#import "XMPPCapabilitiesCoreDataStorage.h"
#import "XMPPRosterCoreDataStorage.h"
#import "XMPPvCardAvatarModule.h"
#import "XMPPvCardCoreDataStorage.h"
#import "DDLog.h"
#import "DDTTYLogger.h"
#import <AudioToolbox/AudioServices.h>
#import <CFNetwork/CFNetwork.h>

結束之後,synthesize在.h檔中的property,完成後如下圖。


接著在.m檔中開始撰寫以下的函數

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Configure logging framework
[DDLog addLogger:[DDTTYLogger sharedInstance]];
     
 //設定接收推播 (Push notification)
 [[UIApplication sharedApplication] registerForRemoteNotificationTypesUIRemoteNotificationTypeAlert|UIRemoteNotificationTypeSound|UIRemoteNotificationTypeBadge];
 // 設定 XMPP stream
[self setupStream];

// 設定XMPP連線,未連線成功則跳到logIn頁面 (loginViewController )
if (![self connect])
{
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 0.0 * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[navigationController presentViewController:loginViewController animated:YES completion:NULL];
});
}
return YES;
}


- (void)applicationDidEnterBackground:(UIApplication *)application 
{
DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);

#if TARGET_IPHONE_SIMULATOR
DDLogError(@"The iPhone simulator does not process background network traffic. "
  @"Inbound traffic is queued until the keepAliveTimeout:handler: fires.");
#endif

if ([application respondsToSelector:@selector(setKeepAliveTimeout:handler:)]) 
{
[application setKeepAliveTimeout:600 handler:^{
DDLogVerbose(@"KeepAliveHandler");
// Do other keep alive stuff here.
}];
}
}

- (void)applicationWillEnterForeground:(UIApplication *)application 
{
DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);

}

- (void)applicationDidBecomeActive:(UIApplication *)application
{
    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    
    [self connect ];
}

- (void)dealloc
{
[self teardownStream];
}


設定Core Data

#pragma mark Core Data

- (NSManagedObjectContext *)managedObjectContext_roster
{
    return [xmppRosterStorage mainThreadManagedObjectContext];
}

- (NSManagedObjectContext *)managedObjectContext_capabilities
{
    return [xmppCapabilitiesStorage mainThreadManagedObjectContext];

}




- (void)setupStream {
    
    NSAssert(xmppStream == nil, @"Method setupStream invoked multiple times");
    
    xmppStream = [[XMPPStream alloc] init];
    
//確定裝置並非模擬器
#if !TARGET_IPHONE_SIMULATOR
    {
        //let xmpp run in the background
        xmppStream.enableBackgroundingOnSocket = YES;

    }
#endif
    
    
    //init Reconnect 斷線後可以重新連線
    xmppReconnect = [[XMPPReconnect alloc] init];
    
    // 此處可以自行更動成你想要使用的儲存方式
    // 這裡是使用core data進行資料儲存
    xmppRosterStorage = [[XMPPRosterCoreDataStorage alloc] init];
    xmppRoster = [[XMPPRoster alloc] initWithRosterStorage:xmppRosterStorage];
    
    // 設定是否自動從伺服器抓取你的好友名單
    xmppRoster.autoFetchRoster = YES;
    
    // 設定是否自動接受送來的好友邀請
    xmppRoster.autoAcceptKnownPresenceSubscriptionRequests = YES;
    
    
    
    //XMPPRoster 會自動整合 XMPPvCardAvatarModule 以取得roster中使用者的大頭照.
    //vCard Avatar module 會和標準的 vCard Temp module 一起下載使用者大頭照.
    xmppvCardStorage = [XMPPvCardCoreDataStorage sharedInstance];
    xmppvCardTempModule = [[XMPPvCardTempModule alloc] initWithvCardStorage:xmppvCardStorage];
    xmppvCardAvatarModule = [[XMPPvCardAvatarModule alloc] initWithvCardTempModule:xmppvCardTempModule];

    
    // 設定 capabilities: For hashing
    xmppCapabilitiesStorage = [XMPPCapabilitiesCoreDataStorage sharedInstance];
    xmppCapabilities = [[XMPPCapabilities alloc] initWithCapabilitiesStorage:xmppCapabilitiesStorage];
    
    xmppCapabilities.autoFetchHashedCapabilities = YES;
    xmppCapabilities.autoFetchNonHashedCapabilities = NO;
    
    // 設定Core Data
    xmppMessageArchivingCoreDataStorage = [XMPPMessageArchivingCoreDataStorage sharedInstance];
    xmppMessageArchivingModule = [[XMPPMessageArchiving alloc]initWithMessageArchivingStorage:xmppMessageArchivingCoreDataStorage];
    
    [xmppMessageArchivingModule setClientSideMessageArchivingOnly:YES];
    
    // Activate xmpp modules
    [xmppReconnect         activate:xmppStream];
    [xmppRoster            activate:xmppStream];
    [xmppvCardTempModule   activate:xmppStream];
    [xmppvCardAvatarModule activate:xmppStream];
    [xmppCapabilities      activate:xmppStream];
    [xmppMessageArchivingModule activate:xmppStream];

    
    // 加自己為 delegate
    [xmppStream addDelegate:self delegateQueue:dispatch_get_main_queue()];
    [xmppRoster addDelegate:self delegateQueue:dispatch_get_main_queue()];
    [xmppvCardTempModule addDelegate:self delegateQueue:dispatch_get_main_queue()];
    [xmppvCardAvatarModule addDelegate:self delegateQueue:dispatch_get_main_queue()];
    [xmppMessageArchivingModule addDelegate:self delegateQueue:dispatch_get_main_queue()];
    
    //設定伺服器的Host Name & Port (這裡請設定你自己的伺服器IP和Port)
    [xmppStream setHostName:@"140.112.XXX.XX"];
    [xmppStream setHostPort:5222];
    
    // You may need to alter these settings depending on the server you're connecting to
    customCertEvaluation = YES;
    
}


- (void)teardownStream {
    [xmppStream removeDelegate:self];
    [xmppRoster removeDelegate:self];
    
    [xmppReconnect         deactivate];
    [xmppRoster            deactivate];
    [xmppvCardTempModule   deactivate];
    [xmppvCardAvatarModule deactivate];
    [xmppCapabilities      deactivate];
    
    [xmppStream disconnect];
    
    xmppStream = nil;
    xmppReconnect = nil;
    xmppRoster = nil;
    xmppRosterStorage = nil;
    xmppvCardStorage = nil;
    xmppvCardTempModule = nil;
    xmppvCardAvatarModule = nil;
    xmppCapabilities = nil;
    xmppCapabilitiesStorage = nil;
}

設定使用者上下線

- (void)goOnline
{
    XMPPPresence *presence = [XMPPPresence presence];
    [[self xmppStream] sendElement:presence];
}

- (void)goOffline
{
    XMPPPresence *presence = [XMPPPresence presenceWithType:@"unavailable"];
    [[self xmppStream] sendElement:presence];
}


設定XMPP的連線

#pragma mark Connect/disconnect

- (BOOL)connect
{
    if (![xmppStream isDisconnected]) {
        NSLog(@"已經連好線拉");
        return YES;
    }
    
    NSString *myJID = [[NSUserDefaults standardUserDefaults] stringForKey: @"JID"];
    NSString *myPassword = [[NSUserDefaults standardUserDefaults] stringForKey: @"JPassword"];
    
    
    if (myJID == nil || myPassword == nil) {
       
        return NO;
    }
    
    
    [xmppStream setMyJID:[XMPPJID jidWithString:myJID]];
    password = myPassword;
    
    NSError *error = nil;
    if (![xmppStream connectWithTimeout:XMPPStreamTimeoutNone error:&error])
    {
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error connecting"
                                                            message:@"See console for error details."
                                                           delegate:nil
                                                  cancelButtonTitle:@"Ok"
                                                  otherButtonTitles:nil];
        [alertView show];
        
        DDLogError(@"Error connecting: %@", error);
        return NO;
    }
    
    return YES;
}

- (void)disconnect
{
    [self goOffline];
    [xmppStream disconnect];
}


設定XMPPStream Delegate (這裡的Function 較多,要仔細看一下)


#pragma mark XMPPStream Delegate

- (void)xmppStream:(XMPPStream *)sender socketDidConnect:(GCDAsyncSocket *)socket
{
    DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
}

- (void)xmppStream:(XMPPStream *)sender willSecureWithSettings:(NSMutableDictionary *)settings
{
    DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
    
    NSString *expectedCertName = [xmppStream.myJID domain];
    if (expectedCertName)
    {
        [settings setObject:expectedCertName forKey:(NSString *)kCFStreamSSLPeerName];
    }
    
    if (customCertEvaluation)
    {
        [settings setObject:@(YES) forKey:GCDAsyncSocketManuallyEvaluateTrust];
    }
}

- (void)xmppStream:(XMPPStream *)sender didReceiveTrust:(SecTrustRef)trust
 completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler
{
    DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
    
    dispatch_queue_t bgQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(bgQueue, ^{
        
        SecTrustResultType result = kSecTrustResultDeny;
        OSStatus status = SecTrustEvaluate(trust, &result);
        
        if (status == noErr && (result == kSecTrustResultProceed || result == kSecTrustResultUnspecified)) {
            completionHandler(YES);
        }
        else {
            completionHandler(NO);
        }
    });
}

- (void)xmppStreamDidSecure:(XMPPStream *)sender
{
    DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
}

- (void)xmppStreamDidConnect:(XMPPStream *)sender
{
    DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
    
    isXmppConnected = YES;
    
    NSError *error = nil;
    
    if (![[self xmppStream] authenticateWithPassword:password error:&error])
    {
        DDLogError(@"Error authenticating: %@", error);
    }
}


//這個函數特別重要,他是認證使用者是否為user的函數,當使用者欲建立連線時,會call這個函數
//很多人在做註冊新會員時,就需要透過此函數新增使用者
//這裡寫的是如果通過認證就上傳vcard(個人資料)


- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender
{
    DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);

    NSLog(@"認證通過~~");
    
    // 上傳使用者的資料 (vCard) , 先拿看看vCard,若不存在創一個新的上傳,若已存在,直接更新
    XMPPvCardTemp *temp =  [self.xmppvCardTempModule myvCardTemp];
    
    if (!temp)
    {
        NSXMLElement *vCardXML = [NSXMLElement elementWithName:@"vCard" xmlns:@"vcard-temp"];
        XMPPvCardTemp *newvCardTemp = [XMPPvCardTemp vCardTempFromElement:vCardXML];
        [newvCardTemp setNickname:@"UserABC"];
        [newvCardTemp setPhoto:nil];
        [xmppvCardTempModule updateMyvCardTemp:newvCardTemp];
    }
    else{
        
        temp.nickname = @"UserABC";
        temp.photonil ;
        [self.xmppvCardTempModule updateMyvCardTemp:temp];
    }
    [self goOnline];

}

- (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(NSXMLElement *)error
{
    DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
    
}

- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq
{
    DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
    
    return NO;
}


//收到他人送來的訊息時會呼叫此函數

- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message
{
    DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
    
    
    if ([message isChatMessageWithBody])
    {
        XMPPUserCoreDataStorageObject *user = [xmppRosterStorage userForJID:[message from]
                                                                 xmppStream:xmppStream
                                                       managedObjectContext:[self managedObjectContext_roster]];
        
        NSString *nickName = [user nickname];
        if (nickName == NULL) {
            nickName = @"Someone";
        }
        
        if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateActive)
        {
            //處理使用者正在使用APP時收到新訊息的狀況,本處先讓手機震動一下
            AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
        }
        else
        {
            //若收到訊息時並未開啟APP,則推播新訊息給User
            UILocalNotification *localNotification = [[UILocalNotification alloc] init];
            localNotification.alertAction = @"OK";
            localNotification.alertBody = [NSString stringWithFormat:@"%@ send you a message",nickName];
            
            [[UIApplication sharedApplication] presentLocalNotificationNow:localNotification];
        }
        
    }
}

//收到他人上下線時會呼叫此函數

- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence
{
    DDLogVerbose(@"%@: %@ - %@", THIS_FILE, THIS_METHOD, [presence fromStr]);
  
    if (presence.status) {
        //處理接到他人上下線狀態更動
    }

}

- (void)xmppStream:(XMPPStream *)sender didReceiveError:(id)error
{
    DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
}

- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error
{
    DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
    
    if (!isXmppConnected)
    {
        DDLogError(@"Unable to connect to server. Check xmppStream.hostName");
    }
}



最後,處理關於好友請求的部分與Push Notification的設定就ok拉!



#pragma mark xmppRoster Deledgate

//當接收到他人對你的訂閱請求
- (void)xmppRoster:(XMPPRoster *)sender didReceivePresenceSubscriptionRequest:(XMPPPresence *)presence
{
    
    DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
    
    XMPPUserCoreDataStorageObject *user = [xmppRosterStorage userForJID:[presence from]
                                                             xmppStream:xmppStream
                                                   managedObjectContext:[self managedObjectContext_roster]];

    DDLogVerbose(@"didReceivePresenceSubscriptionRequest from user %@ ", user.jidStr);
    
    //直接接受他人對你的訂閱請求
    [xmppRoster acceptPresenceSubscriptionRequestFrom:[presence from]
                                       andAddToRoster:YES];
    
    
}


#pragma mark Receive Notification

//處理推播相關的函數

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
    
    NSLog(@"Received description: %@", [userInfo description]);
#if 0
    NSDictionary *aps = [userInfo objectForKey:@"aps"];
    if(aps != nil) {
        NSDictionary *alert = [aps objectForKey:@"alert"];
        if(alert != nil) {
            NSString *loc_key = [alert objectForKey:@"loc-key"];
            NSString *body = [alert objectForKey:@"body"];
            if(loc_key != nil) {
                if([loc_key isEqualToString:@"IM_MSG"]) {
                    // ???
                }
                else if([loc_key isEqualToString:@"IC_MSG"]) {
                    // ???
                }
            }
            if (body != nil) {
                PNMessage *pnMessage = [NSEntityDescription insertNewObjectForEntityForName:@"PNMessage"
                                                                     inManagedObjectContext:[self managedObjectContext]];
                
                NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
                [dateFormatter setDateFormat:@"HH:mm:ss"];
                NSString *TimeStr = [dateFormatter stringFromDate:[NSDate date]];
                [dateFormatter setDateFormat:@"yyyy/MM/dd"];
                NSString *DateStr = [dateFormatter stringFromDate:[NSDate date]];
                [dateFormatter release];
                
                NSString *fromStr =[NSString stringWithFormat:@"推播訊息 %@ %@", DateStr, TimeStr];
                
                pnMessage.fromUser = fromStr;
                pnMessage.messageText = body;
                NSError *error = nil;
                if (![[self managedObjectContext]save:&error]) {
                    NSLog(@"新增物件時遇到錯誤");
                }
                else {
                    NSLog(@"儲存成功:fromUser=%@ messageText=%@",
                          pnMessage.fromUser, pnMessage.messageText);
                }
            }
        }
    }
#endif
}


//Receive the push notification
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification
{
    // process background incoming call
    NSLog(@"didReceiveLocalNotification!");
    
    [self showAlarm:notification.alertBody];
    application.applicationIconBadgeNumber = 0;
    
}

- (void)showAlarm:(NSString *)text {
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Alarm"
                                                        message:text delegate:nil
                                              cancelButtonTitle:@"OK"
                                              otherButtonTitles:nil];
    [alertView show];
}




終於把APP Delegate的最初設定做完了,不過在這裡還有一些細節並未執行,像是收到訊息時應該如何顯示/收到好友上下線狀態改變等等,如果之後有時間再來處理吧!但是現在至少要確定它可以執行無誤,接下來就可以比較輕鬆地處理與XMPP的註冊、登入、使用者資料修改、好朋友與訊息傳送接收和聊天室拉。