Building a Jabber Client for iOS: Custom Chat View and Emoticons

Building a Jabber Client for iOS: Custom Chat View and Emoticons

Tutorial Details
  • Technology: iOS SDK
  • Completion Time: 30 - 60 Minutes
  • Difficulty: Intermediate
This entry is part 4 of 4 in the series Building a Jabber Client for iOS

In this part of the series, we will build a custom view to make chat messages look more professional. Moreover, we will also add real emoticons to display in place of their textual counterparts.

Small Bug Fix

Before going on we have noticed a small bug introduced in part 3 of the series. When we receive a notification that a new buddy is online, we add it to the array of online people and refresh the view.

- (void)newBuddyOnline:(NSString *)buddyName
{
    [onlineBuddies addObject:buddyName];
    [self.tView reloadData];
}

This could work if we received an online notification just once. In reality, such a notification is sent out periodically. This might be due to the nature of the XMPP protocol or the ejabbered implementation that we are using. In any case, to avoid duplicates, we should check whether we have already added to the array the buddy carried in the notification. So, we refactor like this:

- (void)newBuddyOnline:(NSString *)buddyName {
    if (![onlineBuddies containsObject:buddyName]) {
        [onlineBuddies addObject:buddyName];
        [self.tView reloadData];
    }
}

And the bug is fixed.

Building Custom Chat Messages

During the series we have built a chat view controller which displays messages using standard visual components included in the iOS SDK. Our goal is to build something prettier, which displays the sender and the time of a message. We take inspiration from the SMS application bundled in iOS, which displays the content of the message wrapped by a balloon like bubble. The result that we want to achieve is shown in the following figure:

The expected result

The components for the input are on the top, as in the current implementation. We need to create a custom view for the cells of the table. This is the list of requirements:

  • Each cell shows the sender and the time of the message by means of a label at the top
  • Each message is wrapped by a balloon image with some padding
  • Background images for the message are different according to the sender
  • The height of the message (and its background image) may vary according to the length of the text

Saving the Timestamp of a Message

The current implementation does not save the time at which a message as been sent/received. Since we have to perform this operation in more than one place, we create a utility method which returns the current date and time in the form of a string. We do it by means of a category, extending the NSString class.
Following the convention suggested by Apple we create two source files named NSString+Utils.h and NSString+Utils.m. The header file contains the following code:

@interface NSString (Utils)
+ (NSString *) getCurrentTime;
@end

In the implementation, we define the static method getCurrentTime as follows

@implementation NSString (Utils)
+ (NSString *) getCurrentTime {
	NSDate *nowUTC = [NSDate date];
	NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
	[dateFormatter setTimeZone:[NSTimeZone localTimeZone]];
	[dateFormatter setDateStyle:NSDateFormatterMediumStyle];
	[dateFormatter setTimeStyle:NSDateFormatterMediumStyle];
	return [dateFormatter stringFromDate:nowUTC];
}
@end

Such a method will return strings like the following: Sep 12, 2011 7:34:21 PM

If you want to customize the format of the date you can consult the documentation of NSFormatter.
Now that we have the utility method ready we need to save the date and time of sent and received messages. Both modifications pertain to the SMChatViewController when we send a message:

- (IBAction)sendMessage {
    NSString *messageStr = self.messageField.text;
    if([messageStr length] > 0) {
		...
		NSMutableDictionary *m = [[NSMutableDictionary alloc] init];
		[m setObject:@"you" forKey:@"sender"];
		[m setObject:[NSString getCurrentTime] forKey:@"time"];
         ...
    }
    ...
}

And when we receive it:

- (void)newMessageReceived:(NSDictionary *)messageContent {
	NSString *m = [messageContent objectForKey:@"msg"];
  ...
	[messageContent setObject:[NSString getCurrentTime] forKey:@"time"];
  ...
}

Now we have all the data structures we need to build our custom interface, so let’s start by customizing our cell view.

The Balloon View

Most of the modifications we are going to introduce are related to the SMChatViewController, and particularly to the method -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath, which is where the content of each cell is drawn.
The current implementation uses a generic UITableViewCell, but that is not enough for our requirements, so we need to subclass it. We call our new class SMMessageViewTableCell.

The class needs three visual elements:

  • A label to show date and time
  • A textual view to show the message
  • An image view to display a balloon shaped custom view

Here is the corresponding interface file:

@interface SMMessageViewTableCell : UITableViewCell {
	UILabel	*senderAndTimeLabel;
	UITextView *messageContentView;
	UIImageView *bgImageView;
}
@property (nonatomic,assign) UILabel *senderAndTimeLabel;
@property (nonatomic,assign) UITextView *messageContentView;
@property (nonatomic,assign) UIImageView *bgImageView;
@end

The first step of the implementation is to synthesize properties and set up the deallocation of instances.

@implementation SMMessageViewTableCell
@synthesize senderAndTimeLabel, messageContentView, bgImageView;
- (void)dealloc {
    [senderAndTimeLabel release];
    [messageContentView release];
    [bgImageView release];
    [super dealloc];
}
@end

Then we can override the constructor to add the visual elements to the contentView of the cell. The senderAndTimeLabel is the only element with a fixed position so we can set its frame and appearance right in the constructor.

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
		senderAndTimeLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 5, 300, 20)];
		senderAndTimeLabel.textAlignment = UITextAlignmentCenter;
		senderAndTimeLabel.font = [UIFont systemFontOfSize:11.0];
		senderAndTimeLabel.textColor = [UIColor lightGrayColor];
		[self.contentView addSubview:senderAndTimeLabel];
    }
    return self;
}

The image view and the message field do not need any positioning. That will be managed in the table view method, for we need to know the length of the message to calculate its frame. So the final implementation of the constructor is the following.

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
	if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
		senderAndTimeLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 5, 300, 20)];
		senderAndTimeLabel.textAlignment = UITextAlignmentCenter;
		senderAndTimeLabel.font = [UIFont systemFontOfSize:11.0];
		senderAndTimeLabel.textColor = [UIColor lightGrayColor];
		[self.contentView addSubview:senderAndTimeLabel];
		bgImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
		[self.contentView addSubview:bgImageView];
		messageContentView = [[UITextView alloc] init];
		messageContentView.backgroundColor = [UIColor clearColor];
		messageContentView.editable = NO;
		messageContentView.scrollEnabled = NO;
		[messageContentView sizeToFit];
		[self.contentView addSubview:messageContentView];
    }
    return self;
}

Now let’s rewrite the -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath method using the new custom cell we have built. First, we need to substitute the old cell class with the new one.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
	NSDictionary *s = (NSDictionary *) [messages objectAtIndex:indexPath.row];
	static NSString *CellIdentifier = @"MessageCellIdentifier";
	SMMessageViewTableCell *cell = (SMMessageViewTableCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
	if (cell == nil) {
		cell = [[[SMMessageViewTableCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
	}
}

Since it makes no sense to assign geometrical dimensions in the constructor we start with zero. Here is a crucial step. We need to calculate the size of the text according to the length of the string sent or received. Fortunately the SDK provides a handy method called sizeWithFont:constrainedToSize:lineBreakMode: which calculates the height and width of a string as rendered according to the constraints we pass as parameter. Our only constraint is the width of the device which is 320 logical pixels in width. Since we want some padding we set the constraint to 260, whereas the height is not a problem, so we can set a much higher number.

	CGSize  textSize = { 260.0, 10000.0 };
	CGSize size = [message sizeWithFont:[UIFont boldSystemFontOfSize:13]
					  constrainedToSize:textSize
						  lineBreakMode:UILineBreakModeWordWrap];

Now, size is a parameter that we will use to draw both the messageContentView and the balloon view. We want messages sent to appear left-aligned, and messages received to appear right-aligned. So the position of messageContentView changes according to the sender of the message, as follows:

	NSString *sender = [s objectForKey:@"sender"];
	NSString *message = [s objectForKey:@"msg"];
	NSString *time = [s objectForKey:@"time"];
	CGSize  textSize = { 260.0, 10000.0 };
	CGSize size = [message sizeWithFont:[UIFont boldSystemFontOfSize:13]
					  constrainedToSize:textSize
						  lineBreakMode:UILineBreakModeWordWrap];
	cell.messageContentView.text = message;
	cell.accessoryType = UITableViewCellAccessoryNone;
	cell.userInteractionEnabled = NO;
	if ([sender isEqualToString:@"you"]) { // sent messages
		[cell.messageContentView setFrame:CGRectMake(padding, padding*2, size.width, size.height)];
	} else {
         [cell.messageContentView setFrame:CGRectMake(320 - size.width - padding,
												        padding*2,
													 size.width,
													 size.height)];
  }
	...

Now we have to display the balloon image as a wrapper for the message view. First, we need to get graphical assets. You can build your own or use the following ones.

Balloon image for received message
Balloon image for sent message

The first, with the “arrow” on the left will be used for sent messages, and the other for received ones. You might wonder why the assets are so small. We won’t need big images to be adapted in size, but we will stretch those assets to adapt to the frame of the message view. The stretching will spread only the central part of the assets, which is made of a solid color, so there won’t be any unwanted deformation effect. To achieve that we use a handy method [[UIImage imageNamed:@"orange.png"] stretchableImageWithLeftCapWidth:24 topCapHeight:15];. The parameters represent the limit (from borders) where the stretching can start. Now our image is ready to be positioned.

The final implementation is the following:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
	NSDictionary *s = (NSDictionary *) [messages objectAtIndex:indexPath.row];
	static NSString *CellIdentifier = @"MessageCellIdentifier";
	SMMessageViewTableCell *cell = (SMMessageViewTableCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
	if (cell == nil) {
		cell = [[[SMMessageViewTableCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease];
	}
	NSString *sender = [s objectForKey:@"sender"];
	NSString *message = [s objectForKey:@"msg"];
	NSString *time = [s objectForKey:@"time"];
	CGSize  textSize = { 260.0, 10000.0 };
	CGSize size = [message sizeWithFont:[UIFont boldSystemFontOfSize:13]
					  constrainedToSize:textSize
						  lineBreakMode:UILineBreakModeWordWrap];
	size.width += (padding/2);
	cell.messageContentView.text = message;
	cell.accessoryType = UITableViewCellAccessoryNone;
	cell.userInteractionEnabled = NO;
	UIImage *bgImage = nil;
	if ([sender isEqualToString:@"you"]) { // left aligned
		bgImage = [[UIImage imageNamed:@"orange.png"] stretchableImageWithLeftCapWidth:24  topCapHeight:15];
		[cell.messageContentView setFrame:CGRectMake(padding, padding*2, size.width, size.height)];
		[cell.bgImageView setFrame:CGRectMake( cell.messageContentView.frame.origin.x - padding/2,
											  cell.messageContentView.frame.origin.y - padding/2,
											  size.width+padding,
											  size.height+padding)];
	} else {
		bgImage = [[UIImage imageNamed:@"aqua.png"] stretchableImageWithLeftCapWidth:24  topCapHeight:15];
		[cell.messageContentView setFrame:CGRectMake(320 - size.width - padding,
													 padding*2,
													 size.width,
													 size.height)];
		[cell.bgImageView setFrame:CGRectMake(cell.messageContentView.frame.origin.x - padding/2,
											  cell.messageContentView.frame.origin.y - padding/2,
											  size.width+padding,
											  size.height+padding)];
	}
	cell.bgImageView.image = bgImage;
	cell.senderAndTimeLabel.text = [NSString stringWithFormat:@"%@ %@", sender, time];
	return cell;
}

We should not forget that the height of the whole cell is dynamic, so we should also update the following method:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
	NSDictionary *dict = (NSDictionary *)[messages objectAtIndex:indexPath.row];
	NSString *msg = [dict objectForKey:@"msg"];
	CGSize  textSize = { 260.0, 10000.0 };
	CGSize size = [msg sizeWithFont:[UIFont boldSystemFontOfSize:13]
				  constrainedToSize:textSize
					  lineBreakMode:UILineBreakModeWordWrap];
	size.height += padding*2;
	CGFloat height = size.height < 65 ? 65 : size.height;
	return height;
}

Now we are ready to run our new implementation of custom view cells. Here is the result:

The chat custom view

Emoticons

Many chat programs like iChat, Adium, or even web-based chats like Facebook Chat, support emoticons, that is expressions made of letters and punctuation that represent an emotion like :) for happyness, :( for sadness, and so on. Our goal is to customize the the message view so that images are displayed instead of letters and punctuation. To enable this behavior we need to parse each message and substitute occurrences of emoticons with the corresponding Unicode characters. For a list of emoticons available on the iPhone you can check out this table. We can add the substitution method in the Utils category we have already used to calculate the current date. This is the implementation:

- (NSString *) substituteEmoticons {
	//See http://www.easyapns.com/iphone-emoji-alerts for a list of emoticons available
	NSString *res = [self stringByReplacingOccurrencesOfString:@":)" withString:@"\ue415"];
	res = [res stringByReplacingOccurrencesOfString:@":(" withString:@"\ue403"];
	res = [res stringByReplacingOccurrencesOfString:@";-)" withString:@"\ue405"];
	res = [res stringByReplacingOccurrencesOfString:@":-x" withString:@"\ue418"];
	return res;
}

Here we replace only three emoticons just to give you an idea of how the method works. Such a method needs to be called before storing messages in the array which populates the SMChatViewController. When we send a message:

- (IBAction)sendMessage {
    NSString *messageStr = self.messageField.text;
    if([messageStr length] > 0) {
         ...
		NSMutableDictionary *m = [[NSMutableDictionary alloc] init];
		[m setObject:[messageStr substituteEmoticons] forKey:@"msg"];
         ...
		[messages addObject:m];]
    }
	   ...
}

When we receive it:

- (void)newMessageReceived:(NSDictionary *)messageContent {
	NSString *m = [messageContent objectForKey:@"msg"];
	[messageContent setObject:[m substituteEmoticons] forKey:@"msg"];
	[messages addObject:messageContent];
  ...
}

Our Jabber Client is now complete. Here is a screenshot of the final implementation:

The final result

Ready to chat?

Source Code

The complete source code for this project can be found on GitHub here.

Series Navigation«Building a Jabber Client for iOS: XMPP Setup

Note: Want to add some source code? Type <pre><code> before it and </code></pre> after it. Find out more
  • Mike

    Thanks for the great article! I’ve implemented this successfully! :) Thanks! Thanks!

    I need some help with this though..
    I’m getting a “subscribe” presence type when my buddy adds me in his contact list. Is it possible for me to programmatically authorize all requests?

    • vikas Ojha

      Hi Mike,

      i tried installing ejabberd -2.1.8 and 2.1.9 versions om Mac but none of them gets installed and shows post install script error .I dont know i tried installation 2 months earlier at that time ejabbered 2.1.8 got installed on seperate Mac min iand i was able to run these commands also on terminal.Any suggestion would be appreciated,

  • Mike

    Oh I solved my problem. Hope this helps someone.

    In (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence

    do this:

    if ([presenceType isEqualToString:@"subscribe"])
    {
    NSXMLElement *message = [NSXMLElement elementWithName:@"presence"];
    [message addAttributeWithName:@"type" stringValue:@"subscribed"];
    [message addAttributeWithName:@"to" stringValue:[NSString stringWithFormat:@"%@@%@", presenceFromUser, @"domain"]];

    [self.xmppStream sendElement:message];
    }

  • Alan Peng

    Thanks for this tutorial though in the latest update there are still warnings and all the files in XMPP library appears to be missing that causes build errors.

    Another thing is, there appears to be a few lines of code there to demonstrate the TurnSocket connection, how do we use it to correctly implement the file transfer function or would it be in the coming part of this tutorial?

    Thanks again and am looking forward to part 5 of this tutorial. ;-)

  • rupert

    The tutorial is a very nice way to learn more about jabber, thanks!

    I am not able to compile the project though…
    - I’ve dowloaded the project from GIT
    - I’ve checked out xmppframework
    - I’ve copied xmpp into the Xcode project

    I get “Cocoa/Cocoa.h” not found
    I’ve replaced all the
    #import
    with
    #import

    but I still get things like
    “use of undeclared identifier NSPoint”

    Any idea?
    Thanks!

  • rupert

    Managed to build.
    Had to delete “Xcode” folder from xmpp.

    The iPhone app only RECEIVES, it does not send.

    In the log it says:

    Attempting TURN connection to cesare@jerry.local
    TURN Connection failed!

  • Mayur

    hey…Nice tutorial… But I have a doubt. Its actually kind of extension. Instead of the simple one, can we have the video chat functionality as well?? Can we implement this using LibJingle or something else??? For iOS, that is……

  • Agreensh

    Hi,

    Very nice tutorial, got it working nicely.

    How could this be changed to chat via Facebook ? I know that uses Xmpp, but don’t know what the server would be…

    Any help appreciated.

    Thanks,
    A

    • Andy

      Very easy…

      User name is xxxxxxx@chat.facebook.com where xxxxxxx is the Facebook login.
      Now have a very nice FB chat app – needed to store messages history though.

      • comonitos

        what is facebook login format? your email or id? a am able to autentificate only with typed in password and existing facebook nickname (that creating instead of id number)
        [xmppStream authenticateWithFacebookAccessToken:at error:&error]
        not working. can you help me?

  • http://mobile.tutsplus.com/tutorials/iphone/building-a-jabber-client-for-ios-custom-chat-view-and-emoticons/ vishal

    this was an awesome tutorial …!!
    :)
    how should i transfer a file using this .. u have used xep-65.. but u have turn connection failed in logs..
    how to over come this problem…

    thanks in advance

    • crispin

      Hi,
      In face book chat i got the negative user id’s like this
      -1479115148@chat.facebook.com. Please help me out in this problem….

      Thanks in advance

  • ArunKumara O D

    Hi,
    Can any one help me i am facing one issue with this sample code. if i enterd the text in the text field for chat once i send i have missed some words in the chat bubble. please help me regarding this. thanks in advance.

    • Andreas

      I solved this problem by doing this:

      CGSize size = [message sizeWithFont:[UIFont systemFontOfSize:13] constrainedToSize:textSize lineBreakMode:UILineBreakModeWordWrap];
      size = CGSizeMake(size.width+1, size.height);

      in cellForRowAtIndexPath.

      Seems like for some words it calculates the size 1px too small, so by increasing it with 1 the word gets shown. I hope this works for you as well.

  • http://quickblox.com PHWizard

    also – to make your life easier – here you can download a location integrated chat sample code:
    http://quickblox.com/supersample-ios/

    there are XMPP and own server solutions provided along with a free backend. API reference:
    http://wiki.quickblox.com/Chat

  • Paresh Vaghela

    I have tried all ways, can anybody give me sample code for file transfer using jabber client in iPhone ?

  • Jack

    This is a nice tutorial … and i am learnt lots of thing from this.

    I am new to iphone.

    I have configured whole application and its working fine for me.but i am able to login with the user that i have created with my localhost server.

    is this possible that i can use my facebook account and chat with the facebook users?

    And How can i add new user from my application to my localhost server so he also can login through my application…

    Thanks in advance..

  • dovi

    thanks allot for this tutorial!

  • rcsl

    Wow … man, wonderful tutorial – Thank you!

  • Richyaxe

    Just looking for this…Thanks for such a wonderfult tutorial with GREAT explanation & step by step instructions… Its makes thing very easy to understand. You are a genious…

  • Crispin

    Hi, In face book chat i got the negative user id’s like this -1479115148@chat.facebook.com. Please help me out in this problem….

    Thanks in advance

  • baachasa

    Hi, First of let me say thanks for this great tutorial. As I was desperately searching for a chat feature for my very first iphone app, someone recommended your code. After investing days of work, I succeeded installing ejabberd on my mac (tested as you recommended) and then jumped at once into the implementation using major part of your code for the chat functionality. It all looked fine. My problem is, I really do not know how the onlineBuddies array is populated. I have two registered ejabberd users and my desire is to see that these users connect and chat. When testing, I normally use my app to login as one of the users and I can send messages successfully but I simply cannot see the other user to test my app. I will appreciate it very much if you can give me tips on how I can go about fixing this,. Thanks in advance.

    Best regards and thanks a lot in advance.

    -kisimi