Sou o Guga Oliveira (@gugaoliveira), empreendedor, cursei engenharia eletrônica mas sempre trabalhei com softwares e programação. Abri uma empresa de desenvolvimento Web em Juiz de Fora no início de 2000, e em 2007 fundamos a Handcom focada em mobilidade. Trabalhamos com iOS desde o início, e criamos aplicativos em sua maioria focados no varejo e com foco empresarial. Em 2014 fundamos a startup Microlocation que foi acelerada pelo Seed-MG e hoje faz parte de nossa solução para o varejo Smart-Retail, atualmente estou estudando Swift e Análise de Dados, e participando bastante do desenvolvimento dos ecossistemas de Minas Gerais com o MGTI em BH e o Zer040 em Juiz de Fora.
interface assíncrona? sem Interface Builder?
Fui apresentado ao AsyncDisplayKit pelo Heberti Almeida, excelente dev iOS que conheci na WWDC de 2013. Ele estava usando o ASDK
no desenvolvimento do novo app da PostBeyond, startup em que ele trabalha. No começo achei complicado apostar em uma biblioteca de terceiros para substituir as bibliotecas próprias do iOS como UIKit
, XIBs e Storyboards.
Mas na WWDC 2016, o Heberti me convidou para ir no evento do Pinterest que acontecia em volta da WWDC. O evento foi promovido pelo Scott Goodsom @ScottGoodson, que é Head of Core Experience no Pinterest, e que idealizou a AsyncDisplayKit para desenvolver o Paper para o Facebook.
WWDC 2016 AsyncDisplayKit event
O que vi no evento me surpreendeu bastante, engenheiros de apps famosos utilizando intensamente a biblioteca, com foco total em performance visual. O Pinterest utiliza o AsyncDisplayKit e possui engenheiros trabalhando de forma obsessiva na engenharia de renderização das telas para que fiquem extremamente responsivas, com o mínimo drop de frames possível.
Como nosso aplicativo possui uma tela com muitas imagens de produtos e um fluxo quase infinito, como em uma timeline de produtos, resolvemos apostar na re-escrita com o AsyncDisplayKit nas próximas versões, além de quebrar um grande paradigma na empresa, que é o uso de Storyboard e Interface Builder.
Como é um framework complexo, e não por isso difícil de usar, este artigo pretende ser introdutório ao AsyncDisplayKit, tentarei complementar com assuntos mais avançados em breve.
AsyncDisplayKit
O AsyncDisplayKit é uma framework Open Source iOS feito no topo do UIKit com o objetivo de deixar até as interfaces visuais mais complexas com fluxo suave e responsivo. O Framework é Open Source e está disponível via CocoaPods ou Carthage.
O UIKit
é um dos frameworks mais maduros do iOS, e foi desenvolvido quando os iPhones tinham recursos restritos de sistema, no início nem existia possibilidade de utilizar códigos assíncronos concorrentes. O problema é que mesmo atualmente o UIKit
roda em uma única thread e só funciona na main queue. Na maior parte dos casos funciona bem, mas quando envolve interfaces mais complexas ou necessita-se de mais recursos, a interface acaba não sendo tão responsiva. Para estes casos, trabalhar de forma assíncrona é extremamente necessário para criar uma interface lisa e responsiva com o usuário.
O iOS renderiza a interface com o usuário em 60 frames por segundo, o que nos dá apenas 16 milisegundos de CPU por frame, e caso você ultrapasse este mínimo intervalo de tempo o sistema operacional vai começar a liberar drop de frames imediatamente, tornando nossa interface muito menos responsiva e suave, principalmente agora que muitos apps utilizam de animações e efeitos para obter a atenção do usuário.
E nos tempos de autolayout e animações, existem várias tarefas para a main thread realizar e consumir o pequeno tempo de frame que temos, como calcular a dimensão automática de cada célula, resolver os constraints dos conteúdos das views, renderizar e decodificar imagens, criar sombras e contornos, efeitos como blur e shadow, redimensionamento de imagens para exibição e criação e manipulação de objetos do sistema.
O ASDK
(AsyncDisplayKit) chegou para resolver esses problemas, ele move o máximo de trabalho de UI
para ser executado em background. Por default ele permite que todas os cálculos de medidas, estruturações layout e renderizações sejam feitas de forma assíncrona, sem muitas otimizações um app pode experimentar uma grande redução do trabalho realizado na main thread.
O ASDK
é incompatível com Interface Builder e Auto Layout, a biblioteca foi escrita em Objective-C, mas o ASDK
suporta completamente o Swift .
Começando
A unidade fundamental do ASDK
é o node
ou ASDisplayNode
, ele é uma abstração da UIView
, que por conseguinte é uma abstração da CALayer
. A UIView
só pode ser manipulada na main thread, mas os nodes
são thread safe e podem ser instanciados e ter toda sua hierarquia configurada em background .
A idéia básica do ASDK
é que você possa trabalhar com os nodes
da mesma forma como já trabalha com as views
, a grande maioria dos métodos e parâmetros da UIView
e da CALayer
possuem equivalentes nos nodes
.
Tudo o que aparece na tela do iOS é renderizado por um objeto CALayer
, a UIView
cria e mantém a referência do objeto CALayer
, e os nodes
extendem a UIView
da mesma forma, como na imagem acima. Você pode acessar a view e o layer que estão abaixo diretamente com node.view
e node.layer
, desde que esteja na main thread.
Containers de nodes
Os nodes
do ASDK
devem ser usados dentro de containers de nodes
, o ASDK
oferece alguns containers que tem similares com classes do UIKit
que estamos acostumados a trabalhar. Um node
não deve ser adicionado diretamente a uma hirerarquia de view
(se fizer isso é quase certo de você obter piscadas de tela). Um node
deve ser adicionado como subnode de um container de nodes. Esses containers possuem a tarefa de gerenciar os subnodes para informá-los se está tudo pronto para renderizar na tela de forma mais eficiente possível.
Uma vantagem do uso dos containers de nodes
é que o container automaticamente gerencia o pre-loading inteligente (Inteligent Preloading) dos nodes
(views
), que significa que toda tarefa de medição do layout, de carregamento de dados, decodificação e renderização serão feitos de forma assíncrona automaticamente.
Os principais containers estão abaixo com os equivalentes no UIKit
:
Container ASDK |
Equivalente UIKit |
---|---|
ASViewController |
UIViewController |
ASTableNode |
UITableView |
ASCollectionNode |
UICollectionView |
ASPagerNode |
UIPageViewController |
ASNavigationController |
UINavigationController |
ASTabBarController |
UITabBarController |
Layout Engine
A Layout Engine do ASDK
é muito poderosa e oferece recursos únicos, ela é baseada no CSS Box Model
, e fornece formas de declarar e especificar o tamanho e posição dos subnodes
. Os nodes
(views
) são renderizados de forma concorrente por default, mas os cálculos de medidas e posição são executados de forma assíncrona ao se usar um ASLayoutSpec
para cada node
. Aqui está a maior diferença entre o ASDK
e o UIKit
, no ASDK
o layout é declarativo e no UIKit
é baseado em constraint com o Auto Layout
.
A Layout Engine será assunto de um segundo artigo que sairá em breve, no entanto no fim do artigo deixarei referências.
Instalando
O ASDK
pode ser adicionado ao seu projeto via CocoaPods ou Carthage.
CocoaPods
Adicione o seguinte comando no seu Podfile
:
target 'MyApp' do
pod "AsyncDisplayKit"
end
Saia do XCode
, rode o comando dentro do diretório do projeto no Terminal:
> pod install
Para atualizar a versão do ASDK
, rode o comando dentro do diretório do projeto no Terminal:
> pod update AsyncDisplayKit
Carthage
Adicione o seguinte comando no seu Cartfile
para usar a última versão do ASDK
:
github "facebook/AsyncDisplayKit"
Ou para usar a branch master:
github "facebook/AsyncDisplayKit" "master"
No terminal rode:
> carthage update
Verifique no terminal se AsyncDisplayKit
, PINRemoteImage
and PINCache
foram carregadas e compiladas.
importando o framework
Importe o header do framework para usá-lo
#import <AsyncDisplayKit/AsyncDisplayKit.h>
Para usar com swift
deve-se criar um bridge header.
Mão na massa
A melhor maneira para iniciar é explorar os projetos de exemplo existentes no GitHub do ASDK
. A pasta examples possui diversos exemplos, inclusive um simulando o Instagram chamado ASDKgram.
Vamos usar o exemplo ASViewController em Objective-c para nosso exemplo:
Para criar uma equivalente de ViewController
vamos usar o container ASViewController
@interface ViewController : ASViewController
Nossa ViewController
contem uma TableView
que irá exibir as categorias de Imagens disponíveis para visualização, vamos então implementar a TableView
usando o ASTableNode
.
Declarando o array que contem os nomes da categoria e o node
que reprensenta a TableView
:
@property (nonatomic, copy) NSArray *imageCategories;
@property (nonatomic, strong, readonly) ASTableNode *tableNode;
Vamos configurar a ViewController
, iniciando a TableNode
e o array
usando o initWithNode
:
- (instancetype)init
{
self = [super initWithNode:[ASTableNode new]];
if (self == nil) { return self; }
_imageCategories = @[@"abstract", @"animals", @"business", @"cats", @"city", @"food", @"nightlife", @"fashion", @"people", @"nature", @"sports", @"technics", @"transport"];
return self;
}
Assim como na TableView
para usar o ASTableNode
deveremos nos conformar ao delegate
e ao datasource
da TableNode
....
@interface ViewController () <ASTableDataSource, ASTableDelegate>
.....
- (void)viewDidLoad
{
[super viewDidLoad];
...
self.node.delegate = self;
self.node.dataSource = self;
}
....
- (void)dealloc
{
self.node.delegate = nil;
self.node.dataSource = nil;
}
Agora vamos implementar os delegates
e datasource
da TableNode
- (NSInteger)tableNode:(ASTableNode *)tableNode numberOfRowsInSection:(NSInteger)section
{
return self.imageCategories.count;
}
- (ASCellNodeBlock)tableNode:(ASTableNode *)tableNode nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Como o bloco é executado em outra thread em background temos que armazenar a imageCategory fora do bloco
NSString *imageCategory = self.imageCategories[indexPath.row];
return ^{
ASTextCellNode *textCellNode = [ASTextCellNode new];
textCellNode.text = [imageCategory capitalizedString];
return textCellNode;
};
}
- (void)tableNode:(ASTableNode *)tableNode didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *imageCategory = self.imageCategories[indexPath.row];
DetailRootNode *detailRootNode = [[DetailRootNode alloc] initWithImageCategory:imageCategory];
DetailViewController *detailViewController = [[DetailViewController alloc] initWithNode:detailRootNode];
detailViewController.title = [imageCategory capitalizedString];
[self.navigationController pushViewController:detailViewController animated:YES];
}
Reparem que os métodos são bem similares aos da UITableView
, a ASTextCellNode
é uma subclasse de ASCellNode
, que é o equivalente do ASDK
a UITableViewCell
ou UICollectionViewCell
. No caso a ASTextCellNode
possui um label simples para exibição de texto. Mas existe uma diferença no método de exibição da célula.
//UITableView
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
//ASTableNode
- (ASCellNodeBlock)tableNode:(ASTableNode *)tableNode nodeBlockForRowAtIndexPath:(NSIndexPath *)indexPath
A diferença aqui é que o retorno é um bloco de código que pode ser executado em background pelo ASDK
, neste caso a variável indexPath
não pode ser usada dentro do bloco, pois os dados podem mudar antes do bloco ser executado. Não é necessário reusar as celulas, o ASDK
faz o trabalho para você, basta iniciar o ASCellNode
e retorná-lo ao fim do bloco.
Ao se selecionar uma linha da TableNode
instanciamos a classe DetailRootNode, que é uma subclasse de ASDisplayNode
(o node fundamental do ASDK
), com o inicializador que recebe o nome da categoria de imagens, depois instanciamos a DetailViewController que é uma subclasse de ASViewController
com o node
DetailRootControler
Na DetailRootNode temos uma instância de ASCollectionNode
o container de nodes
que é equivalente a UIViewController
. Aqui vamos usar a capacidade de um node
gerenciar os subnodes
, automaticamente o ASDK
irá adicionar a CollectionView
à view DetailRootNode que é adicionada à DetailViewController.
// DetailRootNode é um ASDisplayNode
@interface DetailRootNode : ASDisplayNode
//o ASDisplayNode possui uma ASCollectionNode (equivalente UIViewCollection)
@property (nonatomic, strong, readonly) ASCollectionNode *collectionNode;
....
// conforma-se os delegates e datasource da ASCollectionView
@interface DetailRootNode () <ASCollectionDataSource, ASCollectionDelegate>
....
- (instancetype)initWithImageCategory:(NSString *)imageCategory
{
self = [super init];
if (self) {
// Habilita o gerenciamento automatico dos subnodes todos os nodes que
// estiverem referenciados no metodo layoutSpecThatFits: serão adicionados automaticamente
self.automaticallyManagesSubnodes = YES;
_imageCategory = imageCategory;
// Cria a ASCollectionView. Não é necessário adicionar como subnode explicitamente por que iremos configurar o parametro usesImplicitHierarchyManagement para YES
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
_collectionNode = [[ASCollectionNode alloc] initWithCollectionViewLayout:layout];
_collectionNode.delegate = self;
_collectionNode.dataSource = self;
_collectionNode.backgroundColor = [UIColor whiteColor];
}
return self;
}
...
// adiciona e calcula automaticamente as dimensoes do DisplayNode com a CollectionView
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
{
return [ASWrapperLayoutSpec wrapperWithLayoutElement:self.collectionNode];
}
...
#pragma mark - ASCollectionDataSource
- (NSInteger)collectionNode:(ASCollectionNode *)collectionNode numberOfItemsInSection:(NSInteger)section
{
return 10;
}
- (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath
{
NSString *imageCategory = self.imageCategory;
return ^{
DetailCellNode *node = [[DetailCellNode alloc] init];
node.row = indexPath.row;
node.imageCategory = imageCategory;
return node;
};
}
- (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode constrainedSizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
CGSize imageSize = CGSizeMake(CGRectGetWidth(collectionNode.view.frame), kImageHeight);
return ASSizeRangeMake(imageSize, imageSize);
}
Como podem perceber a ViewController
DetailViewController é criada com uma view que contem uma CollectionView
, esta carrega 10 imagens de uma api em cada célula. A DetailCellNode é uma CollectionViewCell que é configurada em um bloco carregado em background pelo ASDK
após as imagens serem carregadas. A UI
em nenhum momento deixa de ser responsível ou fluida.
Terminando
Este é um exemplo simples que demonstra como é intuitivo e fácil trabalhar com o ASDK
, existem muitos tópicos avançados de como melhorar ainda mais a performance. Neste artigo não pudemos falar sobre Layout, Inteligent Preloading e outras conveniências. Ele serve como um start para a avaliação da biblioteca como substituta do UIKit para criar interfaces mais fluidas e responsivas.
Agradeço a leitura e fico aberto a sugestões e críticas, este foi o meu primeiro artigo de desenvolvimento, e pretendo escrever mais.
Seguem meus contatos:
- Twitter (@gugaoliveira)
- Slack do iOSDevBR (@gugaoliveira).
Agradecimentos especiais ao Heberti Almeida, que já utiliza o ASDK
há bastante tempo e me apresentou esta biblioteca fantástica e também seus criadores, foi realmente bacana poder conversar com os criadores e ter uma canal de comunicação direta.
Referências
- AsyncDisplayKit Getting Starded
- Using AsyncDisplayKit to Develop Responsive UIs in iOS [Ziad Tamim, 29/12/2016]
- AsyncDisplayKit 2.0 Tutorial: Getting Started [Luke Parham, 5/12/2016]
- Slack do AsyncDisplayKit
Referências de Layout
- AsyncDisplayKit 2.0 Tutorial: Automatic Layout [Luke Parham, 19/12/2016]
- Layout at Scale with AsyncDisplayKit 2.0 [NSMeetup 2016]
- ASStackLayout Game