АбраКодастр
23,2
рейтинг
4 июня 2014 в 08:41

Разработка → Как собрать WhatsApp за сутки. Часть 1 из песочницы tutorial



Здравствуйте, дорогие читатели Хабрахабра!

В этой серии статей я расскажу, как быстро и почти безболезненно поднять свой собственный WhatsApp под iOS. Статью делю на две части для вашего удобства:

  1. Создание проекта, простой UI, привязка к сервису мгновенных сообщений
  2. Делаем красивый UI, добавляем видео и аудио звонки, передачу фото и документов

К сожалению, пособие о том, как набрать 400 000 000 пользователей и продать сервис за 19 Инстаграмов, затерялось где-то на книжной полке. Постараюсь его найти, если кому интересно.

Заинтересовавшихся прошу под кат.

Создание проекта


Открываем Xcode и создаем новый проект.



Берем Single View Application за основу.



Вводим все данные для приложения и жмем «Next». Я выбрал наименее претенциозные регалии.



И проект готов.



Но, что это такое? Какая ужасная сортировка файлов по группам! Давайте это поправим.



Так-то лучше! Вы можете использовать свой способ сортировки файлов, но в этом руководстве я буду придерживаться модели выше. Кстати говоря, комбинация клавиш для создания новой группы — это Command + Alt + N.

Простой UI


Тем временем, я позволил себе создать новый класс NKLoginViewController и привязать его к UIViewController объекту в Interface Builder. Этот View Controller будет первым, что увидит пользователь. Это и логично — никакого чата без регистрации!



Продолжая развлекаться, я прикрутил текстовые поля, как Outlet, и Action кнопки «Войти» к нашему NKLoginViewController. Считаю это хорошим тоном, прикручивать IB объекты в .m файлах, дабы они были недоступны извне. Более того, мне нравится, когда код поделен на «Прагмы».



Создаем еще один контроллер (как представление в IB, так и новый класс) — список чатов. Используем стандартный код UITableViewController — нам никакого сверхъестественного функционала тут не нужно, пока что.



Слегка изменим код NKChatListTableViewController.m, чтобы в таблице хоть что-то отображалось:

Жми меня!
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return 20;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
    cell.textLabel.text = @"Vasiliy Pupkin";
    return cell;
}

Теперь подумаем над навигацией. Все приложение у нас будет встроено в один UINavigationController и контроллеры мы будем «пушить» и «попать» в зависимости от ситуации. Встроим же приложение в UINavigationController! Let the magic time begin!



Добавляем названия контроллеров и Segue от Login View Controller до Chat List Table View Controller. Назовем ее «SegueToChatList». Вот так выглядит наше приложение сейчас.



Слегка поработаем над кодом Login View Controller. Дадим пользователю возможность убирать клавиатуру. Для этого мы сделаем контроллер делегатом текстовых полей.



А сам код контроллера поправим следующим образом:

NKLoginViewController.h
#import <UIKit/UIKit.h>

// Добавим нужный протокол к интерфейсу
@interface NKLoginViewController : UIViewController <UITextFieldDelegate>

@end

NKLoginViewController.m
#import "NKLoginViewController.h"

@interface NKLoginViewController ()

@property (weak, nonatomic) IBOutlet UITextField *emailTextField;
@property (weak, nonatomic) IBOutlet UITextField *passwordTextField;

- (IBAction)loginTouched:(UIButton *)sender;

@end

@implementation NKLoginViewController

#pragma mark - UITextFieldDelegate -

// Сделаем так, чтобы по нажатию "Done" клавиатура пряталась
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
    [textField resignFirstResponder];
    return YES;
}

#pragma mark - Button methods -

// Пока что просто переместимся на следующий экран по нажатию кнопки "Войти".
- (IBAction)loginTouched:(UIButton *)sender
{
    [self performSegueWithIdentifier:@"SegueToChatList" sender:self];
}

На время перенесем все элементы на контроллере логина вверх — это ведь простой UI. О том, как интерактивно перемещать элементы интерфейса вверх при появлении клавиатуры, я расскажу в следующей части.

Наше приложение уже можно потыкать!



Создаем третий — и последний — контроллер. Попадать в него мы будем при помощи нажатия на ячейку предыдущего контроллера. Сам контроллер состоит из UITableView, источником данных которого назначен контроллер, текстового поля и кнопки «Отправить». Полагаю, этот экран интуитивно понятен.



Код NKChatViewController.m ниже:

Жми меня!
#import "NKChatViewController.h"

@interface NKChatViewController ()

@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (weak, nonatomic) IBOutlet UITextField *messageTextField;

- (IBAction)sendTouched:(UIButton *)sender;

@end

@implementation NKChatViewController

#pragma mark - View Controller life cycle -

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    // Показываем клавиатуру, как только попадаем на контроллер
    [_messageTextField becomeFirstResponder];
}

#pragma mark - UITableViewDataSource -

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return 20;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    cell.textLabel.text = @"Вася Пупкен";
    cell.detailTextLabel.text = @"Привет, как дела?";
    return cell;
}

#pragma mark - Button methods -

- (IBAction)sendTouched:(UIButton *)sender
{
    
}

@end

Простенький UI для нашего мессенджера готов. Приступаем к самому интересному — начинке приложения!

Привязка к сервису мгновенных сообщений


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

Все, что вам нужно сделать — это зарегистрировать на c2call.com и купить учетку за $100. К сожалению, в бесплатной версии не работает регистрация через low-language API. Возможно, что-то изменится на момент прочтения вами этой статьи. Однако вместо помесячной оплаты C2Call сняли с меня $100 и, похоже, забыли про меня. Больше денег не списывали. Я не призываю вас ни покупать продукт, ни пытать удачу с месячной подпиской. Полагаю, мне просто повезло.

После регистрации, покупки учетки и регистрации приложения на сервисе — это довольно тривиальная задача — качаем SDK. В архиве пара-тройка примеров, как собирать приложения. Нам понадобятся следующие два объекта:



Переносим их в наш проект.



Добавляем следующие фреймворки и библиотеки в проект:

Ужасающий список фреймворков и библиотек
AVFoundation.framework
Accounts.framework
AdSupport.framework
AddressBook.framework
AddressBookUI.framework
AssetsLibrary.framework
AudioToolbox.framework
CFNetwork.framework
CoreAudio.framework
CoreData.framework
CoreFoundation.framework
CoreLocation.framework
CoreMedia.framework
CoreTelephony.framework
CoreText.framework
CoreVideo.framework
MapKit.framework
MediaPlayer.framework
MessageUI.framework
MobileCoreServices.framework
OpenGLES.framework
QuartzCore.framework
QuickLook.framework
Security.framework
StoreKit.framework
SystemConfiguration.framework
iAd.framework
libsqlite3.dylib
libz.dylib

И прописываем следующее в Build Settings:

HEADER_SEARCH_PATHS = /usr/include/libxml2
OTHER_LDFLAGS = -lxml2 -lstdc++
ARCHS = armv7
VALID_ARCHS = armv7

Теперь немного поменяем App Delegate:

NKAppDelegate.h
#import <UIKit/UIKit.h>
#import <SocialCommunication/SocialCommunication.h>

@interface NKAppDelegate : C2CallAppDelegate <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;

@end

NKAppDelegate.m
@implementation NKAppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.affiliateid = @"6B9DF5671444320162B";
    self.secret = @"2fd9cd18aa4d957a4030c0455101646d";
    
    return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

@end

Мы засабклассили элемент от C2Call, да рассказали ему о наших данных. Ваши Affiliate ID и Secret вы можете посмотреть в админке сервиса.

Закончили с настройкой фреймворка, пора его использовать.

Создадим подкласс NSObject под названием NKChat, в котором мы инкапсулируем всю логику чата. Думаю, будет правильным дать вам примерный листинг кода NKChat.m, а после объяснить его.

NKChat.m
#import "NKChat.h"
#import <SocialCommunication/SocialCommunication.h>

@implementation NKChat

#pragma mark - Singleton pattern -

// 1
+ (instancetype)sharedManager
{
    static NKChat *sharedChat = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedChat = [self new];
    });
    return sharedChat;
}

#pragma mark - Accessors -

// 2
- (NSArray *)chatHistory
{
    return [self fetchChatHistory];
}

#pragma mark - General methods -

// 3
- (void)login:(NSString *)email password:(NSString *)password success:(void(^)())successBlock failure:(void(^)())failureBlock
{
    NSDictionary *dictionary = @{@"EMail":email,
                                 @"Password":password};
    
    [[C2CallPhone currentPhone] registerUser:dictionary
                       withCompletionHandler:^(BOOL success, NSString *result) {                           if (success) {
                               [[C2CallPhone currentPhone] startC2CallPhone];
                               successBlock();
                           } else {
                               failureBlock();
                           }
                       }];
}

// 4
- (void)logout
{
    [(C2CallAppDelegate *)[UIApplication sharedApplication].delegate logoutUser];
}

// 5
- (void)sendMessage:(NSString *)message toUser:(NSString *)userId
{
    [[C2CallPhone currentPhone] submitMessage:message toUser:userId];
}

// 6
- (NSArray *)fetchChatHistory
{
    // Получаем все Managed Object истории чата
    NSFetchRequest *request = [[SCDataManager instance] fetchRequestForChatHistory:YES];
    NSFetchedResultsController *controller = [[SCDataManager instance] fetchedResultsControllerWithFetchRequest:request sectionNameKeyPath:nil cacheName:nil];
    NSError *error;
    [controller performFetch:&error];
    
    // Собираем результирующий массив
    NSMutableArray *result = [NSMutableArray array];
    for (NSManagedObject *chat in controller.fetchedObjects) {
        // Получаем словарь чата
        NSArray *chatKeys = @[@"contact", @"lastTimestamp", @"missedEvents"];
        NSMutableDictionary *inChat = [[chat dictionaryWithValuesForKeys:chatKeys] mutableCopy];
        
        // Проверяем на дубликаты
        NSMutableDictionary *dublicate = nil;
        for (NSMutableDictionary *dict in result) {
            if ([dict[@"contact"] isEqualToString:inChat[@"contact"]]) {
                dublicate = dict;
                break;
            }
        }
        
        // Получаем все сообщения
        NSMutableArray *messages = (dublicate) ? dublicate[@"messages"] : [NSMutableArray array];
        for (NSManagedObject *chatEvent in [chat valueForKey:@"chatHistory"]) {
            NSArray *chatEventKeys = [[[chatEvent entity] attributesByName] allKeys];
            NSMutableDictionary *inChatEvent = [[chatEvent dictionaryWithValuesForKeys:chatEventKeys] mutableCopy];
            //            NSLog(@"%@",inChatEvent);
            inChatEvent[@"ManagedObject"] = chatEvent;
            [messages addObject:inChatEvent];
        }
        [messages sortUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"timevalue" ascending:YES]]];
        
        if (dublicate) {
            dublicate[@"messages"] = messages;
            [dublicate[@"ManagedObjects"] addObject:chat];
            dublicate[@"missedEvents"] = @([dublicate[@"missedEvents"] intValue] + [inChat[@"missedEvents"] intValue]);
            if (!dublicate[@"name"])
                dublicate[@"name"] = inChat[@"name"];
        } else {
            inChat[@"messages"] = messages;
            inChat[@"ManagedObjects"] = [NSMutableArray arrayWithObject:chat];
        }
        
        // Добавляем словарь в результат
        if (!dublicate)
            [result addObject:inChat];
    }
    
    // Сортируем результат
    [result sortUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"lastTimestamp" ascending:NO]]];
    
    // Возвращаем результирующий массив
    return [result copy];
}

@end

Пойдем по-порядку:

  1. Стандартный шаблон — синглтон. Ничего необычного для вас здесь быть не должно. У нас один объект, который отвечает за чат — больше не надо.
  2. Метод-аксессор, который возвращает массив истории чата в нужной нам форме.
  3. Метод для регистрации и логина. Фишка C2Call в том, что, когда вы входите с одними данными в первый раз, вы регистрируетесь. Когда вы входите с теми же данными второй раз, вы просто входите. Этот метод как-раз и недоступен бесплатным подписчикам, к сожалению. Вы можете обойти этот метод добавив нативное окно регистрации от C2Call, дабы сэкономить.
  4. Метод для логаута. Дешево и сердито.
  5. Метод для посылки сообщения — тоже довольно прост.
  6. Ужасный и монструозный метод-скатерть, который возвращает в нужном формате историю чата. Здесь собраны все камни, о которые можно столкнуться, используя C2Call. Во-первых, данные хранятся в Core Data. Во-вторых, имена контактов постоянно разные — то id придет, то имя и фамилия. В-третьих, забудьте пока что про этот метод. Он работает и для этого туториала сойдет :)

Ну, когда все готово для работы, пора использовать магию кода!

Добавьте в NKAppDelegate.m инициализацию NKChat, если еще не сделали этого.

NKAppDelegate.m
#import "NKAppDelegate.h"

@implementation NKAppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.affiliateid = @"6B9DF5671444320162B";
    self.secret = @"2fd9cd18aa4d957a4030c0455101646d";
    [NKChat sharedManager];
    return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

@end

Теперь слегка изменим метод loginTouched у класса NKLoginViewController. Не забудьте сделать импорт NKChat!

Жми меня!
- (IBAction)loginTouched:(UIButton *)sender
{
    sender.enabled = NO;
    [[NKChat sharedManager] login:_emailTextField.text
                         password:_passwordTextField.text
                          success:^{
                              [self performSegueWithIdentifier:@"SegueToChatList" sender:self];
                              sender.enabled = YES;
                          }
                          failure:^{
                              sender.enabled = YES;
                          }];
}

Здесь мы выключили кнопку, пока грузится ответ с сервера, отправили запрос на сервер, переходим в новый контроллер в случае успеха, включаем кнопку, вне зависимости от результата.

В этой части туториала мы будем работать с двумя учетными записями: nikita@borodutch.com и luke@borodutch.com. Мы просто захардкодим возможность отправлять сообщения этим двум контактам, временно.

Слегка изменим NKChatListTableViewController.m таким образом, чтобы можно было отправлять сообщения только этим двум контактам.

Жми меня!
#import "NKChatListTableViewController.h"

@interface NKChatListTableViewController ()

@end

@implementation NKChatListTableViewController

#pragma mark - UITableViewDataSource -

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return 2;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
    cell.textLabel.text = (indexPath.row) ? @"nikita@borodutch.com" : @"luke@borodutch.com";
    return cell;
}

@end

Результат манипуляций:



Добавим метод передачи информации в следующий контроллер в NKChatListTableViewController.m.

Жми меня!
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(UITableViewCell *)sender
{
    UIViewController *dest = segue.destinationViewController;
    dest.title = sender.textLabel.text;
}

Нам осталось только получать нужную историю чата и отправлять сообщения нужным контактам! Дело в шляпе, сударь.

Как в старые добрые времена, приведу листинг NKChatViewController.m вместе с объяснениями чуть ниже.

Жми меня!
#import "NKChatViewController.h"
#import <SocialCommunication/SocialCommunication.h>
#import "NKChat.h"

@interface NKChatViewController ()

@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (weak, nonatomic) IBOutlet UITextField *messageTextField;

- (IBAction)sendTouched:(UIButton *)sender;

@end

@implementation NKChatViewController
{
    NSArray *tableData;
}

#pragma mark - View Controller life cycle -

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    // 1
    tableData = [self getTableData];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    // Показываем клавиатуру, как только попадаем на контроллер
    [_messageTextField becomeFirstResponder];
    
    // 2
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receivedMessage) name:@"kC2CallPhoneReceivedMessage" object:nil];
}

#pragma mark - UITableViewDataSource -

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // 3
    return tableData.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 4
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    cell.textLabel.text = ([tableData[indexPath.row][@"eventType"] isEqualToString:@"MessageIn"]) ? self.title : @"Я";
    cell.detailTextLabel.text = tableData[indexPath.row][@"text"];
    return cell;
}

#pragma mark - Button methods -

- (IBAction)sendTouched:(UIButton *)sender
{
    // 5
    [[NKChat sharedManager] sendMessage:_messageTextField.text
                                 toUser:@"c45645f71465dcff18e"];
    [self addMessage:_messageTextField.text];
    _messageTextField.text = @"";
}

#pragma mark - General Methods -

- (void)addMessage:(NSString *)message
{
    // 6
    NSMutableArray *mTableData = [tableData mutableCopy];
    [mTableData addObject:@{@"text":message,
                            @"eventType":@"MessageOut"}];
    tableData = mTableData;
    [_tableView reloadData];
}

- (void)receivedMessage
{
    // 7
    tableData = [self getTableData];
    [_tableView reloadData];
}

- (NSArray *)getTableData
{
    // 8
    for (NSDictionary *chat in [NKChat sharedManager].chatHistory)
        if ([chat[@"contact"] isEqualToString:self.title])
            return chat[@"messages"];
    return nil;
}

@end

По-порядку:

  1. Как только контроллер загрузился, мы заполняем его нужными данными
  2. kC2CallPhoneReceivedMessage — это дефиниция названия нотификации о том, что пришло новое сообщение; подписываемся на это событие
  3. Нам нужно столько ячеек, сколько всего сообщений есть в истории этого чата
  4. Каждой ячейке даем нужное имя контакта и сообщение
  5. Отправляем сообщение при помощи метода из NKChat; добавляем сообщение в локальные данные контроллера, потому что сообщению нужно время для того, чтобы оно добавилось в историю C2Call; очищаем поле отправки
  6. Метод добавления сообщения в локальные данные контроллера. Полагаю, интуитивно понятен
  7. При получении сообщения нужно перезагрузить историю в контроллере и заставить таблицу обновить свои данные
  8. Просто проходимся по всей истории и возвращаем историю нужного нам контакта

Вот, что у нас получилось (большая гифка):



Заключение


Огромное спасибо, что дошли до конца первой части этого руководства. В скором времени, как появится пара свободных деньков, напишу вторую часть. Исходный код первой части тут.

Во второй части мы решим несколько щепетильных вопросов по UI, обойдем пару багов C2Call (например, тот, что виден на последней гифке с получением сообщений), добавим функционала приложению и влепим пару-тройку котиков.

Если у вас есть какие-либо вопросы по туториалу, смело задавайте их в комментариях — на все отвечу.

В случае обнаружения вами опечаток или неточностей в статье, прошу обращаться в мой хабрацентр.

До скорой встречи.
Nikita Kolmogorov @backmeupplz
карма
14,0
рейтинг 23,2
АбраКодастр
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (55)

  • +7
    Спасибо. Просто большое спасибо.
    • +4
      Пожалуйста! В следующей статье предстоит описать еще очень много интересного :) Начиная от ячеек UITableView с высотой по размерам текста, заканчивая передачей файлов «на лету».
  • +3
    <offtop>С возвращением, бро</offtop>

    Под iOS не пишу, но
    К сожалению, пособие о том, как набрать 400 000 000 пользователей и продать сервис за 19 Инстаграмов, затерялось где-то на книжной полке. Постараюсь его найти, если кому интересно.

    было бы интересно. Ищи пособие :D
    • +3
      <offtop>Спасибо, стараюсь ;)</offtop>

      Поскребу по сусекам книжных полок сразу, как доберусь до дома!
      • +1
        я б даже сказал, нафиг вторую часть, давай сразу про 400 000 000 пользователей! Части мы и сами нарисуем какие-нибудь, в крайнем случае 8-)
  • +8
    С сегодняшнего дня актуально писать код на Swift.
    Если сделаете пример с кодом на Swift цены Вам не будет.
    • +5
      Спасибо! Учту. В следующую статью добавлю прилично Стрижа :)
    • +14
      уже актуально? эх, языку еще трех дней не исполнилось, а уже :)
      • 0
        Учитывая, что уже в сентябре Apple начнёт принимать приложения на Свифте для продажи, то да — актуально до чёрта!
        • 0
          ну это да, но objective-c пока все так же работает, как и раньше, я думаю, что его еще рано списывать со счетов
    • +4
      Требования: 5+ лет опыта на Swift.
      Пока еще явно рано называть его актуальным. Хотя бы пусть зарелизится Xcode 6, тогда уж в принципе, можно думать о рабочих проектах на нем.
      • 0
        Так а зачем думать о рабочих — рабочие по старинке на обкатаном ObjC.
        А это ради расширения кругозора и обучения.
  • +1
    Не пробовали наш QuickBlox в качестве бекенда? Как раз для этих целей, и $100 не надо платить.
    • 0
      Пробовали, хорошая штука :) Но с тем фреймворком, что используется в статье, я работал дольше и глубже знаю его структуру, поэтому и привожу его в качестве примера.
      • +3
        Понял. Мы скоро релизим много вкусного:
        • open-source проект-пример с готовым профессиональным UI из коробки под iOS, Android и Web. Всё можно менять, но не нужно будет делать пресловутые чат бабблы с нуля.
        • Chat 2.0 — набор дополнительных REST JSON API для управления чат-сервером, чатами и комнатами + более мощная админка для ручного управления
        • полная интеграция с WebRTC для видео и аудио звонков

        Было бы хорошо получить обратную связь от вас после этих релизов, т.к. расчет на то, чтобы уменьшить всю эту работу еще в разы и действительно добиться вот этого «WhatsApp» за сутки, не просто на уровне proof of concept, а с UI и кучей необходимых примочек.
        • 0
          Пользуюсь в 1 проекте, Все хорошо, только 1 вопрос:
          А документация полная по всем методам в библиотеке под Android, да и под iOS имеется?
          • +2
            да, конечно, вот джавадоки:
            sdk.quickblox.com/android/
            и эпплдоки:
            sdk.quickblox.com/ios/

            скоро выпускаем серьезный апгрейд iOS библиотеки
            • +1
              Видимо невнимательно искал. Спасибо!
  • +16
    Какой же это мессенджер за 24 часа, если у него под капотом чужой движок передачи данных и чужой механизм работы с БД. Это скорее «простенький UI за 24 часа».
    Как обычно, 95% времени занимает доделать остальные, хм, «5%» работы. Нормальные тянущиеся бабблы для сообщений, сложные случаи одновременной отправки-прихода-переотправки-удаления. А когда дойдет до того, что нужно будет добавить еще один тип сообщений? Мы же хотим и видео отправлять, и фоточки, и стикеры и прочую муть.

    Касательно примера: на каждый приход сообщения делается reloadData. Вы серьезно? И когда у ячеек будут разые размеры, то каждый раз перерасчитывать все высоты? А если там CoreText? Нужны отдельные insert'ы и reload'ы. И в UITableView там начинается самое интересное. Поэтому (и не только поэтому) я бы посоветовал сразу делать UI на основе UICollectionView.

    UI всегда состоит из мелочей, которые особенно важны пользователям и именно они занимают львиную часть работы.
    • +1
      Уважаемый prizzrak,

      1. В статье я написал, что можно использовать самописный сервер, но это может занять больше 24х часов.
      2. Не вижу ничего плохого в том, чтобы использовать готовое решение, в котором есть и кроссплатформенность, и удобный API, и пуш-нотификации, и текстовые, видео и аудио сообщения. Более того, там есть аудио и видео звонки. Сколько времени вы потратите на то, чтобы поднять такой сервер? Полагаю, больше 24х часов.
      3. Тянущиеся баблы будут во второй части — там сложный UI.
      4. «Сложные случаи одновременной отправки-прихода-переотправки-удаления» правильно обрабатываются этим фреймворком из коробки. Еще один плюс использования готового решения.
      5. «А когда дойдет до того, что нужно будет добавить еще один тип сообщений?» — увы, этот туториал только про текстовые, аудио, видео сообщения, передачу файлов и аудио и видео звонки, а так же про профили пользователей. К сожалению, я решил этим ограничиться.
      6. Да, в этом примере делается reloadData. В следующей части я и покажу, как делать insert'ы. Не вижу смысла использовать здесь UICollectionView. Это мое личное мнение, вы можете написать статью, используя UICollectionView, вам никто не мешает.
      7. Да, UI состоит из мелочей, которые я и затрону во второй части. «Делаем красивый UI» в описании второй части, как бы, намекает.

      В любом случае, спасибо за feedback. Надеюсь, я ответил на ваши насущные вопросы. Если еще что-то не ясно — не стесняйтесь спрашивать.
      • +10
        Ну так и назвали бы честно статью — «Интеграция с C2Call». О чем сейчас туториал — непонятно. Использование UITableView на базовом уровне? Свежо.

        >> Не вижу смысла использовать здесь UICollectionView.
        Надеюсь, что при выборе использования одной сторонней библиотеки для всего вы куда более серезьно рассмотрели все проблемы, чем когда решили использовать UITableView.
        • 0
          Эта серия туториалов о том, как с нуля поднять свой мессенджер. Может быть полезной для новичков. Прошу прощения за то, что первая часть получилось немного суховатой для вас. Полагаю, то, что может быть интересно вам, просто сюда не влезло и будет во второй части.

          Статья названа честно. Это чат-мессенджер по типу WhatsApp за сутки.

          Расскажите мне, чем плох UITableView для решения этой задачи, пожалуйста. Вполне возможно, что это я дурак и делаю все неправильно.
          • +3
            Да это какой-то антитуториал :)
            Еще раз посмотрел предложенный код — так там еще и fetchChatHistory вытягивает всю О_о историю чата. Нет ни слова про частичный фетч, ни как будет определяться, что изменилось в FRC и как делать diff для обновления UI…
            Не понятно на что там потрачено 24 часа. На написание статьи на хабре что ли.
            • 0
              У меня создается стойкое ощущение, что вы просто хотите к чему-то придраться.

              Да, местами код не идеален. Про частичный фетч хотел рассказать во второй статье.

              Вы правда хотели, чтобы я все это уместил в одну статью? Не забывайте, что это только первая часть.
              • +11
                Я думаю, товарищ prizzrak, как разработчик Viber, точно знает, что невозможно написать мессенджер за 24 часа ;)
          • +2
            Мне понравился ваш туториал, поэтому, пожалуйста, продолжайте несмотря на всю критику. Уже несколько раз пытался начинать писать приложение на iOS, но обычно сталкивался с отсутствием туториалов, описывающих более-менее приближенные к реальности концепты. После прочтения вашего поста занялся снова и с нетерпением жду вторую часть! Спасибо. ;)
            • 0
              Спасибо большое! :) Такие комментарии мотивируют писать дальше.
        • 0
          Да, разверните мысль про UICollectionView.
          • +6
            Надо иметь ввиду, здесь я веду речь исключительно в разрезе использования UITableView/UICollectionView в качестве контроллера для UI элементов Conversation.
            С UITableView есть одна очень большая проблема. Она связана с тем, что контроллер должен обновляться. Часто. Каждая отправка/прием/обновление статуса(sending/delivered/seen/unsent) сообщения генерируют обновление для UI. Перезагружать данные в таблице целиком (как в примере) нельзя, это чревато просто гигантским оверхедом. Нужно перезагружать/вставлять/удалять ячейки по-отдельности. И, как со мной любезно согласился backmeupplz, ввиду того, что UI состоит из мелочей — все эти операции должны быть анимированны. Анимации могут быть добавлены группами, сразу к нескольким ячейкам. И из-за того, что такие операции вызываются очень часто, предыдущие анимации не успевают закончиться, а когда для одной и той же ячейки применяется несколько анимаций — UITableView просто падает. Это все — главный поток, городить гуарды и блокировать поток нельзя. Если делать очередь анимаций — это дает весьма заметный лаг на действия пользователя: он вроде написал сообщения, а оно не может появиться, пока не отработают предыдущие анимации. Не говоря уж о принципиальной большой сложности реалицзации этой очереди. В UICollectionView есть batchUpdate.
            Далее. UITableView — это функционально UICollectionView с вертикальным лэйаутом, который никак нельзя изменить. Например, есть простая задача. Баббл с исходящим текстом прижимается к правой стороне. Баббл с входящим — к левой. Ну, как в Wath’s app. Нужно сделать так, чтобы верхняя граница левого баббла была по середине высоты правого баббла, чтобы бабблы располагались компактнее. Каждый баббл — ячейка. Как вы будете решать эту задачу на UITableView? В UICollectionView пишется кастомный лайаут и все.
            Далее. На экране могут быть разные дополнительные динамические и статические элементы, кроме текстовых бабблов. Это разные разрывные линии, isTyping, разлиные системные сообщения. ЧТо-то из этого можно сделать отедльными ячейками. Но когда вы начнете делать ячейку “user is typing”, которая должна постоянно появляться и пропадать не вызывая движения всей таблицы, которая была сделана на UITableView… UICollectionView вместе с supplementary view передают горячий привет.
            Далее. UIDynamicKit. Больше можно ничего не писать.
            Конечно, и у UICollectionView есть свои подводные камни. Я не помню у эппла ни одного компонента без магии. Но только того, что я написал, хватило нам для того, чтобы мы дропнули iOS5 и переписали код на UICollectionView.
            • 0
              Теперь понятно. Просто вы так много нюансов оставили за скобками утверждения в начале, что появился такой вопрос у меня.
            • –3
              1. Не знаю, каким образом вы писали код для UITableView или насколько быстро отправляли\принимали сообщения, но insert вполне достаточно, чтобы вставлять ячейки со скоростью 3-4 в секунду, да при том анимировать все это действо. UITableView мы использовали в нескольких соц. сетях, которые разработали — везде все красиво и не подтормаживает.
              2. Решение с тем, чтобы один бабл притянуть влево, а другой вправо — это простое создание двух прототипов ячеек. И не надо мне говорить, что это неправильное решение. Примите тот факт, что есть несколько правильных решений, которые могут отличаться от вашего.
              3. Чтобы баблы распологались компактнее — тут да, удобнее использовать UICollectionView. Но у нас нет такой задачи в этом туториале.
              4. «User is typing» свободно делается еще одним прототипом ячейки. Про правильные решения посмотрите, пожалуйста, пункт 2.
              5. У нас нет задачи использовать здесь UIDynamicKit. Если бы была, то да — удобнее использовать UICollectionView.
              6. Я только что привел 5 причин, почему дропать UITableView в данной ситуации не обязательно.

              Выше уже писал, но не кажется ли вам, что все проблемы притянуты за уши? UITableView вполне себе справляется со всеми поставленными задачами в рамках данного руководства. А про UICollectionView вообще можно написать отдельную статью.
              • +5
                Вы привели один аргумент «мне это не встречалось» и несколько «сегодня мне это не нужно». Если с первым еще как-то можно согласится, то второе — это конец. Ваши решения немасштабируемы. Нужно будет добавить — и все нужно будет выкинуть и написать снова. И если раньше разработчики были ограничены числом поддерживаемых осей или числом доступных контролов, то сейчас это больше похоже на «склепаем на коленке кое-как».
                Вы где-то выше спросили к чему у меня претензии. У меня претензии к качеству, к поверхностности и эпическому замаху на what's app, хотя на деле показан пшик.
                За сим извольте откланяться.
                • –4
                  Уважаемый prizzrak,

                  Это не продукт, который нужно масштабировать, а просто туториал. Proof of concept, если желаете — без каких либо претензий на эпичность и значимость. Просто статья, которая может показать начинающим, как создаются приложения от 0 и до определенной готовности.
  • +5
    Мессенджер за сутки? Вы смеётесь! Cинхронизация списка контактов, подтверждение доставки сообщений, смайлы, ошибки базы данных, многопоточная рассинхронизация. Если бы всё было так просто, скайп бы не глючил :)
    • +1
      Вы читали статью? :) Здесь используется готовое серверное решение, иначе было бы невозможно сделать это за сутки.
      • +1
        Похоже, комментарий минусуют те, кто могут это сделать. Перефразирую: без готового серверного решения в рамках 24х часов подобное приложение сделать крайне сложно.
  • +3
    Я уже ожидал увидеть статью о написании на swift :))
  • 0
    > C2Call
    Чего только не выдумают, лишь бы Jabber не юзать.
    • +2
      Напишите статью, как за 24 часа написать простенький чат-мессенджер с использованием Jabber ;)
  • +1
    А есть какие то анонимные мессенджеры с хорошим шифрованием и открытым кодом?
    • +1
      Не хочу ничего рекламировать и боюсь, что заминусуют, но тут один персонаж, создавший одну социальную сеть в СНГ, у которой офис в Санкт-Петербурге, сделал мессенджер. Правда не помню, open source ли он.
      • +2
        Протокол — да, серверы — нет.
        • +1
          Спасибо за поправку! Знал же, что чего-то не знаю :) хорошо, что никаких ссылок не дал
  • +4
    Если интересно, могу рассказать (возможно отдельной статьей, либо просто дав доступ к исходникам) о том, как сделать красивый UI для bubbles для чата. Как раз недавно задачу такую реализовывал.
    • 0
      Расскажите отдельной статьей! Думаю, всем будет интересно :)

      Если руки не дойдут — то просто покажите исходники, пожалуйста.
      • +2
        Хорошо. Не знаю правда, достаточно ли этого материала для отедльной статьи ) В любом случае отпишусь.
  • +1
    Хотелось бы добавить, что двухсимвольный префикс классов — прерогатива Apple.
    Простым смертным рекомендуется использовать трёхсимвольные префиксы.
    Хотя с релизом Свифта это правило потеряет актуальность.
    • 0
      Правда? Никогда не знал об этой детали. И десятки других разработчиков, судя по всему, тоже: cocoadev.com/ChooseYourOwnPrefix
      • +1
        Не все читают документацию, что поделать.

        Two-letter prefixes like these are reserved by Apple for use in framework classes.

        Your own classes should use three letter prefixes.


        Но у меня почти все проекты тоже с двумя :)
        • 0
          Ого! Вот это поворот. Спасибо за информацию. Пора перестраиваться.
          • 0
            Лучше начать осваивать Свифт с человеческими неймспейсами :)
  • +2
    Я смотрю, тут некоторые слишком болезненно восприняли название статьи. Ну нельзя же так серьезно (уверен, автор назвал так статью не без доли иронии)! Никто не умаляет достоинств разработчиков мессенджеров, всем понятно, что нормальный мессенджер за сутки не делается.

    Цель статьи — объяснить новичкам некие базовые принципы работы на примере разработки конкретного… приложения (ну, или UI).

    Что же в этом плохого? На похожих статьях учился говнокодить на PHP.

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

    Тем, кто хочет разобраться и нужны реальные примеры — вполне полезная статья. В нете полно трэша, а тут более менее пристойное что-то.

    Или я не прав?
    • +2
      Оп, и снова работа для той самой «программы, умеющею распознавать сарказм, и которую ищут спецслужбы». Видите, нужна!
  • 0
    А магазин стикеров будет?!
    • 0
      Такого я еще не делал :) думаю, не буду городить плохой код и эту тему рассматривать не буду в рамках текущей серии статей.

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