Welcome to the third installment of our series on building a Jabber client with the iOS SDK. In this tutorial, we will add XMPP functionalities to the Application Delegate. Placing the functionality in the App Delegate will enable us to access XMPP functionalities from anywhere in the application easily.
Integrating XMPP in the AppDelegate
As mentioned, inserting the XMPP functionality in the App Delegate is a great way to make the functionality easily available throughout the app. At any place in your application code, you can access the App Delegate with the following code snippet:
[[UIApplication sharedApplication] delegate]
The XMPP class will dispatch events by means of protocols which we will define below. This is the list of events handled by this class:
- The client connected with the server
- The client authenticated with the server
- The client received a notification of presence (e.g. a user logged in)
- The client received a message
Let's get started by adding the some property to the application delegate. First we need to import some XMPP stuff in the header:
#import "XMPP.h"
This is the minimal set of classes needed to build our application. If you want to digg into something more complex you can checkout the example bundled with the XMPP library repository. Here is our first implementation of this class:
@class SMBuddyListViewController; @interface jabberClientAppDelegate : NSObject { UIWindow *window; SMBuddyListViewController *viewController; XMPPStream *xmppStream; NSString *password; BOOL isOpen; } @property (nonatomic, retain) IBOutlet UIWindow *window; @property (nonatomic, retain) IBOutlet SMBuddyListViewController *viewController; @property (nonatomic, readonly) XMPPStream *xmppStream; - (BOOL)connect; - (void)disconnect; @end
XMPPStream
will be the barebone of our client-server communication system and all messages will be exchanged through it. We will also define the methods to manage connection and disconnection. The implementation of this class is pretty complex, so we will break it down into many steps. First, we need a few more accessory methods to handle client-server communications. These can be private, so we place them in the implementation of the class:
@interface JabberClientAppDelegate() - (void)setupStream; - (void)goOnline; - (void)goOffline; @end @implementation JabberClientAppDelegate @end
Here, the most important is setupStream
, which creates the channel to manage the exchange of messages.
- (void)setupStream { xmppStream = [[XMPPStream alloc] init]; [xmppStream addDelegate:self delegateQueue:dispatch_get_main_queue()]; }
Just two lines of code, but behind that many things happen. The dispatch_get_main_queue()
is a function which returns a reference
to the system level asynchronous execution mechanism, to which we can sumbit tasks and receive notifications. Here we "simply" tell
that our class is the delegate for the notifications sent from the main queue, which is run in the main thread of our application. See here for more details about Grand Central Dispatch.
Offline and Online functions are able to notify other users when we are connected or not. They are defined by sending an XMPPPresence
object through the socket. The server will dispatch the notification accordingly.
- (void)goOnline { XMPPPresence *presence = [XMPPPresence presence]; [[self xmppStream] sendElement:presence]; } - (void)goOffline { XMPPPresence *presence = [XMPPPresence presenceWithType:@"unavailable"]; [[self xmppStream] sendElement:presence]; }
The connect
method is the most important, for it manages the login operation. It returns a boolean representing whether the connection was successful or not. At first it sets up the stream, then it uses data stored in NSUserDefaults
to decorate the stream and call a connect message. An alert view is displayed if the connection is not successful.
- (BOOL)connect { [self setupStream]; NSString *jabberID = [[NSUserDefaults standardUserDefaults] stringForKey:@"userID"]; NSString *myPassword = [[NSUserDefaults standardUserDefaults] stringForKey:@"userPassword"]; if (![xmppStream isDisconnected]) { return YES; } if (jabberID == nil || myPassword == nil) { return NO; } [xmppStream setMyJID:[XMPPJID jidWithString:jabberID]]; password = myPassword; NSError *error = nil; if (![xmppStream connect:&error]) { UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error" message:[NSString stringWithFormat:@"Can't connect to server %@", [error localizedDescription]] delegate:nil cancelButtonTitle:@"Ok" otherButtonTitles:nil]; [alertView show]; [alertView release]; return NO; } return YES; }
For sake of completeness, we also implement the disconnect method which is defined as follows:
- (void)disconnect { [self goOffline]; [xmppStream disconnect]; }
Now that we have some of the basic functions we can use them in specific cases, for example, when the application becomes active or inactive.
- (void)applicationWillResignActive:(UIApplication *)application { [self disconnect]; } - (void)applicationDidBecomeActive:(UIApplication *)application { [self connect]; }
We are left with the core of the system, the notifications of events and related behaviors, which we implement by means of protocols.
Defining Protocols
We will define two protocols, one for chat notifications like "a buddy went offline", and one for dispatching messages received. The first protocol includes the description of three events:
@protocol SMChatDelegate - (void)newBuddyOnline:(NSString *)buddyName; - (void)buddyWentOffline:(NSString *)buddyName; - (void)didDisconnect; @end
The first two messages are related to the presences of a buddy. We will react to these by adding or removing elements to the online buddies table. The third just notifies the server when our client disconnects. The second protocol is simpler, for it manages just the event of message reception.
@protocol SMMessageDelegate - (void)newMessageReceived:(NSDictionary *)messageContent; @end
For the sake of simplicity, to represent the message we will use a dictionary with two keys, @"msg" and @"sender", to represent the actual message and the actual sender.
Implementing Protocols
Both protocols dispatch messages from the UIApplicationDelegate
. So we extend our main class by adding two properties (one for each delegate).
@interface JabberClientAppDelegate : NSObject { ... __weak NSObject *_chatDelegate; __weak NSObject *_messageDelegate; } @property (nonatomic, assign) id _chatDelegate; @property (nonatomic, assign) id _messageDelegate; @end
In the implementation we should remember to synthesize these properties.
@synthesize _chatDelegate, _messageDelegate;
Now our main class is ready to dispatch events to delegates. But which events? Those received from the Grand Central Dispatch. If you remember,
we have setup our UIApplicationDelegate
as a delegate for stream messages. Such delegates have the following signatures. The names
are pretty self explanatory, but we added comments within to make it even clearer.
- (void)xmppStreamDidConnect:(XMPPStream *)sender { // connection to the server successful } - (void)xmppStreamDidAuthenticate:(XMPPStream *)sender { // authentication successful } - (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message { // message received } - (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence { // a buddy went offline/online }
Let's start by authentication when we connect to the server.
- (void)xmppStreamDidConnect:(XMPPStream *)sender { isOpen = YES; NSError *error = nil; [[self xmppStream] authenticateWithPassword:password error:&error]; }
When authentication is successful, we should notify the server that we are online.
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender { [self goOnline]; }
When we receive a presence notification, we can dispatch the message to the chat delegate.
- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence { NSString *presenceType = [presence type]; // online/offline NSString *myUsername = [[sender myJID] user]; NSString *presenceFromUser = [[presence from] user]; if (![presenceFromUser isEqualToString:myUsername]) { if ([presenceType isEqualToString:@"available"]) { [_chatDelegate newBuddyOnline:[NSString stringWithFormat:@"%@@%@", presenceFromUser, @"jerry.local"]]; } else if ([presenceType isEqualToString:@"unavailable"]) { [_chatDelegate buddyWentOffline:[NSString stringWithFormat:@"%@@%@", presenceFromUser, @"jerry.local"]]; } } }
The delegate will use these events to populate the online buddies table accordingly (see below). Finally, we are left with the message received notification.
- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message { NSString *msg = [[message elementForName:@"body"] stringValue]; NSString *from = [[message attributeForName:@"from"] stringValue]; NSMutableDictionary *m = [[NSMutableDictionary alloc] init]; [m setObject:msg forKey:@"msg"]; [m setObject:from forKey:@"sender"]; [_messageDelegate newMessageReceived:m]; [m release]; }
In this case, we build a dictionary as requested by the protocol and we call the corresponding method. At this point the core of our system is ready. We just have to make the user interface components react accordingly.
Hooking Up Views and Controllers
We start by modifying the buddy list controller, which manages the first view displayed when the app is started. We add the chat delegate to the interface as follows:
@interface SMBuddyListViewController : UIViewController <..., SMChatDelegate> { @end
We add a few access methods to point to the application delegate and stream:
- (JabberClientAppDelegate *)appDelegate { return (JabberClientAppDelegate *)[[UIApplication sharedApplication] delegate]; } - (XMPPStream *)xmppStream { return [[self appDelegate] xmppStream]; }
We also have to extend the viewDidLoad
message to set our view controller as a delegate for the chat protocol.
- (void)viewDidLoad { ... JabberClientAppDelegate *del = [self appDelegate]; del._chatDelegate = self; }
When the view appears, if credentials have been entered already, we call the connect method of the application delegate:
- (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; NSString *login = [[NSUserDefaults standardUserDefaults] objectForKey:@"userID"]; if (login) { if ([[self appDelegate] connect]) { NSLog(@"show buddy list"); } } else { [self showLogin]; } }
Finally, we have to add or remove objects from the array of online buddies according to the events dispatched by the application delegate.
- (void)newBuddyOnline:(NSString *)buddyName { [onlineBuddies addObject:buddyName]; [self.tView reloadData]; } - (void)buddyWentOffline:(NSString *)buddyName { [onlineBuddies removeObject:buddyName]; [self.tView reloadData]; }
If you run the application now and a buddy comes online, the table view gets populated with his username as in the following figure:
Important Note: Depending on the server settings, you might need to wait for some time in order to receive the "new buddy is online" notifications. This time tends to be from 20 to 60 seconds.
To start a chat with the user we have to show the chat view when the corresponding cell is tapped.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSString *userName = (NSString *) [onlineBuddies objectAtIndex:indexPath.row]; SMChatViewController *chatController = [[SMChatViewController alloc] initWithUser:userName]; [self presentModalViewController:chatController animated:YES]; }
To finalize the application we need to add the implementation of the message delegate to the chat view controller. The steps to do so are similar to those applied to the buddy list controller. We add the delegate in the interface file:
@interface SMChatViewController : UIViewController < ... , SMMessageDelegate> @end
We add accessors to the implementation:
- (JabberClientAppDelegate *)appDelegate { return (JabberClientAppDelegate *)[[UIApplication sharedApplication] delegate]; } - (XMPPStream *)xmppStream { return [[self appDelegate] xmppStream]; }
We add the implementation of initWithUser:username
:
- (id) initWithUser:(NSString *) userName { if (self = [super init]) { chatWithUser = userName; } return self; }
We extend the viewDidLoad
to declare the message delegate and we also set the text field as a first responder to keyboard input:
- (void)viewDidLoad { ... JabberClientAppDelegate *del = [self appDelegate]; del._messageDelegate = self; [self.messageField becomeFirstResponder]; }
To send a message, we need to create an xml element as required by the XMPP protocol and send it over the stream. Here is how we update the sendMessage
method:
- (IBAction)sendMessage { NSString *messageStr = self.messageField.text; if([messageStr length] > 0) { NSXMLElement *body = [NSXMLElement elementWithName:@"body"]; [body setStringValue:messageStr]; NSXMLElement *message = [NSXMLElement elementWithName:@"message"]; [message addAttributeWithName:@"type" stringValue:@"chat"]; [message addAttributeWithName:@"to" stringValue:chatWithUser]; [message addChild:body]; [self.xmppStream sendElement:message]; self.messageField.text = @""; NSString *m = [NSString stringWithFormat:@"%@:%@", messageStr, @"you"]; NSMutableDictionary *m = [[NSMutableDictionary alloc] init]; [m setObject:messageStr forKey:@"msg"]; [m setObject:@"you" forKey:@"sender"]; [messages addObject:m]; [self.tView reloadData]; [m release]; } }
We are now done! You can test the final implementation of our iOS client. We start the server, iChat and our jabber client. After awhile, both clients should receive a presence notification and recognize each other as online. On the iPhone we tap on the online buddy and the chat view shows up. Now we are ready to chat. Here is a screenshot of the final application at work.
Source Code
The complete source code for this project can be found on GitHub here.
Comments