Статьи

Создание клиента Jabber для iOS: настраиваемый вид чата и смайлики

В этой части серии мы создадим собственный вид, чтобы сообщения чата выглядели более профессионально. Кроме того, мы также добавим настоящие смайлики для отображения вместо их текстовых аналогов.

Прежде чем продолжить, мы заметили небольшую ошибку, появившуюся в третьей части серии. Когда мы получаем уведомление о том, что новый собеседник находится в сети, мы добавляем его в массив онлайн-пользователей и обновляем представление.

1
2
3
4
5
6
— (void)newBuddyOnline:(NSString *)buddyName
{
    [onlineBuddies addObject:buddyName];
    [self.tView reloadData];
     
}

Это может сработать, если мы получим онлайн-уведомление только один раз. На самом деле такое уведомление отправляется периодически. Это может быть связано с природой протокола XMPP или ejabbered-реализацией, которую мы используем. В любом случае, чтобы избежать дубликатов, мы должны проверить, добавили ли мы уже к массиву собеседника в уведомлении. Итак, мы осуществляем рефакторинг следующим образом:

1
2
3
4
5
6
— (void)newBuddyOnline:(NSString *)buddyName {
    if (![onlineBuddies containsObject:buddyName]) {
        [onlineBuddies addObject:buddyName];
        [self.tView reloadData];
    }
}

И ошибка исправлена.

В течение серии мы создали контроллер представления чата, который отображает сообщения с использованием стандартных визуальных компонентов, включенных в iOS SDK. Наша цель — сделать что-то более красивое, что отображает отправителя и время сообщения. Мы черпаем вдохновение из SMS-приложения, поставляемого в комплекте с iOS, которое отображает содержимое сообщения, обернутого воздушным шариком. Результат, которого мы хотим достичь, показан на следующем рисунке:

Ожидаемый результат

Компоненты для ввода находятся сверху, как в текущей реализации. Нам нужно создать собственное представление для ячеек таблицы. Это список требований:

  • Каждая ячейка показывает отправителя и время сообщения с помощью метки вверху
  • Каждое сообщение оборачивается изображением воздушного шара с некоторыми отступами
  • Фоновые изображения для сообщения различаются в зависимости от отправителя
  • Высота сообщения (и его фоновое изображение) может варьироваться в зависимости от длины текста

Текущая реализация не экономит время, когда сообщение было отправлено / получено. Поскольку мы должны выполнить эту операцию в нескольких местах, мы создаем служебный метод, который возвращает текущую дату и время в виде строки. Мы делаем это с помощью категории , расширяющей класс NSString .
Следуя соглашению, предложенному Apple, мы создаем два исходных файла с именами NSString+Utils.h и NSString+Utils.m . Заголовочный файл содержит следующий код:

1
2
3
4
5
@interface NSString (Utils)
 
+ (NSString *) getCurrentTime;
 
@end

В реализации мы определяем статический метод getCurrentTime следующим образом

01
02
03
04
05
06
07
08
09
10
11
12
@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

Такой метод будет возвращать строки, подобные следующим: Sep 12, 2011 7:34:21 PM

Если вы хотите настроить формат даты, вы можете обратиться к документации NSFormatter .
Теперь, когда у нас есть готовый служебный метод, нам нужно сохранить дату и время отправленных и полученных сообщений. Обе модификации относятся к SMChatViewController, когда мы отправляем сообщение:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
— (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»];
         
         …
         
    }
     
    …
}

И когда мы получим это:

1
2
3
4
5
6
7
8
9
— (void)newMessageReceived:(NSDictionary *)messageContent {
     
    NSString *m = [messageContent objectForKey:@»msg»];
  …
    [messageContent setObject:[NSString getCurrentTime] forKey:@»time»];
 
  …
 
}

Теперь у нас есть все структуры данных, которые нам нужны для создания нашего пользовательского интерфейса, поэтому давайте начнем с настройки нашего представления ячеек.

Большинство модификаций, которые мы собираемся представить, связаны с SMChatViewController и, в частности, с методом -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath , в котором -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath содержимое каждой ячейки ,
Текущая реализация использует универсальный UITableViewCell, но этого недостаточно для наших требований, поэтому нам нужно создать его подкласс. Мы называем наш новый класс SMMessageViewTableCell .

Классу нужны три визуальных элемента:

  • Метка для отображения даты и времени
  • Текстовое представление для отображения сообщения
  • Представление изображения для отображения пользовательского вида в форме шара

Вот соответствующий файл интерфейса:

01
02
03
04
05
06
07
08
09
10
11
@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

Первым шагом реализации является синтез свойств и настройка освобождения экземпляров.

01
02
03
04
05
06
07
08
09
10
11
12
@implementation SMMessageViewTableCell
 
@synthesize senderAndTimeLabel, messageContentView, bgImageView;
 
— (void)dealloc {
    [senderAndTimeLabel release];
    [messageContentView release];
    [bgImageView release];
    [super dealloc];
}
 
@end

Затем мы можем переопределить конструктор, чтобы добавить визуальные элементы в contentView ячейки. senderAndTimeLabel — единственный элемент с фиксированной позицией, поэтому мы можем установить его рамку и внешний вид прямо в конструкторе.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
— (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;
 
}

Вид изображения и поле сообщения не нуждаются в позиционировании. Это будет управляться в методе табличного представления, поскольку нам нужно знать длину сообщения для вычисления его кадра. Итак, окончательная реализация конструктора следующая.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
— (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;
     
}

Теперь давайте перепишем -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath метод -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath с использованием новой пользовательской ячейки, которую мы создали. Во-первых, нам нужно заменить старый класс ячеек новым.

01
02
03
04
05
06
07
08
09
10
11
12
— (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];
    }
 
}

Поскольку нет смысла назначать геометрические размеры в конструкторе, мы начинаем с нуля. Вот решающий шаг. Нам нужно рассчитать размер текста в соответствии с длиной отправленной или полученной строки. К счастью, в SDK предусмотрен удобный метод sizeWithFont:constrainedToSize:lineBreakMode: который вычисляет высоту и ширину строки в том виде, в котором она отображается в соответствии с ограничениями, которые мы передаем в качестве параметра. Нашим единственным ограничением является ширина устройства, которая составляет 320 логических пикселей в ширину. Так как мы хотим добавить некоторые отступы, мы установили ограничение на 260, тогда как высота не является проблемой, поэтому мы можем установить намного большее число.

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

Теперь размер — это параметр, который мы будем использовать для рисования как messageContentView и messageContentView . Мы хотим, чтобы отправленные сообщения выглядели выровненными по левому краю, а полученные сообщения выровненными по правому краю. Таким образом, позиция messageContentView изменяется в соответствии с отправителем сообщения следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  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)];
 
}
 
  …

Теперь нам нужно отобразить изображение всплывающей подсказки в качестве оболочки для просмотра сообщения. Во-первых, нам нужно получить графические активы. Вы можете создать свой собственный или использовать следующие.

Изображение воздушного шара для полученного сообщения
Изображение шара для отправленного сообщения

Первый со стрелкой слева будет использоваться для отправленных сообщений, а второй для полученных. Вы можете удивиться, почему активы такие маленькие. Нам не понадобятся большие изображения для адаптации по размеру, но мы растянем эти ресурсы, чтобы адаптировать их к рамке представления сообщения. Растяжение распространит только центральную часть активов, которая сделана из сплошного цвета, поэтому не будет никакого нежелательного эффекта деформации. Для этого мы используем удобный метод [[UIImage imageNamed:@"orange.png"] stretchableImageWithLeftCapWidth:24 topCapHeight:15]; , Параметры представляют предел (от границ), где может начаться растяжение. Теперь наш образ готов к размещению.

Окончательная реализация заключается в следующем:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
— (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;
     
}

Мы не должны забывать, что высота всей ячейки является динамической, поэтому мы должны также обновить следующий метод:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
— (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 ?
    return height;
     
}

Теперь мы готовы запустить нашу новую реализацию пользовательских ячеек представления. Вот результат:

Пользовательский вид чата

Многие чат-программы, такие как iChat, Adium или даже веб-чаты, такие как Facebook Chat, поддерживают смайлики , то есть выражения, состоящие из букв и знаков препинания, которые представляют собой эмоции типа 🙂 для счастья, 🙁 для грусти и т. Д. Наша цель настроить представление сообщения таким образом, чтобы вместо букв и знаков пунктуации отображались изображения. Чтобы включить это поведение, нам нужно проанализировать каждое сообщение и заменить вхождения смайликов соответствующими символами Unicode. Список смайликов, доступных на iPhone, можно посмотрите эту таблицу . Мы можем добавить метод подстановки в категорию Utils, которую мы уже использовали для вычисления текущей даты. Это реализация:

01
02
03
04
05
06
07
08
09
10
11
12
— (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;
     
}

Здесь мы заменим только три смайлика, чтобы дать вам представление о том, как работает метод. Такой метод необходимо вызывать перед сохранением сообщений в массиве, который заполняет SMChatViewController . Когда мы отправляем сообщение:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
— (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];]
         
    }
       …
}

Когда мы получим это:

01
02
03
04
05
06
07
08
09
10
— (void)newMessageReceived:(NSDictionary *)messageContent {
     
    NSString *m = [messageContent objectForKey:@»msg»];
     
    [messageContent setObject:[m substituteEmoticons] forKey:@»msg»];
 
    [messages addObject:messageContent];
   
  …
}

Наш клиент Jabber завершен. Вот скриншот окончательной реализации:

Конечный результат

Готов общаться?

Полный исходный код этого проекта можно найти на GitHub здесь .