«Искусство – это все, что вы можете сделать хорошо. Все, что вы можете сделать качественно» (Robert M. Pirsig). AsyncDisplayKit – это UI фреймворк, который был разработан и применен Facebook в приложении Paper. Он помог команде разработчиков Paper найти ответ на один из ключевых вопросов: как сохранить основной поток приложения максимально свободным. Сейчас многие приложения основываются на пользовательском интерфейсе, который в значительной степени зависит от непрерывных движений и анимации на основе физики. По крайней мере, ваш UI, наверняка, базируется на каком-то из видов Scroll View. Такие типы пользовательских интерфейсов полностью зависят от основного потока и чрезвычайно чувствительны к его тормозам. Нагруженный основной поток означает отброшенные фреймы и неприятный пользовательский опыт. Основные факторы работы потока включают: При правильном использовании AsyncDisplayKit позволяет выполнять все измерения, компоновку и рендеринг асинхронно по умолчанию. Без дополнительной оптимизации приложение может примерно на порядок уменьшить объем работ, выполняемых в основном потоке. Кроме этих преимуществ в производительности, современный AsyncDisplayKit предлагает впечатляющий набор удобств для разработчиков, позволяющий реализовывать сложные интерфейсы с минимальным количеством кода.  В этом туториале AsyncDisplayKit 2.0, состоящем из двух частей, вы узнаете всё необходимое для создания полезного и динамического приложения с использованием ASDK.  Для начала загрузите стартовый проект.
Проект использует CocoaPods, чтобы подключить AsyncDisplayKit. Итак, в обычном стиле CocoaPods, зайдите и откройте RainforestStarter.xcworkspace, но НЕ RainforestStarter.xcodeproj. Скомпилируйте и запустите приложение, состоящее из одной UITableView, содержащей список животных. Если вы посмотрите на код в AnimalTableController, то увидите, что это обычный класс UITableViewController, который вы, наверное, видели много раз. Проскрольте таблицу и обратите внимание на количество фреймов, которые были пропущены. Не нужно запускать инструменты, чтобы увидеть, что это приложение нуждается в улучшении производительности.  Вы можете исправить это, используя AsyncDisplayKit. ASDisplayNode – базовый класс ASDK и, по сути, просто объект «view» в паттерне MVC, аналогично UIView или CALayer. Лучший способ представить ASDisplayNode – подумать об отношении между UIViews и CALayers, с которыми вы уже должны быть знакомы. Помните, что на экране в iOS-приложении всё представлено через объект CALayer. UIViews создают и владеют поддержкой CALayer, к которому они добавляют сенсорную обработку и другие функции. Сам класс UIView не является подклассом CALayer. Вместо этого он оборачивает объект слоя, расширяя его функциональность.  В случае ASDisplayNode эта абстракция расширяется: вы можете думать о них как об обертывании представления так же, как представление заворачивает слой.
Что узлы приносят в таблицу кроме обычного представления, так это то, что они могут быть созданы и сконфигурированы в фоновых очередях и одновременно представлены по умолчанию.  К счастью, API для работы с узлами должен быть невероятно знакомым любому, кто использовал UIViews или CALayers. Все свойства view, которые вы обычно используете, доступны в эквивалентном классе узла. Вы можете получить даже доступ к самой вью или лееру, так же, как к .layer класса UIView. Хотя сами по себе узлы обеспечивают возможность значительного повышения производительности, настоящая магия происходит, когда они используются совместно с одним из четырех контейнерных классов. К этим классам относятся:  Если честно, настоящая магия исходит от ASRangeController, который использует каждый из этих классов, чтобы повлиять на поведение содержащихся узлов. А пока просто доверьтесь мне и запомните это на будущее. Первое, что вы должны сделать – это преобразовать текущее представление таблицы в узел таблицы. Это довольно просто. Сначала перейдите к AnimalTableController.m. Добавьте следующую строку ниже других import в этом классе:  Так вы импортируете ASDK, чтобы использовать фреймворк.
Затем перейдите и замените объявление свойства tableView: на tableNode: Это приведет к тому, что большинство кода в этом классе сломается, но не паникуйте!  Серьезно, не переживайте. Эти ошибки и предупреждения будут служить вашим ориентиром при преобразовании того, что сейчас есть, в то, что вы хотите. Ошибки в -viewDidLoad, конечно, связаны с тем, что tableView больше не существует. Я не собираюсь заставлять вас просматривать весь проект и изменять все экземпляры tableView на tableNode (найти и заменить не так уж и сложно, поэтому не бойтесь), но если вы это сделали, то увидите, что: На этом этапе вы должны просто заменить -viewDidLoad на следующее: Интересно, что вы вызываете -addSubnode: для UIView. Этот метод был добавлен ко всем UIViews через категорию и в точности эквивалентен: Затем исправьте -viewWillLayoutSubviews, замените реализацию этого метода на следующую: Все это заменяет self.tableView на self.tableNode для установки фрейма таблицы.
Затем найдите метод -applyStyle и замените реализацию следующим: Строка, которая задает separatorStyle для таблицы – единственная измененная строка. Обратите внимание, как осуществляется доступ к свойству представления табличного узла, чтобы установить separatorStyle для таблицы. ASTableNode не раскрывает все свойства UITableView, поэтому вам нужно получить доступ к экземпляру UITableView базового узла таблицы, чтобы изменить определенные свойства UITableView. Затем добавьте следующую строку в самом начале -initWithAnimals: И добавьте этот код в конец, перед оператором return инициализатора: Так вы инициализируете AnimalTableController с узлом таблицы и вызовете -wireDelegation для подключения делегатов узла таблицы. Как и UITableView, ASTableNode использует источник данных и делегат для получения информации о себе. Протоколы ASTableDataSource таблицы Table и ASTableDelegate очень похожи на UITableViewDataSource и UITableViewDelegate. Фактически, они определяют некоторые из тех же самых методов, таких как -tableNode: numberOfRowsInSection:. Два набора протоколов не соответствуют друг другу полностью, потому что ASTableNode ведет себя немного иначе, чем UITableView. Найдите -wireDelegation и замените tableView на tableNode в реализации:  Теперь вы получите warning, что AnimalTableController на самом деле не соответствует правильному протоколу. Сейчас AnimalTableController соответствует UITableViewDataSource и UITableViewDelegate. В следующих разделах вы будете принимать и реализовывать каждый из этих протоколов, чтобы узел таблицы мог функционировать. В верхней части AnimalTableController.m найдите следующее объявление интерфейса категории DataSource: И замените UITableViewDataSource на ASTableDataSource: Теперь, когда объявление AnimalTableController соответствует ASTableDataSource, пришло время сделать это.  Перейдите в нижнюю часть AnimalTableController.m и найдите реализацию категории DataSource.
Сначала измените UITableViewDataSource метод -tableView: numberOfRowsInSection: на метод протокола ASTableDataSource, заменив его следующим. Далее ASTableNodes ожидает, что его ячейки будут возвращены по-другому, нежели ячейки UITableView. Чтобы приспособиться к новой парадигме, замените -tableView: cellForRowAtIndexPath: на следующий метод: Рассмотрим этот раздел: Это подводит меня к важному моменту. Как вы, возможно, уже заметили, при использовании ASDK нет переиспользования ячеек. Об этом уже дважды говорилось, но просто имейте это ввиду. Не бойтесь перейти в начало класса и удалить. Вам больше это не нужно. Только подумайте. Вам никогда не придется волноваться о -prepareForReuse снова … Перейдите в начало AnimalTableController.m и найдите следующее объявление интерфейса категории Делегата: и замените UITableViewDelegate на ASTableDelegate: Теперь, когда AnimalTableController принимает ASTableDelegate, пора перейти к реализации. Переместитесь к нижней части AnimalTableController.m и найдите реализацию этой категории Делегата. Уверены, вы знаете, что с UITableView вам нужно обеспечить реализацию -tableView:heightForRowAtIndexPath:. Это происходит из-за того, что с UIKit, высота каждой ячейки вычисляется и возвращается делегатом таблицы. ASTableDelegate не нуждается в -tableView:heightForRowAtIndexPath:. В ASDK все ASCellNodes ответственны за определение их собственного размера. Вместо того, чтобы обеспечивать статическую высоту, вы можете дополнительно определить минимальный и максимальный размер для своих ячеек. В данном случае нужно, чтобы каждая ячейка была как минимум так же высока, как 2/3 экрана. Сейчас просто замените -tableView:heightForRowAtIndexPath: на: Скомпилируйте и запустите проект, чтобы увидеть, что получилось. У нас получилась плавная таблица! Как только вы сделали это, подготовьтесь сделать еще лучше. В большинстве приложений сервер имеет больше возможных данных, чем количество ячеек, которые вы хотели бы показать в таблице. Это означает, что в каждом приложении, над которым вы работаете, будет установлен механизм для загрузки следующего пакета данных от сервера, потому что пользователь приближается к концу текущего набора данных. Много раз это обрабатывалось вручную, запоминая смещение контента в делегате scrollView в методе -scrollViewDidScroll:. С ASDK есть более декларативный способ сделать это. Вы можете сказать заранее, сколько страниц нужно для загрузки новых данных.  Сперва раскомментируйте вспомогательные методы, которые были включены. Перейдите в конец AnimalTableController.m и раскомментируйте эти два метода в категории Helpers. Вы можете считать -retrieveNextPageWithCompletion: сетевым вызовом, в то время как -insertNewRowsInTableNode – стандартный метод для добавления новых элементов в таблицу. Затем добавьте следующую строку к -viewDidLoad:. Установка leadingScreensForBatching в 1.0 означает, что вы хотите, чтобы новые пакеты были загружены каждый раз, когда пользователь прокрутил к точке, где до конца таблицы остался лишь 1 элемент. Теперь добавьте следующий метод к реализации категории Делегата: Этот метод нужен для того, чтобы сказать таблице, должна ли она продолжить выполнять запросы для загрузки новых пакетов. Если вы знаете, что загружать больше нечего – верните NO.
Если вы действительно хотите, чтобы эта таблица скроллилась бесконечно, просто верните YES, чтобы гарантировать, что всегда будут запрашиваться новые пакеты. Затем также добавьте: Этот метод вызовется, когда пользователь приблизится к концу таблицы, и таблица получит YES от -shouldBatchFetchForTableNode:.  Давайте рассмотрим этот раздел:  Вы когда-нибудь пробовали загружать контент заранее в приложении с помощью scrollView или PageViewController? Возможно, вы работали над полноэкранной фото галереей и захотели, чтобы следующие несколько изображений были загружены и ожидали, а пользователи редко видели placeholder.  Когда вы работаете над такой системой, как эта, начинаете осознавать, что есть много вещей, о — которых стоит подумать.  В каждом из контейнерных классов есть понятие состояния интерфейса для каждого из узлов. В любой момент времени узел может быть в любой комбинации: Эти диапазоны также работают с метрикой «screenfuls» и могут быть легко настроены с помощью свойства ASRangeTuningParameters. Например, вы используете ASNetworkImageNode для отображения изображения на каждой странице галереи. Каждый узел запрашивает данные из сети, когда входит в диапазон предварительной загрузки, и декодирует изображение, которое было извлечено, когда оно входит в диапазон отображения. На самом деле, вы не должны постоянно думать об этих диапазонах, если не хотите. Встроенные компоненты, такие как ASNetworkImageNode и ASTextNode, в полной мере используют их. Это означает, что вы увидите преимущества по умолчанию. Возможно, вам интересно, как эти диапазоны правильно работают? Хорошо, что спросили.
У каждого узла в системе есть interfaceState свойство, которое является типом «битового поля» (NS_OPTION) ASInterfaceState. Поскольку ASCellNode перемещается через представление прокрутки, которым управляет ASRangeController, каждый подузел обновляет interfaceState свойство. Это значит, что даже самые глубокие узлы в дереве могут реагировать на изменения interfaceState. К счастью, редко приходится возиться с битами interfaceState узла напрямую. Чаще всего вы просто хотите отреагировать на переход узла в определенное состояние или выход из него. Вот тут-то и появляются обратные вызовы состояния интерфейса.  Чтобы увидеть, как узел переходит из одного состояния в другое, полезно дать ему имя. Таким образом, вы сможете наблюдать, как каждый узел загружает свои данные, отображает их содержимое, появляется на экране, а затем делает всё это наоборот, когда исчезает.
Вернитесь к -tableNode:nodeBlockForRowAtIndexPath: и найдите комментарий, в котором говорится: Ниже добавьте следующую строку, чтобы дать каждой ячейке debugName. Теперь вы сможете отслеживать перемещение ячеек через диапазоны. Перейдите в CardNode_InterfaceCallbacks.m. Здесь вы найдете шесть методов, которые сможете использовать для отслеживания прогресса узла через различные диапазоны. Раскомментируйте их, а затем скомпилируйте и запустите. Удостоверьтесь, что ваша консоль в Xcode видна, и затем медленно прокручивайте. Смотрите, как различные ячейки реагируют на свои изменяющиеся состояния.  В версии 2.0 была введена концепция интеллектуальной предварительной загрузки контента по нескольким направлениям. Скажем, у вас есть вертикально прокручиваемое представление таблицы, и в какой-то момент на экране появляется ячейка, содержащая горизонтальный вид коллекции.  Хотя эта коллекция сейчас технически находится в видимой области, вам не хотелось бы загружать всю коллекцию заранее. Вместо этого оба вида прокрутки имеют свой собственный ASRangeController с отдельными настраиваемыми параметрами настройки диапазона. Теперь, когда вы завершили AnimalTableController, вы можете использовать его как страницу в ASPagerNode. Контроллер представления, который вы будете использовать для размещения этого пейджера, уже находится в проекте, поэтому первое, что вам нужно сделать, это перейти в AppDelegate.m.
Найдите -installRootViewController и замените: на: Затем перейдите в AnimalPagerController.m и добавьте следующие строки в инициализатор, прямо перед оператором return. Все, что вам нужно сделать, это создать новый пейджер и установить его dataSource в качестве этого контроллера представления. Узел пейджера фактически является подклассом ASCollectionNode, предварительно сконфигурированным для использования таким же образом, как и UIPageViewController. Хорошо, что думать об API на самом деле немного проще, чем о UIPageViewController. Следующее, что вам нужно сделать, это реализовать методы источника данных пейджера. Перейдите к реализации категории ASPagerDataSource в конец этого файла. Сначала скажите пейджеру, что число его страниц равно количеству массивов животных, в данном случае, три, заменив существующий -numberOfPagesInPagerNode:. Затем вам нужно реализовать -pagerNode:nodeAtIndex: аналогично методу источника данных узла, который вы ранее реализовали для ASTableNode. Давайте рассмотрим этот раздел: После добавления этого метода, у вас будет полностью функционирующий пейджер, ячейки которого будут сгенерированы из созданного ранее tableNodeController. Это полностью поставляется с двухмерной предварительной загрузкой, основанной на вертикальной и горизонтальной прокрутке, выполняемой пользователем!  Полную версию проекта для этого туториала по AsyncDisplayKit 2.0 можно скачать здесь. У нас также есть вариант на Swift.  Скоро будет готов перевод 2 части этого проекта. Она позволит узнать о новой, мощной системе компоновки, представленной в AsyncDisplayKit 2.0. Можете провести небольшое исследование, прежде чем двигаться дальше: откройте домашнюю страницу AsyncDisplayKit и прочитайте часть документации. У Скотта Гудсона (Scott Goodson, оригинального автора AsyncDisplayKit) также есть несколько лекций, которые могут вас заинтересовать, самые новые из которых дают хороший обзор нескольких больших проблем с изображениями, которые фреймворк пытается решить. Возможно, вам будет интересно Building Paper. Хотя в то время все это не было open sourse, и многое изменилось, довольно интересно посмотреть, с чего все это начиналось. Наконец, сообщество AsyncDisplayKit приветствует вновь прибывших. Есть общедоступный канал в Slack, в который любой может вступить и задать вопросы. Надеемся, вам понравился этот туториал, сообщите, есть ли у какие-либо вопросы или комментарии, присоединившись к обсуждению на форуме. От переводчика:
В апреле разработчики переименовали фреймворк. Теперь он называется Texture. Подробнее можно прочитать тут.  P.S. Отдельное спасибо BeataSunshine и evgen за помощь в переводе статьи.