Desenvolvimento

23 ago, 2017

Não sejamos reféns de Storyboard – Parte 2

Publicidade

Não sejamos reféns de Storyboard

*Este artigo foi originalmente publicado no Medium pessoal do autor. Confira aqui.

No ano passado, publiquei o artigo Reféns de Storyboard, no qual contei um pouco da minha experiência explorando behavior e custom views, fora dessa (que pode se tornar) areia movediça chamada Storyboard.

Em praticamente todas as consultorias (e aqui na Concrete não é diferente), a rotatividade entre projetos é bem grande, muito pelo fato de atuarmos em praticamente todas as frentes do produto, não apenas no desenvolvimento, mas por todo o ciclo de vida do projeto.

Este cenário oferece a todos uma oportunidade muito interessante de evoluir e atuar em diversos segmentos do mercado, o que é bem bacana para o crescimento de qualquer profissional do ramo.

Ok, o que esta introdução tem a ver?

Este ano, 2017, foi minha vez de entrar em um novo projeto. Conceitos novos, estrutura nova, mindset novo, enfim, deixei um pouco de lado meu amado Objective C ❤ e entrei de cabeça no nosso mais novo almighty Swift.

Ah, mas não foi apenas isso que o projeto trouxe de interessante. Cheguei em um momento bem bacana, no qual o time decidiu exterminar para sempre XIB’s e Storyboards do projeto, trazendo uma nova abordagem bem interessante, que me motivou a escrever a parte 2 deste artigo.

Chega de se preocupar com “Massive Storyboards”, “Unused Xibs”, “XIB ou Storyboard?”, “Lazy build”, “Merge Conflicts”. Aliás, este último carinha aí, pra mim, já vale a mudança. Quem lembra como resolver conflito em Storyboard ou XIB? Não resolve. Simples assim. Checkout no mais atual e ajusta o que precisar… sad, but true.

Nossa única preocupação será com:

CÓDIGO \m/

Código e Architecture Patterns, as always…

Ok, chega de conversa e vamos à prática. Neste artigo, vou abordar 3 passos essenciais para começar a utilizar View Code em seus projetos mantendo sempre uma boa organização semântica de código e coerência dentro do projeto. Nosso setup sempre vai passar por estes 3 passos:

  • Configuração: vamos configurar todos os nossos elementos, como por exemplo, definir cor, tamanho e texto em um UILabel, estilo e comportamento de um UIButton, background, formas e estilos em uma UIView, entre outras coisas;
  • Hierarquia: vamos adicionar todos os elementos na tela, respeitando sempre a hierarquia de cada elemento, seguindo a mesma linha que faríamos no Interface Builder;
  • Autolayout: por último, não menos importante, vamos definir as constraints. É aqui que nosso layout ganhará forma e cada coisa irá se organizar em seu lugar.

 

Não sejamos reféns de Storyboard

Configuração

Como mencionado, aqui vamos configurar o layout e estados iniciais dos nossos elementos. Para este exemplo, vamos trabalhar apenas com 3 deles: UILabel, UIImageView e UIView.

Mais pro fim vou deixar algumas dicas interessantes de como obter bons resultados em layouts mais complexos.

func configViews() {
 self.labelTitle.font = UIFont(name: "Arial", size: 18)
 self.labelTitle.textColor = UIColor.darkGray
 self.labelTitle.textAlignment = .center
 self.labelTitle.numberOfLines = 0
 self.labelTitle.text = "Hello World View Code!"
 
 self.labelDescription.font = UIFont(name: "Arial", size: 12)
 self.labelDescription.textColor = UIColor.gray
 self.labelDescription.textAlignment = .left
 self.labelDescription.numberOfLines = 0
 self.labelDescription.text = "Hello World View Code! \nO que é? Onde vive? Como se alimenta?"
 
 self.imageView.image = UIImage(named: "viewCodeSample")
}

No código acima, definimos alguns estilos para o título, descrição e uma imagem para ser apresentada.

Neste método, podemos incluir também configurações de background, custom layers, estilo de botões, enfim, tudo relacionado a como cada elemento se comportará visualmente.

Uma dica bacana é sempre tentar agrupar as configurações por tipo. Veja que primeiro defini os estilos para a UILabel para depois definir o da UIImageView, independente de como serão apresentados na tela. Isso torna o bloco de código mais legível e cria um padrão facilitando a localização de cada item.

Hierarquia

Neste step, vamos adicionar os elementos à tela. Lembra lá no interface builder, quando escolhíamos o elemento no Object Library e arrastávamos para uma View específica?

func buildViews() {
 self.view.addSubview(self.content)
 
 self.content.addSubview(self.labelTitle)
 self.content.addSubview(self.imageView)
 self.content.addSubview(self.labelDescription)
}

No código funciona da mesma forma, e neste passo é muito importante tentar imaginar como faríamos utilizando o Interface Builder. A ordem que você chama o addSuview influencia diretamente na hierarquia dos elementos, vai influenciar mais ainda quando formos configurar as constraints que irão compor o Auto Layout e influencia ainda mais na ordem em que a tela será lida com a acessibilidade ligada.

Com isso, a hierarquia se apresenta da seguinte forma:

UIView(view)
— UIView(content)
— — UILabel(labelTitle)
— — UIImageView(imageView)
— — (UILabel(labelDescription)

Se formos imaginar o Interface Builder, o document outline teria esta configuração:

Não sejamos reféns de Storyboard

Autolayout

Aqui vamos finalizar nosso setup, definindo todas as contraints para os elementos que adicionamos.

Podemos fazer isso no modo raiz, mas, para nosso exemplo vou fazer no modo nutella mesmo, utilizando uma dependência muito bacana chamada SnapKit.

Sim, modo nutella, mas nada menos eficiente. Aliás, já vou aproveitar para linkar com uma das dicas que vou colocar mais para o final: Abstração.

func setupConstraints() {
 self.content.snp.makeConstraints { ( make ) in
 make.top.equalTo(self.view.snp.top)
 make.bottom.equalTo(self.view.snp.bottom)
 make.leading.equalTo(self.view.snp.leading)
 make.trailing.equalTo(self.view.snp.trailing)
 }
 
 self.labelTitle.snp.makeConstraints { ( make ) in
 make.top.equalTo(self.content.snp.top).offset(20)
 make.leading.equalTo(self.content.snp.leading).offset(20)
 make.trailing.equalTo(self.content.snp.trailing).offset(-20)
 }
 
 self.imageView.snp.makeConstraints { ( make ) in
 make.top.equalTo(self.labelTitle.snp.bottom).offset(20)
 make.height.equalTo(250)
 make.leading.equalTo(self.content.snp.leading)
 make.trailing.equalTo(self.content.snp.trailing)
 }
 
 self.labelDescription.snp.makeConstraints { ( make ) in
 make.top.equalTo(self.imageView.snp.bottom).offset(20)
 make.leading.equalTo(self.view.snp.leading).offset(20)
 make.trailing.equalTo(self.view.snp.trailing).offset(-20)
 }
}

Me permitam abrir um pequeno parênteses aqui para trazer à luz uma parada muito louca, fruto da fusão: ViewCode + SnapKit + View Animations

Vamos supor que em determinado momento eu precise animar meu título para que ele fizesse uma transição vertical, alterando o top de 20 para 150.

Bem, podemos fazer de duas formas:

  • Capturar o frame atual, alterar o valor do origin.y para 150 e dentro do bloco animations do UIView.animate(…) redefinir o frame da minha label.
  • Criar uma property @IBOutlet do tipo NSLayoutConstraint, linkar com a minha constraints no Interface Builder e também dentro do bloco animations do UIView.animate(…) alterar o valor da constante de 20, para 50.

 

Ok! E como ficaria utilizando View Code? Segue abaixo:

self.labelTitle.snp.updateConstraints { (make) -> Void in
 make.top.equalTo(150)
}
UIView.animate(withDuration: 0.3) {
 self.view.layoutIfNeeded()
}
view rawview-code-constra

O SnapKit possui um método chamado updateConstraints, no qual sem precisar se preocupar em criar referências para uma constraint específica você pode atualizar qualquer constraint do objeto de forma simples e prática.

Brigado, de nada, tchau!

Maravilha! Já temos nossos três principais métodos de setup e já conseguimos visualizar nossa pequena tela.

Péra! De onde surgiram os elementos? Onde foram instanciados?

import UIKit
class ViewCodeViewController: UIViewController {
 
 let content: UIView = UIView(frame: .zero)
 let imageView: UIImageView = UIImageView(frame: .zero)
 let labelTitle: UILabel = UILabel()
 let labelDescription: UILabel = UILabel()
 
}

No caso, definimos e iniciamos os elementos no mesmo local onde definiríamos nossos @IBOutlets.

Agora sim, temos tudo pronto pra iniciar nosso Hello View Code!

Dicas

Init

No exemplo do artigo, com apenas uma view controller, instanciei e inicializei os elementos no mesmo lugar.

Mas outra abordagem interessante seria instanciar os elementos junto com um init da nossa view controller. Como funcionaria?

import UIKit
class ViewCodeViewController: UIViewController {
 
 let content: UIView
 let imageView: UIImageView
 let labelTitle: UILabel
 let labelDescription: UILabel
 
 init(content: UIView, imageView: UIImageView, labelTitle: UILabel, labelDescription: UILabel) {
 self.content = content
 self.labelTitle = labelTitle
 self.imageView = imageView
 self.labelDescription = labelDescription
 
 super.init(nibName: nil, bundle: nil)
 }
 
 required init?(coder aDecoder: NSCoder) {
 fatalError("init(coder:) has not been implemented")
 }
 
}

 

Note que com esta inicialização precisamos implementar também o método required init?(coder aDecoder: NSCoder), garantindo que nosso objeto não será inicializado por um “unarchiver”.

Com isso, eu poderia abstrair a inicialização para uma outra classe, um hanlder ou builder, como você preferir. Assim, ao inicializar minha view controller eu já passaria os elementos inicializados:

class Builder {
 
 static func initViewController() -> UIViewController {
 
 let controller = ViewCodeViewController(content: UIView(frame: .zero), labelTitle: UILabel(), labelDescription: UILabel(), imageView: UIImageView(frame: .zero))
 return controller
 }
 
}
Builder.initViewController()

Pensando em um projeto maior, esta abordagem nos permitiria abstrair a parte de configuração dos elementos, passando a responsabilidade para outra classe, destinada a isso.

Configuração

Abstraction Mindset é um dos principais itens e se encaixa em todas as etapas de setup e life cycle da sua view.

Pensar em como o elemento se comportará não apenas em seu estado inicial, mas em como ele pode auxiliar outras views, vai lhe poupar muitas linhas de código e dor de cabeça na hora de alterar algum comportamento.

Além de abstrair a configuração para um Handler Builder, podemos pensar em elementos componentizados, que podem ser reutilizados em qualquer lugar da sua aplicação.

Em vez de criar um UILabel() e personalizar seus atributos para um Title, podemos criar um UILabelTitle() que já vai conter as configurações necessárias que serão utilizadas em qualquer tela.

Hierarquia

É de extrema importância adicionar os elementos de forma coerente ao que será apresentado na tela, de forma linear e sequencial. Se um elemento for exibido no fim da tela, não faz sentido adicioná-lo antes do título, por exemplo. Como estamos trabalhando unicamente com código, dar uma atenção especial neste ponto vai ajudar muito na manutenção, evolução e – por que não? – refactories futuros.

Nunca é demais lembrar: Os princípios SOLID de arquitetura também são importantes aqui.

Importante ter uma atenção no ciclo de vida da sua tela. Às vezes não precisamos adicionar todos os elementos de uma vez. Podemos ir adicionando on demand. Considerar o peso que nossa tela terá e como isso influencia em interações, animações e mudanças de estado vai evitar muito problema com telas pesadas e lentas, o que vamos combinar, não combina muito com nosso lindo iOS ❤

Pessoal da comunidade não vai curtir, mas o LinearLayout do Android — sim, do Android — é uma ótima referência pra ajudar a entender melhor esse ponto.

Lifecycle

Tenha sempre em mente o caminho que o sistema vai percorrer até configurar, adicionar e exibir os elementos. É muito importante ter um bom gerenciamento do ciclo de vida de cada elemento.

Se vou chamar a configuração no init, se vai ser no viewDidAppear, se é no viewDidLoad, se é no layoutSubviews, layoutIfNeeded, layoutShape, layoutSublayers…

Tudo influencia diretamente no comportamento da sua tela. Dar uma boa olhada na documentação para amarrar algumas pontas em relação a isso nunca é demais. Afinal, nós estamos tirando a responsabilidade de build screen do sistema e trazendo para o nosso gerenciamento.

Lifecycle é algo realmente importante se tratando de View Code.

Libs externas

Alguns podem torcer o nariz ao ver a sugestão de uma lib para configurar constraints. Eu também sou meio xiita às vezes, mas mais uma vez chamo atenção para Abstraction Mindset.

Ninguém precisa reinventar a roda, ela já existe. Precisamos apenas reutilizar, customizar e adaptar o uso para o melhor proveito.

A analogia funciona muito bem para desenvolvimento. Se eu tenho uma lib que abstrai toda a configuração de uma constraint, resumindo linhas e mais linhas de código a um único método, por que não utilizar?

No exemplo eu usei o SnapKit, mas existem outras libs muito boas também que auxiliam neste ponto.

Vamos pensar de forma mais abrangente. Isto vale para tudo que for ser feito, refeito, customizado, editado, em nossa aplicação. Se você pode abstrair, faça! Se pode criar uma lib, crie! Se um método pode ser reutilizado em toda a aplicação, reutilize!

Organização

Por último, mas não menos importante, organização! Se antes tínhamos uma preocupação muito grande com as temidas “Massive Storyboards”, temos que trazer a preocupação se tratando de View Code.

Precisamos utilizar o Swift a nosso favor.

Orientação a Protocolos, Extensions, Structs, Enums e outros vão auxiliar muito na organização e padronização do projeto.

Nem preciso comentar sobre o Project Navigator, certo? rs

Arquiteturas?

Aqui o seu bom senso irá determinar tudo. MVP, MVC, MVVM, VIPER etc, tudo vai depender do que o seu projeto precisa e do resultado que você espera. Be free e escolha o que melhor se encaixa em seu cenário.

Pra fechar, vale lembrar que não existe bala de prata. Nada é 100% perfeito. View code tem suas qualidades? Tem. Storyboards tem suas qualidades? Tem também. Vale ter um senso crítico muito grande, independente da sua preferência, para escolher o melhor caminho.

É isso galera… Dúvidas, críticas, sugestões e ideias, só comentar.

Carry on!

Ah, nosso capítulo já falou sobre o assunto em um artigo do Thiago Lioy, com uma abordagem bem interessante. Vale muito a pena dar uma olhada lá.