Чат своїми руками +4
- 02.06.17 7:08 •
- SSul •
- # 330044 •
- Хабрахабр •
- 0 •
- 2600
- такий же як Forbes, тільки краще.
У цій статті ми опишемо тонкощі написання чату. Розумію, що придумано вже досить готових рішень. Побродивши по закутках безкрайнього, відшукали пару придатних бібліотек, які надають чат «через коробки». У цій статті вони перераховані не будуть. Так ось, перспектива заюзать готове рішення здавалася привабливою. Але, ще раз подумавши про комплексності майбутнього завдання, ми вирішили писати з нуля.
Завдання не зовсім типова, і вимоги по проекту до кінця не визначені. Ризиковано заганяти себе в рамки невикористаної раніше бібліотеки. Чого, звичайно, не скажеш про перевірених в бою лібах начебто RxDataSources, яку ми неодмінно вирішили використовувати. Якщо щось трапиться, в будь-який момент розробки вона легко заміщується вручну прописаної логікою по роботі з осередками. Також не обійшлося без насущного «джентльменського наборчика iOS-розробника»: RxSwift, Realm, Alamofire і Kingfisher. Їх ми також підключили до проекту.
Список повідомлень реалізовували через UICollectionView через його триразової гнучкості. І анімована колекція в UIKit красивіше ніж таблиця. При цьому анімацією можна управляти. Від Storyboard'a в рамках чату відразу відмовилися, тому як Autolayout і продуктивність - речі, якщо не з різних галактик, то, як мінімум, один про одного не знають.
Все це говорить про те, що архітектура повідомлень повинна бути спроектована особливо ретельно.
Контент розміщується ближче до певного краю екрану в залежності від типу повідомлення «своє-чуже». Висота стандартно розраховується за допомогою шаблонної осередки в залежності від вищеописаної конфігурації. Тут же розраховується відстань між осередками. У CollectionViewLayout воно дорівнює нулю, відстань просто додається до висоти комірки, щоб логіка не розносився по об'єктах.
Технічна складність проявилася в посторінковою завантаженні. Десяток повідомлень вантажився половину секунди на iPhone 6 - дуже слабкий результат. Насправді, нічого дивного: осередки важкі і весь процес їх створення і наповнення відбувався в cellForRow. Від цього Main-потік терпів лихо. Досить швидко прийшла ідея створювати вьюха з контентом відразу після отримання повідомлень в Background-потоці. Деякі скажуть, що так не можна, і матимуть рацію. UILabel в тлі створити так і не вийшло, додаток просто фарбований. Решта UIView-примітиви спокійно дозволяють створювати себе в бекграунд, причому, саме клас UIView дозволений до створення в тлі офіційної документацією.
Практика показала, що фрейми у об'єктів також можна задавати з фону. Цього нам вистачило, щоб розмазати створення осередку з повідомленням і винести логіку розрахунку розміру осередку в фон. Також, варто відзначити, що, змістивши додавання всіх сабвьюх в клітинку на момент willDisplayCell, ми помітно підвищили плавність подгрузки. І в цьому методі ми даємо зрозуміти контент-вьюха, що вона нарешті потрапила в Main-потік, а, значить, тут можна налаштувати всі аутлети (створити UILabel'и) і підставити в них контент. По суті, відразу після отримання повідомлення, в тлі йде формування розміру осередку, далі її генерація в цьому розмірі, а в самий останній момент перед відображенням отрисовка всього контенту на ній:
В осередках повинен бути мінімум прозорості. Залозу девайсів складно перетравлювати змішані шари, а тому напівпрозорі вьюха - бич продуктивності, і будь-який складний компонент повинен бути від нього позбавлений. Обрізка кутів на вьюха все одно робить їх напівпрозорими. Для наочності можна в симуляторі пристрою виставити галочку на Color blended layers, і він Отріс їх червоним кольором. Тут нічого не поробиш, принаймні нічого, що можна придумати за короткий час.
Єдиний мінус застосованого підходу - зберігання посилань на вьюха з контентом.
Раз створити, в'ю вже ніколи не деаллоціровалась, хіба що після виходу з екрану. Це рішення було прийнято усвідомлено. Це позбавило нас від проблем з продуктивністю. Рітейн вьюха дозволив прораховувати розмір і створювати їх лише один раз і в тлі. На пам'яті це майже ніяк не позначилося, головний принцип - не потягти за собою картинки. Поле image у UIImageView має обов'язково обнулятиметься в didEndDisplayingCell і знову знаходити значення лише при новому відображенні осередки.
Вуаля, і все плавно! Жарт. Плавно в Телеграма. Але для наших термінів дуже непогана продуктивність. Навіть на мінімально підтримуваному девайсе подгрузка ледве помітна, а на сучасних девайсах і зовсім не видно. Подальше додаток до типам повідомлень не позначилося на продуктивності подгрузки.
І нічого не віщувало біди, як раптом зламався CollectionViewLayout. Ну ви розумієте, як це зазвичай буває: збивається контент сайз, осередки стають невидимими і т.д і ніякої reloadData () в цьому випадку не рятує ... зламався і все тут? \ _ (?) _ /. Траплялося це в невизначений момент часу, просто при Скролл чату. Спробували поміняти вставку одиночних осередків на повне перезавантаження колекції, позбутися від RxDataSources, перенести створення уявлень в головний потік - все без толку. Сталося це після обширного Мержа гілок, тому розібратися, чим саме був викликаний брейк, з ходу не вийшло. Випадок був цікавий, і хотілося дістатися до істини. Замовник хотів збірку.
«TableView, друже, ми так за тобою нудьгували! ... в сенсі зрадники?»
Пішла буквально пара годин, перш ніж колекцію вдалося повністю замінити на таблицю. Не було абсолютної впевненості в тому, що це вирішить проблему. Але TableView відпрацьовував ідеально. Здавалося, що працював він навіть трохи швидше, ніж колекція. Але анімація інсерта осередків залишає бажати кращого. У чаті вона зовсім виглядає неприйнятно, тому було вирішено оновлювати список через reloadData (). А поломка CollectionView так і залишилася загадкою: це один з найскладніших елементів в UIKit'е, і розібратися в ньому за вихідні не вдалося. Головне, що завдання виконане - база чату реалізована на належному рівні.
Продуктивність - тема глибока і цікава. Але далеко не єдина, з якою ми зіткнулися. Були ще цікаві нюанси. Той же переворот таблиці.
Як всі знають, повідомлення в чаті починаються знизу, а у колекцій і таблиць немає відповідного функціоналу для подібного відображення осередків. Тут ми використовували CGAffineTransform: перевернули таблицю догори дном, а осередки перевернули ще раз - все ідеально, і без будь-яких можливих багів і проблем з продуктивністю.
Виштовхування дати текстом реалізували через додавання в текст повідомлення нерозривних пробілів довжиною трохи більше ніж довжина мітки з датою. Якщо прогалини діставали до краю повідомлення - значить і оригінальний текст стосувався дати і в цьому випадку текст переносився на наступний рядок збільшуючи висоту комірки, яка, в свою чергу, тягла за собою мітку з датою. З такою фішкою повідомлення виглядають більш компактно:
Варто сказати пару слів про AutoresizingMask. Деякі недооцінюють її важливість, а при верстці з коду корисно розуміти, як працює ця технологія. У нашому випадку були осередки, які розтягувалися і стискалися за подією. І, щоб кожного разу не відновлювати фрейми, маски були задані так, що уявлення змінювали свій розмір відповідно до розмірів своїх батьків. Без єдиного констрейнтів і рядки коду - чиста магія. Нижче представлений приклад її використання: спочатку маємо осередок, далі ми збільшуємо її висоту (без використання масок контент і залишився б в такому положенні), після чого, в правий нижній кут, підтягується дата, а весь інший контент розтягується:
Наступний нюанс: при появі клавіатури для можливості побачити повідомлення, які залишилися під нею у таблиці виставлявся нижній інсет. Ще він виставлявся зверху, в разі якщо спливала плашка про відсутність інтернету, і знизу, коли поле для введення повідомлення змінювало свою висоту, все це не рахуючи верхній і нижній інсет по дефолту. Віднімати і додавати в кожному місці потрібну константу - досить крихке рішення і зламається якщо хоча б один метод зголоситься зайвий раз. В якості вирішення була написана обгортка для UITableView, що містить масив структур з інсетамі по кожній причини. Причини були описані в перерахуванні, а кожен раз при зміні масиву все відступи складалися і привласнювалися таблиці. Таким чином, з'явилася можливість управляти кожним інсетом, не ламаючи інші. Варто згадати: все відступи в нашому випадку були перевернуті разом з таблицею через застосованих раніше трансформацій, тому верхній інсет був нижнім і навпаки.
Цікавих моментів і відкриттів при розробці було більш ніж достатньо, всіх не згадаєш. Повторюся, можна вкласти нескінченну кількість годин, роблячи додаток все більш плавним, швидким і гнучким. Але це, разом з візуальною частиною, - перша стадія, необхідний мінімум для успішного застосування чату. До наступному ступені можна віднести унікальність досвіду його використання. Чого далеко ходити, візьмемо нативний ios-месенджер iMessage. Крім чуйності, у месенджера безліч "модних фішок", більшості яких просто ніде немає. Для комунікації вони не настільки важливі, але якщо розглядати чат як окрему сутність, володар таких "фішок" виглядає більш досконалим і розвиненим на фоні інших. Етап реалізації таких особливостей в рідкісних випадках можна віднести до першої версії додатка. Тому, випустивши першу версію чату, ми сподіваємося в майбутньому продовжити роботу і наповнити чат унікальними особливостями.
Ви можете допомогти і перевести трохи коштів на розвиток сайту