Как стать автором
Поиск
Написать публикацию
Обновить

Как написать отличную ленту новостей ВКонтакте за 20 часов

Время на прочтение9 мин
Количество просмотров14K
Всем привет! Недавно прошёл конкурс от ВКонтакте Mobile Challenge, и моя работа заняла призовое место. По заданию второго этапа необходимо было разработать ленту новостей для мобильных устройств, а главными критериям оценки были плавность скроллинга и загрузки постов. Ещё когда участвовал решил, что вне зависимости от конечного результата, попробую написать статью о подходе к реализации ленты и о своих эмоциях и переживаниях во время конкурса. Что я и сделал. Под катом советы и рекомендации по разработке новостной ленты в режиме сторителлинга.



О конкурсе


В первую очередь стоит рассказать немного о конкурсе. Как мне известно, компания ВКонтакте ежегодно устраивает подобные мероприятия для мобильных разработчиков. Сам я уже участвовал в 2012 и 2013 годах. Задачами были соответсвенно разработка чата и фильтров для изображений. В 2013 мне удалось дойти до финального раунда и и выиграть 100 000 рублей, что показалось мне тогда очень неплохой суммой за 4 выходных интересной работы.

И, увидев рекламу в социальной сети, я решил, что нужно попробовать, ведь самое крутое – это доказать себе, что ты ещё способен писать качественный код в очень сжатые сроки).
В конкурсе было 2 этапа: в первом предлагалось пройти тест из 30 вопросов, решить 2 олимпиадые задачки и одну качественную (т.е. решение – это набор твоих мыслей, оформленных в виде текста). Во второй раунд из 1000+ человек прошли всего 112 (речь идёт только про iOS, по Android чуть меньшие значения) и начался основной этап – разработка приложения.

По заданию нужно было сделать ленту новостей, интеграцию с API ВК писать самостоятельно, лента должна была быть очень плавной на всех устройствах, были определённые требования по выводу контента (посты с изображениями и каруселькой, счётчики лайков, шэров, количество просмотров, а также возможность сворачивать и разворачивать пост при определённых условиях).

Макеты были выложены в Figma. Для конкурса это хороший выбор – потому что по количеству активных пользователей сразу чувствуется конкуренция (а в субботу даже выбило, т.к. одновременный лимит в 50 подключений был превышен).

Вообще организация конкурса была гладкой за исключением нескольких моментов: сразу после публикации задания ссылка на макеты оказалась битой, далее как я уже сказал выше – в субботу вход в Figma был заблокирован (эту проблему очень быстро починили), ну и после окончания мероприятия достаточно долго пришлось ожидать результатов. Но весь негатив сглаживает подобный пост:


План победы и стратегия


Очень важный и интересный блок, если вы участвуете в конкурсах, либо хотели бы попробовать. Прежде чем участвовать рекомендую каждому задуматься какую цель вы преследуете и какой результат хочется получить на выходе. Для себя я решил ещё перед тем как приступить к конкурсу, что хочу его выиграть (т.е. занять призовое место). И для того, чтобы это сделать нужна стратегия:


Ниже приведена формула выигрышной стратегии (она, мне кажется, универсальная и простая, но уверен, что ей не следует и большинство участников):

  • Прочитайте задание очень внимательно. Через несколько часов прочитайте его ещё раз (я несколько раз попадал в ситуацию, когда не с первого раза воспринимал задачи). А за пару часов до сдачи ещё разок на всякий случай.
  • Составьте вопросы и задайте их организаторам (обычно требования к заданию не учитывают множество ситуаций, либо выставляются очень поверхностно). Я так и сделал, получив ответы в стиле «на ваше усмотрение».
  • Составьте план проекта. Да, участие в конкурсе или хакатоне – это выполнение проекта. У проекта есть цель, сроки, ресурсы и бюджет. Время строго ограничено, и двигать конец сдачи невозможно. Дополнительных людей приглашать нельзя – конкурс же не командный. Всё что у вас есть – это ваши собственные чел. / часы. Сразу определитесь сколько вы готовы потратить (подумайте хорошо сколько уйдёт на сон, еду, прокастинацию, перерывы, и да, ваша производительность будет падать ввиду усталости и нервов от ощущения беспомощности перед неумолимо приближающимися сроками).
  • В плане должны быть задачи, их приоритеты и веса (я использовал оценки вида 1, 2 ,4, 8). Оценка включает в себе длительность, нежелание её выполнять и потенциальные риски (сложность, непонятные условия, нет опыта подобной разработки и т.д.). Далее вы составляете подробный план (или roadmap задач) – сперва самые важные и сложные (оставьте лёгкие и понятные на конец).
  • Выберите несколько крупных интервалов или вех (отлично подходят дни – у нас было 3 дня). И при прохождении каждого из них смотрите на свой план работ, декомпозируйте задачки, сортируйте их, а главное – с наслаждением вычёркивайте те, которые уже сделали.
    Теперь совет (я именно так и поступил): внимательно посмотрите как сформулировано задание, есть ли в нём слова «обязательно» и «дополнительно». И какие критерии оценки будут у пунктов из «дополнительно», т.е. будут ли они перекрывать своим плюсом отсутствие реализованных требований из секции «обязательно». Я сомневаюсь. Поэтому концентрируйтесь на секции «обязательно». Я для себя вообще осознанно вычеркнул секцию «дополнительно» и даже не собирался к ней приступать.
  • При реализации будьте внимательны к деталям. Например, если в задании много интерфейсных форм – то делайте их так, как они отображены на макетах. Обратите внимание на отступы, шрифты, тени, межстрочный интервал, и т.д. Это несомненно прибавит вам очков за аккуратность и соответствия. Кстати, в Figma в отличие от Zeplin есть очень широкий экспорт настроек, например, можно получить неплохую NSAttributedString.

Задание было открыто в ночь с четверга на пятницу. Пятница – рабочий день и заниматься конкурсом на работе возможности и желания не было. Поэтому после возвращения с работы в пятницу вечером я впервую очередь занялся планированием, оценкой и высчитыванием сколько реального времени я смогу потратить.

Первоначальный план был следующим:
День Время Задачи
Пятница 4 часа Авторизация, получение данных для профиля и ленты.
Суббота 9 часов Прототип ленты с отображением красных квадратиков разной высоты, подгрузкой их сверху по pull-to-refresh и бесконечной загрузкой в глубину истории, подготовка всех моделей и сервисов (работа с API, запросное кеширование, кеширование изображений).
Воскресенье 9 часов Вывод реальных данных (текст, одиночные изображения и карусельки), счётчики лайков, просмотров и финальный тюнинг по макетам.

Техника реализации ленты


Ура! Господа разработчики вы нашли нужный блок. Здесь пойдёт речь про реализацию основной части задания – новостную ленту.

Вообще, если абстрагироваться, то новостная лента – это лишь прикладная реализация, обобщённо речь пойдёт про создание списка разнородных сущностей (т.е. у каждой может быть своё отображение, требующее дополнительных расчётов и своя высота, которая может изменяться динамически даже после отображения поста), плюс список умеет обновляться через pull-to-refresh и бесконечно подгружать данные снизу.

Именно общую задачу я и решил решать в первую очередь. Первый шаг – анализ существующих решений. Ориентироваться нужно на сильнейших, поэтому я выбрал 3 приложения для сравнения: Vkontakte, Facebook, Instagram.

Итак я хотел провести исследование 6-и самых острых и критичных проблем:

  1. Pull-to-refresh (добавление в начало списка)
  2. Плавность загрузки истории (добавление в конец списка)
  3. Fast scrolling (попеременно пальцами разгоняем ленту настолько, насколько позволит сила трения)
  4. Scroll to top (накапливаем большую историю и нажимаем на статус-бар)
  5. Есть ли динамическое раскрытие (увеличение) поста и какая при этом анимация
  6. Как работает лента в режиме почти полной потери пакетов (Developer –> Network Link Conditioner –> Very Bad Network)


В целом все приложения ведут себя хорошо, но несколько проблем я всё-таки подметил.
Посмотрите, например, как ведёт себя pull-to-refresh в ВКонтакте, если при обновлении не отпускать палец и аккуратно тянуть ленту вверх (см. на gif слева).

Вы же тоже видите этот скачок?

У Instagram и Facebook такого поведения не обнаружено.

А ещё заметная разница есть при раскрытии поста. У Facebook и Instagram это происходит с плавной анимацией, а ВКонтакте просто обновляет размер по нажатию.



Итак наша задача – сделать плавный скролл, загрузку постов, а также раскрытие с анимацией.

Первый шаг – выбор концепции и создание прототипа на красных квадратиках (интересно, почему я всегда интуитивно выбираю красный цвет для прототипов. Это у всех так?).
Основная моя идея в повышении производительности заключалась в отказе от всех наворотов и изысков, которые Apple вводила многие годы и буквальный откат до разработки под iOS 3. А это значит:

  • Отказ от AutoLayout (да, он медленный, не верите – могу доказать в комментариях)
  • Отказ от автоматического расчёта высоты ячеек таблицы

В итоге была выбрана следующая концепция:



Давайте чуть подробнее.

В главном потоке мы обновляем интерфейс, обрабатываем действия пользователя (скролл вниз и pull-to-refresh, клик по посту для его увеличения) и триггерим сервис-модель для получения и подготовки данных.

Обращение к API. Здесь всё просто – NSURLSession с настроенным NSURLCache. При этом важно, что при загрузке истории вниз, мы пользуемся кешем, а при pull-to-refresh мы его инвалидируем. Ровно такое поведение я подсмотрел у VK и Facebook.

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

Расчёт моделей представления. Самый важный шаг для оптимизации производительности. Здесь транспортные модели преобразуются в сущности с постфиксом ViewModel. ViewModel хранит в себе полностью подготовленные данные для отображения – AttributedString, посчитанную высоту ячеек (для 2-х состояний: свёрнутого и развёрнутого), ФИО в виде строки, строку с датой (уже преобразованную из DateFormatter).

Только после этого происходит возврат данных в главный поток. Реализовать подобную логику на Swift – очень удобно и просто. Делаем ViewModel структурой. Структуры при передаче на новый поток копируются.

Отлично концепт готов, теперь давайте поговорим про сам механизм вывода.

Сперва нужно было выбрать на чём реализовывать ленту – UITableView или UICollectionView (на свою реализацию точно бы не хватило отведенного времени). Очевидно, что UITableView подходит для вывода списка, но меня очень беспокоило не будет ли проблем с увеличением списка сверху, снизу, а также увеличением ячейки содержимого. Поэтому принял решение идти от простого к сложному – т.е. если у UITableView проблем не будет найдено – её и оставляем.

Первым делом я решил определиться с pull-to-refresh. Для реализации данного паттерна существует UIRefreshControl. Когда-то лет 5-6 назад я писал свою реализацию, используя UIActivityIndicator и изменяя contentInset у таблицы. Так вот, пожалуйста, не делайте так сейчас! UIRefreshControl имеет удобный компактный интерфейс и забирает тучу костылей, которые непременно придётся сделать. Использовать его очень просто:



Однако при его использовании становится понятно, откуда растут проблемы, показанные выше для клиента VK. Похоже что они существуют в самом компоненте. Быстро попробовал поискать в чём может быть причина и какие есть решения.

Советы из интернета говорят (такой или такой):

  • проверять при вызове endRefreshing обновляется ли сейчас контрол (isRefreshing)
  • использовать именно UITableViewController (потому что в этом случае вызывается магический приватный метод)

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

Следующий шаг – реализация подгрузки данных снизу.

Сперва у меня получилось не очень круто – при загрузке снизу был ужасный лаг (прыгал contentSize у таблицы видимо):


И это ад :-) С таким результатом точно не выиграть. Но быстрый поиск дал мне потрясающую подсказку, о которой я забыл:



И вуаля:


Осталось решить как анимировать высоту ячейки.

Первая идея, пришедшая в голову – бегать по visibleCells и в блоке анимации увеличивать высоту. Но ещё на стадии анализа эту идею стоит отбросить – проблема в том, что необходимо будет синхронизировать заданную в UITableViewDataSource высоту и что-то делать с contentSize (а это крайне не рекомендуется Apple).

Вторая мысль, пришедшая в голову, оказалось правильной – у UITableView есть методы insert / reload / delete, которые могут быть исполнены в блоке анимации:



Не забываем, что в heightForRowAt нам также нужно добавить новую высоту:



Кажется, что всё, но не совсем! В анимации разворачивания поста остался один тонкий момент. Посмотрите за текстом в Интаграмме или Facebook – он плавно появляется сразу при увеличении высоты. Что делать? Рендерить его построчно? Если дойти до уровня NSTextContainer, то возможно и появится подобная возможность. Но мне показалось, что не такая уж и плохая идея (за такое-то время) выводить весь текст сразу. Просто выставить clipToBounds у superView, в которой будет располагаться UILabel, выводящий наш текст. И подход сработал! Ох, данная анимация сильно меня воодушевило и открылось второе дыхание. Ведь такой анимации нет в нативном клиенте ВК. Значит шансов на победу она должна прибавлять :-)

Остальные детали при реализации ленты не так интересны. Но вы можете спросить их в комментариях. А ещё нет ничего страшного в выкладывании кода. Вот он – github.com/katleta3000/vkmobilechallenge. Простите, что но не причёсан и там местами есть Magic Numbers (но это же конкурс на скорость, чем-то пришлось пожертвовать). Кстати, там можно попрыгать по коммитам (названия достаточно понятные).

Результат можно посмотреть здесь – www.youtube.com/watch?v=Md8YiJxSW1M&feature=youtu.be (качество конечно сильно потерялось, но лучше чем в gif для демонстрации именно smooth scroll)


Статистика, результаты, выводы


Всё время конкурса работал по таймеру. Вышло 20 с половиной часов чистого времени кодинга. Трекинг времени помог в 2-х вещах: как вы помните были же оценки в абстрактных единицах, так вот к середине последнего дня уже была неплохая статистика по трекингу и можно было куда точнее распланировать оставшиеся финальные задачи. А, во-вторых, удалось выявить и доказать закономерность “концентрации за один присест”. Каждое измерение – это новая итерация, так вот получилось, что у меня было 68 итераций написания кода. Если усреднить, то получилось бы 18.5 минут. Но по факту, в первый день итерации были в среднем по 25 минут, а вот уже к концу второго дня по 7 :-) Начинаешь сходить с ума, сильно нервничать, устаёшь и производительность падает. Подобные данные хорошо помогут в следующий раз.

Лично я использовал программу Hourly(можно скачать и попробовать) – просто и решает необходимую задачу (да ещё и сам её разрабатываю):

Из приятных бонусов – за каждую закрытую задачку тебе показывают какой-то очень приятный подбадривающий экранчик типа таких:


Забавно, что именно при завершении задачки “VK Mobile Challenge” мне показался следующий экран:


И да! Так оно и произошло :-) Переходим к результатам.

Не будем тянуть Персика за плюш. Персик, кто не знал, это имя котика – главного персонажа ВК. Очень крутой подарочный мерч:


Я занял 4-е место и получил 175к рублей. (Да и вы, наверняка, спалили уже картинку на Хабра-катом). И да, я конечно же доволен. Я достиг своей цели :-) А это несомненно безумно приятно.

Напоследок хотел бы сказать спасибо vkontakte – всё-таки конкурс был крутым и отлично организованным. А всем читателям рекомендую принимать участие в челленджах и хакатонах – это отличный способ бросить вызов самому себе и посоревноваться с топовыми разработчиками по всему миру (ну или хотя бы русскоговорящему комьюнити).
Теги:
Хабы:
Всего голосов 23: ↑21 и ↓2+19
Комментарии25

Публикации

Ближайшие события