Criando interfaces declarativas para iOS

Marcílio Júnior
Ignus Insights
Published in
7 min readJul 19, 2018

Durante todo meu tempo desenvolvendo aplicativos iOS já tive a oportunidade de trabalhar em diversos modelos diferentes de construção de interfaces. Sempre gostei muito de usar Storyboards, principalmente depois dos avanços feitos no AutoLayout (mas tenho que confessar que demorei algum tempo para entender bem o seu funcionamento).

Com o passar dos anos, aplicativos mais complexos foram aparecendo, e também aplicativos nem tão complexos mas com muitas telas, cenários diferentes, partes que precisam ser reutilizadas em diversos pontos e tudo isso acaba deixando o uso do Storyboard bem complicado. Creio que muitos de vocês já viram arquivos como esse ou até piores.

Nesse cenário, começamos a pensar em diversas soluções que tendem a melhorar e ajudar na sanidade do dev em trabalhar em apps parecidos com esse. Podemos trabalhar com múltiplos Storyboards, usar XIBs para os componentes reutilizáveis e até mesmo fazer toda a UI diretamente no código! Quando escolhemos trabalhar com o Interface Builder, não temos muito para onde fugir, seja criando XIBs ou Storyboards vamos ter possíveis problemas dado o formato proprietário utilizado pela Apple baseado em XML. Dentre esses problemas podemos listar dificuldades de merge de arquivos, carregamento lento das telas dependendo da quantidade de informação ali contida, dificuldade em reutilizar componentes, dificuldades para realizar grandes mudanças de UI, entre outras.

Dado todos os problemas listados anteriormente, muitos decidem criar toda a UI de seus aplicativos no próprio código. O que resolve vários problemas do Interface Builder, mas aumenta consideravelmente a quantidade de código que você precisa escrever em suas telas (consequentemente a possibilidade de bugs) e caso você não tenha o cuidado de ter uma estrutura bem planejada para trabalhar com toda sua UI feita em código, é bem possível que você tenha mais trabalho que utilizando o Interface Builder.

Layout

Alguns meses atrás, quando estava a procura de soluções, sejam frameworks ou até mesmo novas formas de resolver algumas dores que tínhamos trabalhando com o Interface Builder nos projetos da Ignus, que me deparei com o Layout.

Ao ler a documentação inicial do projeto ele cita resolver vários dos problemas que já listei anteriormente permitindo a criação de telas através de arquivos XML muito mais simples que o formato utilizado por XIBs e Storyboards. Ou seja, uma baita mão na roda 😁.

Para os amantes do React Native, o Layout introduz uma nova hierarquia baseada em nodos bem similar ao que faz o RN com o virtual DOM. Além dessa semelhança também temos o Redbox debugger e o Live Reloading.

Ganhos de produtividade incríveis!

Vocês não conseguem imaginar o tamanho dos ganhos de produtividade que temos com isso até que utilizem. Além do mais, o Layout permite criarmos suporte aos nossos próprios componentes e também adicionar novas propriedades em componentes já existentes, como por exemplo uma URL ao UIImageView. Simplesmente incrível!

Show me the code!👨🏻‍💻

Dada todas as promessas vamos ver se isso é realmente verdade colocando um pouco a mão na massa dessa incrível biblioteca.

Para iniciarmos, vamos criar um novo projeto no Xcode e instalar a dependência do Layout. Podemos utilizar o Carthage ou o CocoaPods para facilitar um pouco. Em nosso exemplo vamos com o CocoaPods.

pod 'Layout', '~> 0.6'

Antes de começar a trabalhar com o Layout precisamos conhecer um pouco sobre como ele funciona.

LayoutNode

Essa classe é a base da hierarquia de nodos usada no framework. Podemos trabalhar com LayoutNodes via código ou via arquivos XML. Veja abaixo os exemplos tirados do próprio repositório da biblioteca.

let node = LayoutNode(
view: UIView.self,
expressions: [
"width": "100%",
"height": "100%",
"backgroundColor": "#fff",
],
children: [
LayoutNode(
view: UILabel.self,
expressions: [
"width": "100%",
"top": "50% - height / 2",
"textAlignment": "center",
"font": "Courier bold 30",
"text": "Hello World",
]
)
]
)

Através do código acima, estamos criando uma UIView que irá ocupar toda a área de superview em que for inserida, com um UILabel como filho. Dado que o exemplo é uma View muito simples, não teríamos a necessidade de criar novos arquivos, mas não fazendo isso perdemos uma das melhores features que é o Live Reloading. Vamos ver abaixo como ficaria o mesmo exemplo em um arquivo XML.

<UIView
width="100%"
height="100%"
backgroundColor="#fff">

<UILabel
width="100%"
top="50% - height / 2"
textAlignment="center"
font="Courier bold 30"
text="Hello World"
/>
</UIView>

Praticamente todos os componentes nativos do iOS tem suporte para serem usados em arquivos XML, mas caso queira utilizar componentes customizados é possível criar suas próprias extensões.

LayoutLoading

A forma mais simples de adicionarmos os nodos em View Controllers é através do protocolo LayoutLoading.

class MyViewController: UIViewController, LayoutLoading {
public override func viewDidLoad() {
super.viewDidLoad()
// #1 - create a layout programmatically
self.layoutNode = LayoutNode( ... )

// #2 - load a layout synchronously from a bundled XML file
self.loadLayout(named: ... )

// #3 - load a layout asynchronously from an XML file URL
self.loadLayout(withContentsOfURL: ... ) { error in
...
}
}
}

Caso você decida trabalhar com os nodos gerados via código, use a opção #1. Toda View Controller possui um layoutNode principal, assim como uma view.

Para o caso de arquivos XML externos, use o método mencionado na opção #2. E para o caso de arquivos XML localizados em servidores remotos, temos a possibilidade da opção #3.

Uma outra responsabilidade de muita importância desse protocolo é informar quando o layoutNode é carregado. O método abaixo é responsável por essa tarefa, que é bem recorrente quando estamos utilizando o Live Reloading.

func layoutDidLoad(_ layoutNode: LayoutNode) {
...
}

Constants e State

Trabalhar com XMLs estáticos funciona muito bem em boa parte dos casos, mas em muitos outros precisamos de passar conteúdos dinâmicos para nossas interfaces. O LayoutNode oferece duas maneiras para enviar dados dinâmicos para os arquivos XML, que são referenciadas através de expressões.

Constants são utilizados para valores que se mantém, como o próprio nome diz, constantes durante o ciclo de vida de um nodo. Modificá-los significa resenhar todo um nodo e consequentemente todas as views associadas. Esses valores são passados através do construtor do LayoutNode. É uma excelente forma de passar dados como fontes, cores ou dados fixos de uma tela.

Código fonte

loadLayout(
named: "MyLayout.xml",
constants: [
"title": NSLocalizedString("homescreen.title", message: ""),
"titleColor": UIColor.primaryThemeColor,
"titleFont": UIFont.systemFont(ofSize: 30),
]
)

XML

<UIView ... >
<UILabel
width="100%"
textColor="titleColor"
font="{titleFont}"
text="{title}"
/>
</UIView>

Em alguns casos iremos precisar de conteúdos que precisam ser modificados frequentemente em uma tela, e ter que recriar toda a hierarquia não seria performático. Para esses casos, temos o State. Os valores do state precisam ser inicializados também no construtor, mas através do método setState conseguimos modificar seu valor em tempo de execução.

loadLayout(
named: "MyLayout.xml",
state: [
"isSelected": false,
...
],
constants: [ ... ]
)

func setSelected() {
self.layoutNode?.setState([
"isSelected": true
])
}

Outlets e Actions

Assim como fazemos no Interface Builder também conseguimos linkar componentes dos nossos arquivos XML em propriedades e métodos do código fonte. No caso de outlets, é necessário que a propriedade seja compatível com o Runtime dinâmico do Objective-C através da palavra-chave @objc.

Outlet

Código fonte

class MyViewController: UIViewController, LayoutLoading {
@objc var textLabel: UILabel?

public override func viewDidLoad() {
super.viewDidLoad()
...
}
}

XML

<UIView>
<UILabel
outlet="textLabel"
text="Hello World"
/>
</UIView>

Action

Código fonte

func wasPressed() {
...
}

XML

<UIButton touchUpInside="wasPressed"/>

Esse é o básico do conhecimento que precisamos ter sobre o funcionamento dessa incrível biblioteca, mas recomendo fortemente a leitura do README do repositório dela. Contém de forma mais detalhada muitas outras informações de como fazer o melhor uso da ferramenta. 😉

Projeto de Exemplo

Para iniciar, como vamos precisar carregar os arquivos XML em algumas telas vamos iniciar criando uma classe base responsável por fazer esse trabalho.

import UIKit
import Layout
open class BaseLayoutViewController: UIViewController, LayoutLoading {
open var layoutName: String { return "" }
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public init() {
super.init(nibName: nil, bundle: nil)
loadLayout(named: layoutName)
}
}

Através dessa base controller iremos inicializar todas as View Controllers da aplicação. Para ver o como é simples, vamos ver a seguinte estrutura:

Primeiro criamos duas View Controllers, `ListViewController` e ProfileViewController e seus respectivos arquivos de UI.

ListView.xml

<UIView backgroundColor="red">
</UIView>

ProfileView.xml

<UIView backgroundColor="yellow">
</UIView>

ListViewController.swift

import UIKit
import Layout
final class ListViewController: BaseLayoutViewController {
override var layoutName: String { return "ListView.xml" }
}

ProfileViewController.swift

import UIKit
import Layout
final class ProfileViewController: BaseLayoutViewController {
override var layoutName: String { return "ProfileView.xml" }
}

Agora vamos criar nossa estrutura de tabs.

HomeTab

<UITabBarController view.backgroundColor="white">
<ListViewController tabBarItem.title="Lista" />
<UINavigationController tabBarItem.title="Perfil">
<ProfileViewController navigationItem.title="Perfil" />
</UINavigationController>
</UITabBarController>

HomeTabBarController.swift

import UIKit
import Layout
final class HomeTabBarViewController: BaseLayoutViewController {
override var layoutName: String { return "HomeTabView.xml" }
}

Para carregar as telas, mudamos também nosso `AppDelegate.swift`

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

window = UIWindow()
window?.backgroundColor = .white
window?.rootViewController = HomeTabBarViewController()
window?.makeKeyAndVisible()
return true
}

Conclusão

Como podemos observar, é muito simples e rápido criar uma estrutura baseada TabBarController e NavigationController, os dois componentes de navegação mais básicos do iOS, utilizando o framework Layout.

Ao se aprofundar mais na ferramenta através da documentação e com o próprio uso, você irá notar o quão produtivo será criar sua UI de forma declarativa através do Layout.

Curtiu? 👏👏

Caso tenha qualquer dúvida ou opinões não deixe de comentar! 😄

--

--