Construire un client Jabber pour iOS affichage de chat et émoticônes personnalisés

Dans cette partie de la série, nous allons créer une vue personnalisée pour que les messages de chat aient une apparence plus professionnelle. De plus, nous allons également ajouter de vrais émoticônes à afficher à la place de leurs contreparties textuelles..

Petite correction de bug

Avant de continuer, nous avons remarqué un petit bug introduit dans la troisième partie de la série. Lorsque nous recevons une notification indiquant qu'un nouveau contact est en ligne, nous l'ajoutons à la liste des contacts en ligne et actualisons la vue..

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

Cela pourrait fonctionner si nous recevions une notification en ligne une seule fois. En réalité, une telle notification est envoyée périodiquement. Cela peut être dû à la nature du protocole XMPP ou à la mise en œuvre ejabbered que nous utilisons. Dans tous les cas, pour éviter les doublons, nous devons vérifier si nous avons déjà ajouté au tableau le copain transporté dans la notification. Donc, nous refactorions comme ceci:

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

Et le bug est corrigé.

Création de messages de discussion personnalisés

Au cours de la série, nous avons construit un contrôleur d'affichage de discussion qui affiche les messages à l'aide de composants visuels standard inclus dans le SDK iOS. Notre objectif est de créer quelque chose de plus joli, qui affiche l'expéditeur et l'heure du message. Nous nous inspirons de l’application SMS fournie dans iOS, qui affiche le contenu du message entouré d’une bulle. Le résultat que nous souhaitons atteindre est illustré dans la figure suivante:

Les composants de l'entrée sont en haut, comme dans l'implémentation actuelle. Nous devons créer une vue personnalisée pour les cellules de la table. Voici la liste des exigences:

  • Chaque cellule indique l'expéditeur et l'heure du message au moyen d'une étiquette en haut.
  • Chaque message est entouré d'une image ballon avec un rembourrage
  • Les images de fond du message sont différentes selon l'expéditeur
  • La hauteur du message (et son image d'arrière-plan) peut varier en fonction de la longueur du texte.

Enregistrement de l'horodatage d'un message

L'implémentation en cours n'enregistre pas l'heure à laquelle un message a été envoyé / reçu. Comme nous devons effectuer cette opération à plusieurs endroits, nous créons une méthode utilitaire qui renvoie la date et l'heure actuelles sous la forme d'une chaîne. Nous le faisons au moyen d’une catégorie, en élargissant le NSString classe.
Suivant la convention suggérée par Apple, nous créons deux fichiers sources nommés NSString + Utils.h et NSString + Utils.m. Le fichier d'en-tête contient le code suivant:

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

Dans l'implémentation, nous définissons la méthode statique getCurrentTime comme suit

 @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];  @fin

Une telle méthode retournera des chaînes comme suit: 12 septembre 2011 19:34:21

Si vous souhaitez personnaliser le format de la date, vous pouvez consulter la documentation de NSFormatter.
Maintenant que la méthode de l'utilitaire est prête, nous devons enregistrer la date et l'heure des messages envoyés et reçus. Les deux modifications concernent le SMChatViewController lorsque nous envoyons un message:

 - (IBAction) sendMessage NSString * messageStr = self.messageField.text; si ([longueur de MessageStr]> 0) ? NSMutableDictionary * m = [[NSMutableDictionary alloc] init]; [m setObject: @ "you" forKey: @ "sender"]; [m setObject: [NSString getCurrentTime] forKey: @ "time"] ;? ? 

Et quand on le reçoit:

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

Maintenant que nous avons toutes les structures de données nécessaires pour construire notre interface personnalisée, commençons par personnaliser notre vue de cellule.

Le ballon vue

La plupart des modifications que nous allons introduire sont liées au SMChatViewController, et en particulier à la méthode. -(UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath, qui est où le contenu de chaque cellule est dessiné.
L'implémentation actuelle utilise un UITableViewCell générique, mais cela ne suffit pas pour nos besoins, nous devons donc le sous-classer. Nous appelons notre nouvelle classe SMMessageViewTableCell.

La classe a besoin de trois éléments visuels:

  • Une étiquette pour montrer la date et l'heure
  • Une vue textuelle pour montrer le message
  • Une vue d'image pour afficher une vue personnalisée en forme de ballon

Voici le fichier d'interface correspondant:

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

La première étape de la mise en œuvre consiste à synthétiser les propriétés et à définir le désallocation des instances..

 @implementation SMMessageViewTableCell @synthesize senderAndTimeLabel, messageContentView, bgImageView; - (void) dealloc [version de senderAndTimeLabel]; [messageContentView release]; [version bgImageView]; [super dealloc];  @fin

Ensuite, nous pouvons remplacer le constructeur pour ajouter les éléments visuels au contentView de la cellule. le senderAndTimeLabel est le seul élément avec une position fixe afin que nous puissions définir son cadre et son apparence directement dans le constructeur.

 - (id) initWithStyle: (UITableViewCellStyle) style reuseIdentifier: (NSString *) reuseIdentifier if (self = [super initWithStyle: style reuseIdentifier: reuseIdentifier]) senderAndTimeLabel = [Uilabel alloc:: )]; senderAndTimeLabel.textAlignment = UITextAlignmentCenter; senderAndTimeLabel.font = [UIFont systemFontOfSize: 11.0]; senderAndTimeLabel.textColor = [UIColor lightGrayColor]; [self.contentView addSubview: senderAndTimeLabel];  retourner soi-même; 

La vue de l'image et le champ de message ne nécessitent aucun positionnement. Cela sera géré dans la méthode de la vue tableau, car nous avons besoin de connaître la longueur du message pour calculer son cadre. Donc, l'implémentation finale du constructeur est la suivante.

 - (id) initWithStyle: (UITableViewCellStyle) style reuseIdentifier: (NSString *) reuseIdentifier if (self = [super initWithStyle: style reuseIdentifier: reuseIdentifier]) senderAndTimeLabel = [Uilabel alloc:: )]; 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 all] init]; messageContentView.backgroundColor = [UIColor clearColor]; messageContentView.editable = NO; messageContentView.scrollEnabled = NO; [messageContentView sizeToFit]; [self.contentView addSubview: messageContentView];  retourner soi-même; 

Maintenant, réécrivons le -(UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath méthode utilisant la nouvelle cellule personnalisée que nous avons construite. Premièrement, nous devons remplacer l'ancienne classe de cellules par la nouvelle.

 - (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]; 

Comme il est inutile d’attribuer des cotes géométriques au constructeur, nous commençons par zéro. Voici une étape cruciale. Nous devons calculer la taille du texte en fonction de la longueur de la chaîne envoyée ou reçue. Heureusement, le SDK fournit une méthode pratique appelée sizeWithFont: constrainedToSize: lineBreakMode: qui calcule la hauteur et la largeur d'une chaîne comme rendu en fonction des contraintes passées en paramètre. Notre seule contrainte est la largeur du périphérique, qui a une largeur de 320 pixels logiques. Puisque nous voulons du rembourrage, nous fixons la contrainte à 260, alors que la hauteur ne pose pas de problème, nous pouvons donc définir un nombre beaucoup plus élevé..

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

Maintenant, la taille est un paramètre que nous allons utiliser pour dessiner les deux messageContentView et la vue ballon. Nous voulons que les messages envoyés apparaissent alignés à gauche et que les messages reçus apparaissent alignés à droite. Donc, la position de messageContentView change en fonction de l'expéditeur du message, comme suit:

 NSString * sender = [s objectForKey: @ "sender"]; NSString * message = [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"]) // messages envoyés [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)]; ? 

Nous devons maintenant afficher l'image en ballon comme enveloppe pour la vue du message. Premièrement, nous devons obtenir des ressources graphiques. Vous pouvez construire le vôtre ou utiliser les suivants.

Le premier, avec la "flèche" à gauche, sera utilisé pour les messages envoyés, et le second pour les messages reçus. Vous pourriez vous demander pourquoi les actifs sont si petits. Nous n’aurons pas besoin d’adapter la taille des grandes images, mais nous allons étendre ces ressources pour s’adapter au cadre de la vue du message. L'étirement n'épandra que la partie centrale des actifs, qui est composée d'une couleur unie, évitant ainsi tout effet de déformation indésirable. Pour y parvenir, nous utilisons une méthode pratique [[UIImage imageNamed: @ "orange.png"] stretchableImageWithLeftCapWidth: 24 topCapHeight: 15];. Les paramètres représentent la limite (à partir des frontières) où l'étirement peut commencer. Maintenant notre image est prête à être positionnée.

La mise en œuvre finale est la suivante:

 - (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 = [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 ([expéditeur estEqualToString: @ "vous"]) // aligné à gauche 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: @ "% @% @", expéditeur, heure]; cellule de retour; 

Nous ne devons pas oublier que la hauteur de la cellule entière est dynamique, nous devons donc également mettre à jour la méthode suivante:

 - (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; 

Nous sommes maintenant prêts à exécuter notre nouvelle implémentation de cellules de vue personnalisées. Voici le résultat:

Émoticônes

De nombreux programmes de discussion comme iChat, Adium ou même des discussions sur le Web telles que Facebook Chat soutiennent les émoticônes, expressions constituées de lettres et de signes de ponctuation qui représentent une émotion comme :) pour le bonheur, :( pour la tristesse, etc. Notre objectif consiste à personnaliser la vue des messages afin que les images soient affichées à la place des lettres et des signes de ponctuation. Pour activer ce comportement, nous devons analyser chaque message et substituer les occurrences d’émoticônes aux caractères Unicode correspondants. Pour une liste des émoticônes disponibles sur iPhone, vous pouvez Consultez ce tableau. Nous pouvons ajouter la méthode de substitution à la catégorie Utils que nous avons déjà utilisée pour calculer la date actuelle.

 - (NSString *) substituteEmoticons // Voir http://www.easyapns.com/iphone-emoji-alerts pour la liste des émoticônes disponibles NSString * res = [self stringByReplacingOntrusionsOfString: @ ":)" withString: @ "\ ue415" ]; res = [res stringByReplacingOccurrencesOfString: @ ":(" withString: @ "\ ue403"]; res = [res stringByReplacingOccurrencesOfString: @ ";-)" withString: @ "\ ue405"]; res = [res stringByReplacingOcurrencesOfString: @ ": - x" withString: @ "\ ue418"]; retourne res; 

Ici, nous ne remplaçons que trois émoticônes simplement pour vous donner une idée du fonctionnement de la méthode. Une telle méthode doit être appelée avant de stocker les messages dans le tableau qui remplit la SMChatViewController. Lorsque nous envoyons un message:

 - (IBAction) sendMessage NSString * messageStr = self.messageField.text; si ([longueur de MessageStr]> 0) ? NSMutableDictionary * m = [[NSMutableDictionary alloc] init]; [m setObject: [messageStr substituteEmoticons] forKey: @ "msg"] ;? [messages addObject: m];]? 

Quand on le reçoit:

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

Notre client Jabber est maintenant complet. Voici une capture d'écran de la mise en œuvre finale:

Prêt à discuter?

Code source

Le code source complet de ce projet est disponible sur GitHub ici.