XMPP之ios即时通讯客户端开发-配置XMPP基本信息之工程代码(五)
登录功能完成以后包含以下代码文件:
AppDelegate.h
AppDelegate.m
LoginViewController.h
LoginViewController.m
LoginUser.h
LoginUser.m
以下看代码:
// // AppDelegate.h // XMPP即时通讯 // // Created by Mac on 15/7/15. // Copyright (c) 2015年 聂小波. All rights reserved. // #import <UIKit/UIKit.h> #import "XMPPFramework.h" #define xmppDelegate (AppDelegate *)[[UIApplication sharedApplication] delegate] typedef void(^CompletionBlock)(); @interface AppDelegate : UIResponder <UIApplicationDelegate> @property (strong, nonatomic) UIWindow *window; #pragma mark - XMPP属性及方法 /** * 全局的XMPPStream,只读属性 */ @property (strong, nonatomic, readonly) XMPPStream *xmppStream; /** * 全局的xmppvCard模块,只读属性 */ @property (strong, nonatomic, readonly) XMPPvCardTempModule *xmppvCardModule; /** * 全局的XMPPvCardAvatar模块,只读属性 */ @property (strong, nonatomic, readonly) XMPPvCardAvatarModule *xmppvCardAvatarModule; /** * 全局的xmppRoster模块,只读属性 */ @property (strong, nonatomic, readonly) XMPPRoster *xmppRoster; /** * 全局的XMPPRosterCoreDataStorage模块,只读属性 */ @property (strong, nonatomic, readonly) XMPPRosterCoreDataStorage *xmppRosterStorage; /** * 消息存档(归档)模块,只读属性 */ @property (strong, nonatomic, readonly) XMPPMessageArchiving *xmppMessageArchiving; @property (strong, nonatomic, readonly) XMPPMessageArchivingCoreDataStorage *xmppMessageArchivingCoreDataStorage; /** * 传输文件socket数组 */ @property (strong, nonatomic) NSMutableArray *socketList; /** * 是否注册用户标示 */ @property (assign, nonatomic) BOOL isRegisterUser; /** * 连接到服务器 * * 注释:用户信息保存在系统偏好中 * * @param completion 连接正确的块代码 * @param faild 连接错误的块代码 */ - (void)connectWithCompletion:(CompletionBlock)completion failed:(CompletionBlock)faild; /** * 注销用户登录 */ - (void)logout; @end
// // AppDelegate.m // XMPP即时通讯 // // Created by Mac on 15/7/15. // Copyright (c) 2015年 聂小波. All rights reserved. // #import "AppDelegate.h" #import "LoginUser.h" #import "NSString+Helper.h" #import "NSData+XMPP.h" #define kNotificationUserLogonState @"NotificationUserLogon" @interface AppDelegate() <XMPPStreamDelegate, XMPPRosterDelegate, TURNSocketDelegate> { CompletionBlock _completionBlock; // 成功的块代码 CompletionBlock _faildBlock; // 失败的块代码 XMPPReconnect *_xmppReconnect; // XMPP重新连接XMPPStream XMPPvCardCoreDataStorage *_xmppvCardStorage; // 电子名片的数据存储模块 XMPPCapabilities *_xmppCapabilities; // 实体扩展模块 XMPPCapabilitiesCoreDataStorage *_xmppCapabilitiesCoreDataStorage; // 数据存储模块 } // 设置XMPPStream - (void)setupStream; // 销毁XMPPStream并注销已注册的扩展模块 - (void)teardownStream; // 通知服务器器用户上线 - (void)goOnline; // 通知服务器器用户下线 - (void)goOffline; // 连接到服务器 - (void)connect; // 与服务器断开连接 - (void)disconnect; @end @implementation AppDelegate #pragma mark 根据用户登录状态加载对应的Storyboard显示 - (void)showStoryboardWithLogonState:(BOOL)isUserLogon { UIStoryboard *storyboard = nil; if (isUserLogon) { // 显示Main.storyboard storyboard = [UIStoryboard storyboardWithName:@"LoginViewController" bundle:nil]; } else { // 显示Login.sotryboard storyboard = [UIStoryboard storyboardWithName:@"LoginViewController" bundle:nil]; } // 在主线程队列负责切换Storyboard,而不影响后台代理的数据处理 dispatch_async(dispatch_get_main_queue(), ^{ // 如果在项目属性中,没有指定主界面(启动的Storyboard,self.window不会被实例化) // 把Storyboard的初始视图控制器设置为window的rootViewController [self.window setRootViewController:storyboard.instantiateInitialViewController]; if (!self.window.isKeyWindow) { [self.window makeKeyAndVisible]; } }); } #pragma mark - AppDelegate方法 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 1. 实例化window // self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; // 2. 设置XMPPStream [self setupStream]; // 3. 实例化socket数组 _socketList = [NSMutableArray array]; return YES; } - (void)applicationWillResignActive:(UIApplication *)application { [self disconnect]; } - (void)applicationDidBecomeActive:(UIApplication *)application { // 应用程序被激活后,直接连接,使用系统偏好中的保存的用户记录登录 // 从而实现自动登录的效果! [self connect]; } - (void)dealloc { // 释放XMPP相关对象及扩展模块 [self teardownStream]; } #pragma mark - XMPP相关方法 // 设置XMPPStream - (void)setupStream { // 0. 方法被调用时,要求_xmppStream必须为nil,否则通过断言提示程序员,并终止程序运行! // NSAssert(_xmppStream == nil, @"XMPPStream被多次实例化!"); // 1. 实例化XMPPSteam _xmppStream = [[XMPPStream alloc] init]; // _xmppStream.hostName = @"127.0.0.1"; ////// mysql://localhost:3306/openfire // _xmppStream.hostPort = 5222; // ////// NSString *myJID=@"admin"; // _xmppStream.myJID = [XMPPJID jidWithString:[NSString stringWithFormat:@"admin@127.0.0.1"]]; // 让XMPP在真机运行时支持后台,在模拟器上是不支持后台服务运行的 #if !TARGET_IPHONE_SIMULATOR { // 允许XMPPStream在真机运行时,支持后台网络通讯! [_xmppStream setEnableBackgroundingOnSocket:YES]; } #endif // 2. 扩展模块 // 2.1 重新连接模块 _xmppReconnect = [[XMPPReconnect alloc] init]; // 2.2 电子名片模块 _xmppvCardStorage = [XMPPvCardCoreDataStorage sharedInstance]; _xmppvCardModule = [[XMPPvCardTempModule alloc] initWithvCardStorage:_xmppvCardStorage]; _xmppvCardAvatarModule = [[XMPPvCardAvatarModule alloc] initWithvCardTempModule:_xmppvCardModule]; // 2.4 花名册模块 _xmppRosterStorage = [[XMPPRosterCoreDataStorage alloc] init]; _xmppRoster = [[XMPPRoster alloc] initWithRosterStorage:_xmppRosterStorage]; // 设置自动接收好友订阅请求 [_xmppRoster setAutoAcceptKnownPresenceSubscriptionRequests:YES]; // 自动从服务器更新好友记录,例如:好友自己更改了名片 [_xmppRoster setAutoFetchRoster:YES]; // 2.5 实体扩展模块 _xmppCapabilitiesCoreDataStorage = [[XMPPCapabilitiesCoreDataStorage alloc] init]; _xmppCapabilities = [[XMPPCapabilities alloc] initWithCapabilitiesStorage:_xmppCapabilitiesCoreDataStorage]; // 2.6 消息归档模块 _xmppMessageArchivingCoreDataStorage = [[XMPPMessageArchivingCoreDataStorage alloc] init]; _xmppMessageArchiving = [[XMPPMessageArchiving alloc] initWithMessageArchivingStorage:_xmppMessageArchivingCoreDataStorage]; // 3. 将重新连接模块添加到XMPPStream [_xmppReconnect activate:_xmppStream]; [_xmppvCardModule activate:_xmppStream]; [_xmppvCardAvatarModule activate:_xmppStream]; [_xmppRoster activate:_xmppStream]; [_xmppCapabilities activate:_xmppStream]; [_xmppMessageArchiving activate:_xmppStream]; // 4. 添加代理 // 由于所有网络请求都是做基于网络的数据处理,这些数据处理工作与界面UI无关。 // 因此可以让代理方法在其他线城中运行,从而提高程序的运行性能,避免出现应用程序阻塞的情况 [_xmppStream addDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)]; [_xmppRoster addDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)]; } // 销毁XMPPStream并注销已注册的扩展模块 - (void)teardownStream { // 1. 删除代理 [_xmppStream removeDelegate:self]; [_xmppRoster removeDelegate:self]; // 2. 取消激活在setupStream方法中激活的扩展模块 [_xmppReconnect deactivate]; [_xmppvCardModule deactivate]; [_xmppvCardAvatarModule deactivate]; [_xmppRoster deactivate]; [_xmppCapabilities deactivate]; [_xmppMessageArchiving deactivate]; // 3. 断开XMPPStream的连接 [_xmppStream disconnect]; // 4. 内存清理 _xmppStream = nil; _xmppReconnect = nil; _xmppvCardModule = nil; _xmppvCardAvatarModule = nil; _xmppvCardStorage = nil; _xmppRoster = nil; _xmppRosterStorage = nil; _xmppCapabilities = nil; _xmppCapabilitiesCoreDataStorage = nil; _xmppMessageArchiving = nil; _xmppMessageArchivingCoreDataStorage = nil; } // 通知服务器器用户上线 - (void)goOnline { // 1. 实例化一个”展现“,上线的报告,默认类型为:available XMPPPresence *presence = [XMPPPresence presence]; // 2. 发送Presence给服务器 // 服务器知道“我”上线后,只需要通知我的好友,而无需通知我,因此,此方法没有回调 [_xmppStream sendElement:presence]; } // 通知服务器器用户下线 - (void)goOffline { // 1. 实例化一个”展现“,下线的报告 XMPPPresence *presence = [XMPPPresence presenceWithType:@"unavailable"]; // 2. 发送Presence给服务器,通知服务器客户端下线 [_xmppStream sendElement:presence]; } // 连接到服务器 - (void)connect { // 1. 如果XMPPStream当前已经连接,直接返回 if ([_xmppStream isConnected]) { return; } // 在C语言中if判断真假:非零即真,如果_xmppStream==nil下面这段代码,与上面的代码结果不同。 // if (![_xmppStream isDisconnected]) { // return; // } // 2. 指定用户名、主机(服务器),连接时不需要password NSString *hostName = [[LoginUser sharedLoginUser] hostName]; NSString *userName = [[LoginUser sharedLoginUser] myJIDName]; // 如果没有主机名或用户名(通常第一次运行时会出现),直接显示登录窗口 // if ([hostName isEmptyString] || [userName isEmptyString]) { // [self showStoryboardWithLogonState:NO]; // // return; // } // 3. 设置XMPPStream的JID和主机 [_xmppStream setMyJID:[XMPPJID jidWithString:userName]]; [_xmppStream setHostName:hostName]; // 4. 开始连接 NSError *error = nil; [_xmppStream connectWithTimeout:XMPPStreamTimeoutNone error:&error]; // 提示:如果没有指定JID和hostName,才会出错,其他都不出错。 if (error) { NSLog(@"连接请求发送出错 - %@", error.localizedDescription); } else { NSLog(@"连接请求发送成功!"); } } #pragma mark 连接到服务器 - (void)connectWithCompletion:(CompletionBlock)completion failed:(CompletionBlock)faild { // 1. 记录块代码 _completionBlock = completion; _faildBlock = faild; // 2. 如果已经存在连接,先断开连接,然后再次连接 if ([_xmppStream isConnected]) { [_xmppStream disconnect]; } // 3. 连接到服务器 [self connect]; } // 与服务器断开连接 - (void)disconnect { // 1. 通知服务器下线 [self goOffline]; // 2. XMPPStream断开连接 [_xmppStream disconnect]; } - (void)logout { // 1. 通知服务器下线,并断开连接 [self disconnect]; // 2. 显示用户登录Storyboard [self showStoryboardWithLogonState:NO]; } #pragma mark - 代理方法 #pragma mark 连接完成(如果服务器地址不对,就不会调用此方法) - (void)xmppStreamDidConnect:(XMPPStream *)sender { // 从系统偏好读取用户密码 NSString *password = [[LoginUser sharedLoginUser] password]; if (_isRegisterUser) { // 用户注册,发送注册请求 [_xmppStream registerWithPassword:password error:nil]; } else { // 用户登录,发送身份验证请求 [_xmppStream authenticateWithPassword:password error:nil]; } } #pragma mark 注册成功 - (void)xmppStreamDidRegister:(XMPPStream *)sender { _isRegisterUser = NO; // 注册成功,直接发送验证身份请求,从而触发后续的操作 [_xmppStream authenticateWithPassword:[LoginUser sharedLoginUser].password error:nil]; } #pragma mark 注册失败(用户名已经存在) - (void)xmppStream:(XMPPStream *)sender didNotRegister:(DDXMLElement *)error { _isRegisterUser = NO; if (_faildBlock != nil) { dispatch_async(dispatch_get_main_queue(), ^{ _faildBlock(); }); } } #pragma mark 身份验证通过 - (void)xmppStreamDidAuthenticate:(XMPPStream *)sender { if (_completionBlock != nil) { dispatch_async(dispatch_get_main_queue(), ^{ _completionBlock(); }); } // 通知服务器用户上线 [self goOnline]; // 显示主Storyboard [self showStoryboardWithLogonState:YES]; } #pragma mark 密码错误,身份验证失败 - (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(DDXMLElement *)error { if (_faildBlock != nil) { dispatch_async(dispatch_get_main_queue(), ^{ _faildBlock(); }); } // 显示用户登录Storyboard [self showStoryboardWithLogonState:NO]; } #pragma mark 用户展现变化 - (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence { NSLog(@"接收到用户展现数据 - %@", presence); // 1. 判断接收到的presence类型是否为subscribe if ([presence.type isEqualToString:@"subscribe"]) { // 2. 取出presence中的from的jid XMPPJID *from = [presence from]; // 3. 接受来自from添加好友的订阅请求 [_xmppRoster acceptPresenceSubscriptionRequestFrom:from andAddToRoster:YES]; } } #pragma mark 判断IQ是否为SI请求 - (BOOL)isSIRequest:(XMPPIQ *)iq { NSXMLElement *si = [iq elementForName:@"si" xmlns:@"http://jabber.org/protocol/si"]; NSString *uuid = [[si attributeForName:@"id"]stringValue]; if(si &&uuid ){ return YES; } return NO; } #pragma mark 接收请求 - (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq { NSLog(@"接收到请求 - %@", iq); // 0. 判断IQ是否为SI请求 if ([self isSIRequest:iq]) { TURNSocket *socket = [[TURNSocket alloc] initWithStream:_xmppStream toJID:iq.to]; [_socketList addObject:socket]; [socket startWithDelegate:self delegateQueue:dispatch_get_main_queue()]; } else if ([TURNSocket isNewStartTURNRequest:iq]) { // 1. 判断iq的类型是否为新的文件传输请求 // 1) 实例化socket TURNSocket *socket = [[TURNSocket alloc] initWithStream:sender incomingTURNRequest:iq]; // 2) 使用一个数组成员记录住所有传输文件使用的socket [_socketList addObject:socket]; // 3)添加代理 [socket startWithDelegate:self delegateQueue:dispatch_get_main_queue()]; } return YES; } #pragma mark 接收消息 - (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message { NSLog(@"接收到用户消息 - %@", message); // 1. 针对图像数据单独处理,取出数据 NSString *imageStr = [[message elementForName:@"imageData"] stringValue]; if (imageStr) { // 2. 解码成图像 NSData *data = [[NSData alloc] initWithBase64EncodedString:imageStr options:NSDataBase64DecodingIgnoreUnknownCharacters]; // 3. 保存图像 UIImage *image = [UIImage imageWithData:data]; // 4. 将图像保存到相册 // 1) target 通常用self // 2) 保存完图像调用的方法 // 3) 上下文信息 UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil); } } #pragma mark - XMPPRoster代理 - (void)xmppRoster:(XMPPRoster *)sender didReceivePresenceSubscriptionRequest:(XMPPPresence *)presence { NSLog(@"接收到其他用户的请求 %@", presence); } #pragma mark - TURNSocket代理 - (void)turnSocket:(TURNSocket *)sender didSucceed:(GCDAsyncSocket *)socket { NSLog(@"成功"); // 保存或者发送文件 // 写数据方法,向其他客户端发送文件 // socket writeData:<#(NSData *)#> withTimeout:<#(NSTimeInterval)#> tag:<#(long)#> // 读数据方法,接收来自其他客户端的文件 // socket readDataToData:<#(NSData *)#> withTimeout:<#(NSTimeInterval)#> tag:<#(long)#> // 读写操作完成之后断开网络连接 [socket disconnectAfterReadingAndWriting]; [_socketList removeObject:sender]; } - (void)turnSocketDidFail:(TURNSocket *)sender { NSLog(@"失败"); [_socketList removeObject:sender]; } @end
// LoginViewController.h // 02.用户登录&注册 // #import <UIKit/UIKit.h> @interface LoginViewController : UIViewController @end
// // LoginViewController.m // 02.用户登录&注册 // #import "LoginViewController.h" #import "NSString+Helper.h" #import "AppDelegate.h" #import "LoginUser.h" @interface LoginViewController () <UITextFieldDelegate> @property (weak, nonatomic) IBOutlet UITextField *userNameText; @property (weak, nonatomic) IBOutlet UITextField *passwordText; @property (weak, nonatomic) IBOutlet UITextField *hostNameText; @property (weak, nonatomic) IBOutlet UIButton *registerButton; @property (weak, nonatomic) IBOutlet UIButton *loginButton; @end @implementation LoginViewController #pragma mark - AppDelegate 的助手方法 - (AppDelegate *)appDelegate { return [[UIApplication sharedApplication] delegate]; } - (void)viewDidLoad { [super viewDidLoad]; // 1. 拉伸按钮背景图片 // 1) 登录按钮 UIImage *loginImage = [UIImage imageNamed:@"LoginGreenBigBtn"]; loginImage = [loginImage stretchableImageWithLeftCapWidth:loginImage.size.width * 0.5 topCapHeight:loginImage.size.height * 0.5]; [_loginButton setBackgroundImage:loginImage forState:UIControlStateNormal]; // 2) 注册按钮 UIImage *registerImage = [UIImage imageNamed:@"LoginwhiteBtn"]; registerImage = [registerImage stretchableImageWithLeftCapWidth:registerImage.size.width * 0.5 topCapHeight:registerImage.size.height * 0.5]; [_registerButton setBackgroundImage:registerImage forState:UIControlStateNormal]; // 2. 设置界面文本的初始值 _userNameText.text = [[LoginUser sharedLoginUser] userName]; _passwordText.text = [[LoginUser sharedLoginUser] password]; _hostNameText.text = [[LoginUser sharedLoginUser] hostName]; // 3. 设置文本焦点 if ([_userNameText.text isEmptyString]) { [_userNameText becomeFirstResponder]; } else { [_passwordText becomeFirstResponder]; } } #pragma mark UITextField代理方法 - (BOOL)textFieldShouldReturn:(UITextField *)textField { if (textField == _userNameText) { [_passwordText becomeFirstResponder]; } else if (textField == _passwordText && [_hostNameText.text isEmptyString]) { [_hostNameText becomeFirstResponder]; } else { [self userLoginAndRegister:nil]; } return YES; } #pragma mark - Actions #pragma mark 用户登录 - (IBAction)userLoginAndRegister:(UIButton *)button { // 1. 检查用户输入是否完整,在商业软件中,处理用户输入时 // 通常会截断字符串前后的空格(密码除外),从而可以最大程度地降低用户输入错误 NSString *userName = [_userNameText.text trimString]; // 用些用户会使用空格做密码,因此密码不能去除空白字符 NSString *password = _passwordText.text; NSString *hostName = [_hostNameText.text trimString]; if ([userName isEmptyString] || [password isEmptyString] || [hostName isEmptyString]) { UIAlertView *alter = [[UIAlertView alloc] initWithTitle:@"提示" message:@"登录信息不完整" delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil, nil]; [alter show]; return; } // 2. 将用户登录信息写入系统偏好 [[LoginUser sharedLoginUser] setUserName:userName]; [[LoginUser sharedLoginUser] setPassword:password]; [[LoginUser sharedLoginUser] setHostName:hostName]; // 3. 让AppDelegate开始连接 // 告诉AppDelegate,当前是注册用户 NSString *errorMessage = nil; if (button.tag == 1) { [self appDelegate].isRegisterUser = YES; errorMessage = @"注册用户失败!"; } else { errorMessage = @"用户登录失败!"; } [[self appDelegate] connectWithCompletion:nil failed:^{ UIAlertView *alter = [[UIAlertView alloc] initWithTitle:@"提示" message:errorMessage delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil, nil]; [alter show]; if (button.tag == 1) { // 注册用户失败通常是因为用户名重复 [_userNameText becomeFirstResponder]; } else { // 登录失败通常是密码输入错误 [_passwordText setText:@""]; [_passwordText becomeFirstResponder]; } }]; } @end
// LoginUser.h #import <Foundation/Foundation.h> #import "Singleton.h" @interface LoginUser : NSObject single_interface(LoginUser) @property (strong, nonatomic) NSString *userName; @property (strong, nonatomic) NSString *password; @property (strong, nonatomic) NSString *hostName; @property (strong, nonatomic, readonly) NSString *myJIDName; @end
// // LoginUser.m #import "LoginUser.h" #import "NSString+Helper.h" #define kXMPPUserNameKey @"admin" #define kXMPPPasswordKey @"admin" #define kXMPPHostNameKey @"127.0.0.1" @implementation LoginUser single_implementation(LoginUser) #pragma mark - 私有方法 - (NSString *)loadStringFromDefaultsWithKey:(NSString *)key { NSString *str = [[NSUserDefaults standardUserDefaults] stringForKey:key]; return (str) ? str : @""; } #pragma mark - getter & setter - (NSString *)userName { return [self loadStringFromDefaultsWithKey:kXMPPUserNameKey]; } - (void)setUserName:(NSString *)userName { [userName saveToNSDefaultsWithKey:kXMPPUserNameKey]; } - (NSString *)password { return [self loadStringFromDefaultsWithKey:kXMPPPasswordKey]; } - (void)setPassword:(NSString *)password { [password saveToNSDefaultsWithKey:kXMPPPasswordKey]; } - (NSString *)hostName { return [self loadStringFromDefaultsWithKey:kXMPPHostNameKey]; } - (void)setHostName:(NSString *)hostName { [hostName saveToNSDefaultsWithKey:kXMPPHostNameKey]; } - (NSString *)myJIDName { return [NSString stringWithFormat:@"%@@%@", self.userName, self.hostName]; } @end