Construirea unui client Jabber pentru iOS vizualizare personalizată a chat-ului și emoticoane

În această parte a seriei, vom construi o vizualizare particularizată pentru a face ca mesajele de chat să pară mai profesionale. Mai mult decât atât, vom adăuga emoticoane reale pentru a fi afișate în locul omologilor lor textuali.

Fixarea bug-urilor mici

Înainte de a continua, am observat un mic bug introdus în partea a 3 a seriei. Când primim o notificare că un prieten nou este online, îl adăugăm la o gamă de persoane online și reîmprospătează vederea.

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

Acest lucru ar putea funcționa dacă am primi o notificare online doar o singură dată. În realitate, o astfel de notificare este trimisă periodic. Acest lucru se poate datora naturii protocolului XMPP sau implementării ejabbere pe care o folosim. În orice caz, pentru a evita duplicatele, ar trebui să verificăm dacă am adăugat deja la matricea pe care am avut-o în notificare. Deci, noi refactor ca aceasta:

 - (void) newBuddyOnline: (NSString *) buddyName dacă (! [onlineBuddies conțineObject: buddyName]) [onlineBuddies addObject: buddyName]; [self.tView reloadData]; 

Și bug-ul este fix.

Crearea de mesaje personalizate de chat

În timpul seriei am construit un controler de vizualizare a discuțiilor care afișează mesaje utilizând componente vizuale standard incluse în SDK-ul iOS. Scopul nostru este să construim ceva mai frumos, care să afișeze expeditorul și timpul unui mesaj. Ne inspirăm din aplicația SMS inclusă în iOS, care afișează conținutul mesajului înfășurat de un balon cum ar fi balonul. Rezultatul pe care dorim să-l realizăm este prezentat în figura următoare:

Componentele pentru intrare sunt pe partea de sus, ca și în implementarea actuală. Trebuie să creăm o vizualizare personalizată pentru celulele din tabel. Aceasta este lista cerințelor:

  • Fiecare celulă afișează expeditorul și ora mesajului prin intermediul unei etichete din partea de sus
  • Fiecare mesaj este înfășurat printr-o imagine cu balon, cu unele materiale de umplutură
  • Imaginile de fundal pentru mesaj diferă în funcție de expeditor
  • Înălțimea mesajului (și imaginea de fundal) poate varia în funcție de lungimea textului

Salvarea amprentei de timp a unui mesaj

Actuala implementare nu salvează timpul în care un mesaj a fost trimis sau primit. Deoarece trebuie să efectuăm această operațiune în mai mult de un loc, vom crea o metodă utilitară care returnează data și ora curente sub forma unui șir. O facem prin intermediul unei categorii, extinzându-ne NSString clasă.
Urmând convenția sugerată de Apple, vom crea două fișiere sursă numite NSString + Utils.h și NSString + Utils.m. Fișierul antet conține următorul cod:

 @ interfață NSString (Utils) + (NSString *) getCurrentTime; @Sfârșit

În implementare, definim metoda statică getCurrentTime după cum urmează

 @implementare NSString (Utils) + (NSString *) getCurrentTime NSDate * nowUTC = [Data NSDate]; NSDateFormatter * dateFormatter = [[NSDateFormatter alocare] init]; [dateFormatter setTimeZone: [NSTimeZone localTimeZone]]; [dateFormatter setDateStyle: NSDateFormatterMediumStyle]; [dateFormatter setTimeStyle: NSDateFormatterMediumStyle]; retur [dateFormatter stringFromDate: nowUTC];  @Sfârșit

O astfel de metoda va returna siruri de caractere cum ar fi: Sep 12, 2011 7:34:21 PM

Dacă doriți să personalizați formatul datei, puteți consulta documentația NSFormatter.
Acum, că avem pregătit metoda de utilitate, trebuie să salvăm data și ora mesajelor trimise și recepționate. Ambele modificări se referă la SMChatViewController când trimitem un mesaj:

 - (IBAction) sendMessage NSString * messageStr = auto.messageField.text; dacă ([lungimea mesajuluiStr]> 0) ? NSMutableDictionary * m = [[NSMutableDictionary alloc] init]; [m setObject: @ "tu" pentruKey: @ "expeditor"]; [m setObject: [NSString getCurrentTime] pentruKey: @ "timp"] ;? ? 

Și când o primim:

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

Acum avem toate structurile de date de care avem nevoie pentru a construi o interfață personalizată, așa că haideți să începem prin personalizarea vizualizării noastre celulare.

Vizualizarea balonului

Cele mai multe dintre modificările pe care le vom introduce sunt legate de SMChatViewController, și mai ales de metodă -(UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath, care este în cazul în care conținutul fiecărei celule este desenată.
Actuala implementare utilizează un UITableViewCell generic, dar acest lucru nu este suficient pentru cerințele noastre, deci trebuie să-l subclasăm. Noi numim noua noastră clasă SMMessageViewTableCell.

Clasa are nevoie de trei elemente vizuale:

  • O etichetă pentru a afișa data și ora
  • O vizualizare textuală pentru afișarea mesajului
  • O imagine pentru a afișa o vizualizare personalizată în formă de balon

Aici este fișierul de interfață corespunzător:

 @interface SMMessageViewTableCell: UITableViewCell UILabel * senderAndTimeLabel; UITextView * messageContentView; UIImageView * bgImageView;  @property (nonatomic, atribuire) UILabel * senderAndTimeLabel; @property (nonatomic, atribuire) UITextView * messageContentView; @property (nonatomic, atribuire) UIImageView * bgImageView; @Sfârșit

Primul pas al implementării este de a sintetiza proprietățile și de a configura dealocarea instanțelor.

 @implementation SMMessageViewTableCell @synthesize senderAndTimeLabel, messageContentView, bgImageView; - (void) dealloc [senderAndTimeLabel release]; [releaseContactView release]; [release bgImageView]; [super dealloc];  @Sfârșit

Apoi putem suprascrie constructorul pentru a adăuga elementele vizuale la conținutulView al celulei. senderAndTimeLabel este singurul element cu o poziție fixă, astfel încât să putem stabili cadrul și aspectul său direct în constructor.

 - (id = initWithStyle: style reuseIdentifier: reuseIdentifier)) senderAndTimeLabel = [[UILabel alloc] initWithFrame: CGRectMake (10, 5, 300, 20 )]; senderAndTimeLabel.textAlignment = UITextAlignmentCenter; senderAndTimeLabel.font = [UIFont sistemFontOfSize: 11.0]; senderAndTimeLabel.textColor = [UICcolor lightGrayColor]; [self.contentView addSubview: senderAndTimeLabel];  întoarce-te; 

Imaginea de vizualizare și câmpul mesajului nu necesită nicio poziționare. Acest lucru va fi gestionat în metoda de vizualizare a tabelului, deoarece trebuie să cunoaștem lungimea mesajului pentru a calcula cadrul acestuia. Deci, implementarea finală a constructorului este următoarea.

 - (id = initWithStyle: style reuseIdentifier: reuseIdentifier)) senderAndTimeLabel = [[UILabel alloc] initWithFrame: CGRectMake (10, 5, 300, 20 )]; senderAndTimeLabel.textAlignment = UITextAlignmentCenter; senderAndTimeLabel.font = [UIFont sistemFontOfSize: 11.0]; senderAndTimeLabel.textColor = [UICcolor lightGrayColor]; [self.contentView addSubview: senderAndTimeLabel]; bgImageView = [[UIImageView aloca] initWithFrame: CGRectZero]; [self.contentView addSubview: bgImageView]; messageContentView = [[UITextView aloca] init]; messageContentView.backgroundColor = [UICcolor clearColor]; messageContentView.editable = NU; messageContentView.scrollEnabled = NU; [messageContentView sizeToFit]; [self.contentView addSubview: messageContentView];  întoarce-te; 

Acum să rescrieți -(UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath folosind noua celulă personalizată pe care am construit-o. În primul rând, trebuie să înlocuim clasa celulară veche cu cea nouă.

 - (UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath NSDictionary * s = (NSDictionary *) [mesaje objectAtIndex: indexPath.row]; statică NSString * CellIdentifier = @ "MessageCellIdentifier"; SMMessageViewTableCell * celula = (SMMessageViewTableCell *) [tableView dequeueReusableCellWithIdentifier: CellIdentifier]; dacă (celula == zero) cell = [[[SMMessageViewTableCell alin] initWithFrame: CGRectZero reuseIdentifier: CellIdentifier] autorelease]; 

Deoarece nu are sens să atribuim dimensiuni geometrice în constructor vom începe cu zero. Iată un pas crucial. Trebuie să calculam mărimea textului în funcție de lungimea șirului trimis sau primit. Din fericire, SDK oferă o metodă utilă numită sizeWithFont: constrainedToSize: lineBreakMode: care calculează înălțimea și lățimea unui șir așa cum este redat în funcție de constrângerile pe care le parcurgem ca parametru. Singura noastră constrângere este lățimea dispozitivului, care are 320 de lățime logică logică. Din moment ce vrem o umplutură, stabilim constrângerea la 260, în timp ce înălțimea nu este o problemă, deci putem seta un număr mult mai mare.

 CGSize textSize = 260.0, 10000.0; Dimensiunea CGSize = [dimensiunea mesajuluiWithFont: [UIFont boldSystemFontOfSize: 13] constrainedToSize: textSize lineBreakMode: UILineBreakModeWordWrap];

Acum dimensiunea este un parametru pe care îl vom folosi pentru a desena ambele messageContentView și vizualizarea balonului. Vrem ca mesajele trimise să apară aliniate la stânga, iar mesajele primite să apară în partea dreaptă. Deci, poziția messageContentView modificări în funcție de expeditorul mesajului, după cum urmează:

 NSString * expeditor = [s obiectForKey: @ "expeditor"]; NSString * message = [s obiectForKey: @ "msg"]; NSString * time = [s obiectForKey: @ "timp"]; CGSize textSize = 260.0, 10000.0; Dimensiunea CGSize = [dimensiunea mesajuluiWithFont: [UIFont boldSystemFontOfSize: 13] constrainedToSize: textSize lineBreakMode: UILineBreakModeWordWrap]; cell.messageContentView.text = mesaj; cell.accessoryType = UITableViewCellAccessoryNone; cell.userInteractionEnabled = NU; dacă ([expeditorul esteEqualToString: @ "tu"]) // trimis mesaje [cell.messageContentView setFrame: CGRectMake (umplutură, umplutură * 2, size.width, size.height)];  altceva [cell.messageContentView setFrame: CGRectMake (320 - size.width - padding, padding * 2, size.width, size.height)]; ? 

Acum trebuie să afișăm imaginea balonului ca un înveliș pentru vizualizarea mesajului. În primul rând, trebuie să obținem active grafice. Puteți construi propriul sau utilizați următoarele.

Primul, cu "săgeata" din stânga, va fi folosit pentru mesajele trimise, iar celălalt pentru cele recepționate. S-ar putea să vă întrebați de ce activele sunt atât de mici. Nu vom avea nevoie de imagini mari pentru a fi adaptate în dimensiune, dar le vom întinde pentru a se adapta la cadrul afișării mesajelor. Întinderea va răspândi doar partea centrală a bunurilor, care este realizată dintr-o culoare solidă, astfel încât nu va exista nici un efect de deformare nedorit. Pentru a realiza acest lucru, folosim o metodă la îndemână [[UIImage imageNamed: @ "orange.png"] stretchableImageWithLeftCapWidth: 24 topCapHeight: 15];. Parametrii reprezintă limita (de la granițe) unde începe întinderea. Acum, imaginea noastră este gata să fie poziționată.

Implementarea finală este următoarea:

 - (UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath NSDictionary * s = (NSDictionary *) [mesaje objectAtIndex: indexPath.row]; statică NSString * CellIdentifier = @ "MessageCellIdentifier"; SMMessageViewTableCell * celula = (SMMessageViewTableCell *) [tableView dequeueReusableCellWithIdentifier: CellIdentifier]; dacă (celula == zero) cell = [[[SMMessageViewTableCell alin] initWithFrame: CGRectZero reuseIdentifier: CellIdentifier] autorelease];  NSString * expeditor = [s objectForKey: @ "expeditor"]; NSString * message = [s obiectForKey: @ "msg"]; NSString * time = [s obiectForKey: @ "timp"]; CGSize textSize = 260.0, 10000.0; Dimensiunea CGSize = [dimensiunea mesajuluiWithFont: [UIFont boldSystemFontOfSize: 13] constrainedToSize: textSize lineBreakMode: UILineBreakModeWordWrap]; size.width + = (umplutură / 2); cell.messageContentView.text = mesaj; cell.accessoryType = UITableViewCellAccessoryNone; cell.userInteractionEnabled = NU; UIImage * bgImage = zero; dacă [[expeditorul esteEqualToString: @ "tu"]) // lăsat aliniat bgImage = [[UIImage imageNamed: @ "orange.png"] stretchableImageWithLeftCapWidth: 24 topCapHeight: 15]; [cell.messageContentView setFrame: CGRectMake (umplutură, umplutură * 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)];  altfel bgImage = [[UIImage imageNamed: @ "aqua.png"] stretchableImageWithLeftCapWidth: 24 topCapHeight: 15]; [cell.messageContentView setFrame: CGRectMake (320 - size.width - umplutură, 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 șirWithFormat: @ "% @% @", expeditor, timp]; celule retur; 

Nu trebuie să uităm că înălțimea întregii celule este dinamică, așadar ar trebui să actualizăm și următoarea metodă:

 - (CGFloat) tableView: (UITableView *) tableView inaltimeForRowAtIndexPath: (NSIndexPath *) indexPath NSDictionary * dict = (NSDictionary *) [mesaje objectAtIndex: indexPath.row]; NSString * msg = [dict obiectForKey: @ "msg"]; CGSize textSize = 260.0, 10000.0; Dimensiunea CGSize = [msg sizeWithFont: [UIFont boldSystemFontOfSize: 13] constrainedToSize: textSize lineBreakMode: UILineBreakModeWordWrap]; size.height + = padding * 2; CGFloat înălțime = dimensiune < 65 ? 65 : size.height; return height; 

Acum suntem gata să executăm noua noastră implementare a celulelor vizualizate personalizate. Iată rezultatul:

emoticoane

Multe programe de chat, cum ar fi iChat, Adium sau chiar chat-uri web, cum ar fi Facebook Chat, suport emoticoane, care sunt expresii formate din litere și semne de punctuație, care reprezintă o emoție precum :) pentru fericire, pentru tristețe etc. este de a personaliza vizualizarea mesajului astfel încât imaginile să fie afișate în loc de litere și punctuație Pentru a activa acest comportament este necesar să analizăm fiecare mesaj și să înlocuim apariția emotioanelor cu caracterele corespunzătoare Unicode Pentru o listă de emoticoane disponibile pe iPhone, consultați acest tabel.Puteți adăuga metoda de substituire în categoria Utils pe care am folosit-o deja pentru a calcula data curentă.Aceasta este implementarea:

 - (NSString *) substituteEmoticonii // A se vedea http://www.easyapns.com/iphone-emoji-alerts pentru o listă de emoticoane disponibile NSString * res = [self stringByReplacingOccurrencesOfString: @ ":)" cuString: @ "\ ue415" ]; res = [res stringByReplacingOccurrencesOfString: @ ":(" cuString: @ "\ ue403"]; res = [res stringByReplacingOccurrencesOfString: @ ";-)" cuString: @ "\ ue405"]; res = [res stringByReplacingOccurrencesOfString: @ ": - x" cuString: @ "\ ue418"]; return res; 

Aici vom înlocui doar trei emoticoane doar pentru a vă da o idee despre modul în care funcționează metoda. O astfel de metodă trebuie să fie apelată înainte de stocarea mesajelor în matricea care populare SMChatViewController. Când trimitem un mesaj:

 - (IBAction) sendMessage NSString * messageStr = auto.messageField.text; dacă ([lungimea mesajuluiStr]> 0) ? NSMutableDictionary * m = [[NSMutableDictionary alloc] init]; [m setObject: [messageStr substituteEmoticons] pentruKey: @ "msg"] ;? [mesaje addObject: m];]? 

Când o primim:

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

Clientul nostru Jabber este acum complet. Iată o captură de ecran a implementării finale:

Gata de chat?

Cod sursa

Codul sursă complet pentru acest proiect poate fi găsit pe GitHub aici.

Cod