Як зібрати WhatsApp за добу. Частина 1


 
Здрастуйте, дорогі читачі Хабрахабра!
 
У цій серії статей я розповім, як швидко і майже безболісно підняти свій власний WhatsApp під iOS. Статтю ділю на дві частини для вашої зручності:
 
 
     
  1. Створення проекту, простий UI, прив'язка до сервісу миттєвих повідомлень
  2.  
  3. Робимо красивий UI, додаємо відео і аудіо дзвінки, передачу фото і документів
  4.  
На жаль, посібник про те, як набрати 400 млн користувачів і продати сервіс за 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. Метод-аксессор, який повертає масив історії чату в потрібній нам формі.
  4.  
  5. Метод для реєстрації і логіна. Фішка C2Call в тому, що, коли ви входите з одними даними в перший раз, ви реєструєтесь. Коли ви входите з тими ж даними вдруге, ви просто входите. Цей метод як-раз і недоступний безкоштовним передплатникам, на жаль. Ви можете обійти цей метод додавши нативне вікно реєстрації від C2Call, щоб заощадити.
  6.  
  7. Метод для логаута. Дешево і сердито.
  8.  
  9. Метод для посилки повідомлення — теж досить простий.
  10.  
  11. Жахливий і монструозний метод-скатертину, який повертає в потрібному форматі історію чату. Тут зібрані всі камені, про які можна зіткнутися, використовуючи C2Call. По-перше, дані зберігаються в Core Data. По-друге, імена контактів постійно різні — то id прийде, то ім'я та прізвище. По-третє, забудьте поки що про цей метод. Він працює і для цього туторіал зійде :)
  12.  
Ну, коли все готово для роботи, пора використовувати магію коду!
 
Додайте в 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.  
  3. kC2CallPhoneReceivedMessage — це дефініція назви нотифікації про те, що прийшло нове повідомлення; підписуємося на цю подію
  4.  
  5. Нам потрібно стільки осередків, скільки всього повідомлень є в історії цього чату
  6.  
  7. Кожній комірці даємо потрібне ім'я контакту і повідомлення
  8.  
  9. Відправляємо повідомлення за допомогою методу з NKChat; додаємо повідомлення в локальні дані контролера, тому що повідомленням потрібен час для того, щоб воно додалося в історію C2Call; очищаємо поле відправки
  10.  
  11. Метод додавання повідомлення в локальні дані контролера. Вважаю, інтуїтивно зрозумілий
  12.  
  13. При отриманні повідомлення потрібно перезавантажити історію в контролері і змусити таблицю оновити свої дані
  14.  
  15. Просто проходимся по всій історії і повертаємо історію потрібного нам контакту
  16.  
Ось, що у нас вийшло (велика гифка):
 
 
 
 

Висновок

Величезне спасибі, що дійшли до кінця першої частини цього керівництва. Незабаром, як з'явиться пара вільних деньків, напишу другу частину. Вихідний код першій частині тут .
 
У другій частині ми вирішимо кілька педантичних питань по UI, обійдемо пару багів C2Call (наприклад, той, що видно на останній ДІФКУ з отриманням повідомлень), додамо функціоналу додатком і вліпимо пару-трійку котиків.
 
Якщо у вас є які-небудь питання по туторіали, сміливо задавайте їх у коментарях — на всі відповім.
 
У разі виявлення вами друкарських помилок або неточностей в статті, прошу звертатися в мій хабрацентр.
 
До швидкої зустрічі.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.