Storyboard, XIBs, view code ou SwiftUI?

Nosso problema

Antigamente, aqui no Elo7, implementamos uma arquitetura onde o desenvolvimento de layouts era a partir do React Native. Era uma arquitetura que resolvia um grande problema nosso à época: a duplicação de código entre as duas plataformas nas quais distribuímos nossos aplicativos (Elo7 e Talk7). Por muito tempo foi uma boa solução, mas acabamos nos deparando com um problema.

Queríamos desenvolver features e comportamentos mais complexos, que entregariam uma experiência fora de série para nossos usuários, e para isso precisávamos alinhar o gerenciamento de estado (componentDidMount, componentDidUpdate, etc.) do React Native com o ciclo de vida do nativo (onCreate, viewDidLoad, onDestroy, viewWillDisappear, etc.). Nesse tempo, desenvolvemos várias POCs (proof of concepts) e nenhuma supriu nossas expectativas, e então decidimos que era improdutivo evoluirmos essa arquitetura.

Neste momento, em paralelo, discutíamos sobre a implementação do nosso BFF (Backend For Frontend) e como ele alimentaria nossos aplicativos. Durante essas discussões, resolvemos também falar sobre nossa stack, que até o momento era nativo + React Native, e apresentamos propostas sobre irmos para nativo (apenas nativo) ou fazer um hard reset e começar tudo com Flutter (tá na moda, né?!). Discutimos sobre todos esse assuntos e, por n fatores, tomamos a decisão de que o nativo supriria nosso problema.

Nosso mais novo problema

Beleza, vamos implementar código nativo, então qual é nossa primeira barreira? Temos várias telas em React Native no nosso projeto, então teríamos que começar a implementá-las em nativo. E como faríamos isso? Para o Android, seguimos o padrão, que é a criação de layouts em XML; mas e para o iOS, como vamos prosseguir? Temos várias formas de implementar layouts: temos storyboards, temos os XIBs, SwiftUI e o view code. Qual deles nos ajudaria e elevaria a qualidade da nossa entrega?

Storyboards: Clicar, arrastar e deixar tudo junto

Nossa primeira opção foram os storyboards. Os storyboards são simples de implementar, apesar de terem algumas limitações não tão simples - se você já tentou fazer um botão arredondado com storyboard sabe do que estou falando.

A curva de aprendizado com storyboards é muito menor com relação ao view code, e a prototipagem é facilitada com o Interface Builder, então storyboards seriam uma boa solução para nós.

Seriam mas, por conta de trabalharmos em equipe e adotarmos o pair programming, tentamos paralelizar as entregas o máximo possível, portanto é provável que, em algum momento, duas ou mais partes da equipe estivessem trabalhando no mesmo storyboard. Quando vários desenvolvedores trabalham no mesmo arquivo de storyboard ao mesmo tempo, conflitos são inevitáveis. O storyboard é um arquivo XML gerenciado pelo Xcode, e o significado de cada linha de código desse XML não é tão simples de entender, imagine então resolver conflitos, me dá até calafrios pensar nisso.

Além dos conflitos, uma outra premissa que queríamos é a reutilização de código. No caso de storyboards reutilização não é tão trivial. Para instanciar uma View Controller de um storyboard é necessário que se instancie o storyboard completo, você não pode simplesmente instanciar a View Controller que te interessa, e não era bem o que esperávamos quanto a reutilização de código.

XIBs: Clicar, arrastar e deixar tudo separado

Nossa segunda alternativa eram os XIBs. Os XIBs parecem uma ideia interessante: assim como o storyboard são simples de implementar e também temos o feedback visual. Mas além de carregar os pontos fortes do storyboard, os XIBs carregam também o ponto fraco: problemas de conflitos (em menor escala, é verdade).

Teoricamente dois desenvolvedores trabalharem no mesmo arquivo XIB seria raro, mas pode acontecer. Sendo assim, podemos dizer que os conflitos gerados seriam em menor escala, e então XIBs poderiam ser uma alternativa plausível, isso se não fosse pelo fato de que no Elo7 temos telas dinâmicas. Várias das informações que são apresentadas nas nossas telas são decididas pelo nosso BFF, portanto não temos certeza de quais componentes o nosso layout vai exibir. E os arquivos XIBs não são tão propensos a esse “dinamismo”, tornando eles uma solução parcial para nosso problema.

SwiftUI: Inovação inviável

SwiftUI poderia ser uma alternativa viável, mas nosso principal empecilho é a necessidade de termos nossos aplicativos com versão mínima iOS 13. Fizemos um estudo sobre nossa base de usuários e vimos que uma boa quantidade de usuários utilizam o SO em versões abaixo da 13, portanto SwiftUI foi descartado no momento; quem sabe no futuro.

View Code: Código? É! Código!

Chegamos então à nossa “última” alternativa, o view code. No nosso time já tínhamos um integrante com experiência em view code e que defendia a implementação.

“Ahhhhh, é código, código vocês sabem fazer.” (QUALQUER, Pessoa).

Apesar de ser a solução mais “complexa”, foi a que melhor atendeu nossos problemas e nossas expectativas. O view code tem uma curva de aprendizado muito maior que XIBs e storyboards, além de que o maior pró das outras alternativas é o maior contra do view code, o feedback visual.

O view code tem evoluído bastante nos últimos tempos. Além das próprias bibliotecas da Apple, atualmente temos bibliotecas que auxiliam na implementação, como o SnapKit (torne-se também uma Testemunha de SnapKit!).

Para view code a Apple disponibiliza três alternativas de implementação: Visual Format, Layout Constraints e Layout Anchors. Nossa proposta inicial era começar com Layout Anchors, já que é o mais recente entre os três e que apresentava uma curva de aprendizagem mais interessante para nós. No meio dos nossos estudos, encontramos uma alternativa ainda melhor: o SnapKit! É uma biblioteca que encapsula algumas funcionalidades do Layout Anchors e torna nossa vida um pouco mais simples. Além disso, utilizar Layout Anchors com SnapKit é bastante parecido com as Constraints Layout do Android! Vou trazer alguns exemplos das 4 alternativas abaixo e discutir um pouco sobre o que encontramos de bom e ruim em cada uma delas.

Protótipo

Vou utilizar uma tela bem simples para demonstrar como é a implementação. Ela tem um Label e um Button, nada muito elaborado:

Tela com label simples e botão que não faz nada

Para indicar ao SO que vou definir meus atributos de constraints, devemos atribuir o valor false ao atributo translatesAutoresizingMaskIntoConstraints dos componentes, assim o SO não dimensiona automaticamente os componentes com base nas dimensões do dispositivo e, sim, a partir das constraints implementadas.

import UIKit

class ViewController: UIViewController {

    private lazy var label: UILabel = {
        let label = UILabel()
        label.text = "Exemplo de View Code"
        label.textAlignment = .center
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()

    private lazy var button: UIButton = {
        let button = UIButton()
        button.setTitle("Botão que faz nada", for: .normal)
        button.backgroundColor = .systemYellow
        button.layer.cornerRadius = 15
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        view.addSubview(label)
        view.addSubview(button)

        //TODO: Call constraint method here.
    }

    [...]

Visual Format

Essa é a forma mais antiga de implementação de view code e provavelmente a menos utilizada. Ela não é tão objetiva e simples de entender, eu mesmo nunca tinha visto até então. Mas basicamente é uma string com caracteres especiais que simbolizam componentes, orientações, distanciamentos, conexões, etc.

A formatação da string segue o seguinte padrão:

<orientation>:<superview><connection>[<view>](<connection><view>)<connection><superview>

E apresenta os seguintes símbolos especiais:

  • V: orientação vertical
  • H: orientação horizontal
  • |: superview
  • -: espaçamento padrão (8 px)
  • ==: larguras iguais (opcional)
  • -20-: espaçamento (20 px)
  • <=: menor ou igual
  • >=: maior ou igual
  • @XXX: prioridade da constraint (valor entre 0 e 1000):
    • @250: baixa prioridade
    • @750: alta prioridade
    • @1000: obrigatório

Vou pegar de exemplo a string de formatação que usamos logo abaixo para definir as constraints horizontais do nosso Label:

[...]
    withVisualFormat: "H:|-20-[label]-20-|"
[...]

Primeiro precisamos definir qual a orientação da nossa constraint. Já que nossa constraint é horizontal, usamos o H (horizontal). Em seguida, definimos o componente ao qual vamos “amarrar” a constraint à nossa esquerda, no caso o | (nossa superview), e adicionamos o tamanho da constraint que queremos, -20- (20 de distância entre a superview e o próximo componente). Assim, declaramos o componente que queremos distanciar, nossa [label], e então declaramos a constraint que queremos para a direita, -20-. Por fim, declaramos o nosso componente a esquerda, que é | (nossa superview novamente).

Com um componente simples como esse, a abordagem do Visual Format parece fácil, mas imagine que você vá montar uma tela com vários componentes, seriam muitas constraints para se declarar em uma string, tornando a possibilidade de erros muito maior.

Implementação em Visual Format

Neste bloco de código, temos o nosso layout implementado com Visual Format. É um tanto quanto complexo e bastante verboso a princípio mas, conforme evoluirmos para os outros métodos, este talvez fique um pouco mais claro.

    [...]

    private func setupVisualFormatConstraints() {
        let viewsDict = [
                "label" = label,
                "button" = button
        ]

        let verticalConstraints = NSLayoutConstraint.constraints(
                withVisualFormat: "V:|-40-[label]-20-[button]",
                options: [],
                metrics: nil,
                views: viewsDict
        )

        let labelHorizontalConstraints = NSLayoutConstraint.constraints(
                withVisualFormat: "H:|-20-[label]-20-|",
                options: [],
                metrics: nil,
                views: viewsDict
        )

        let buttonHorizontalConstraints = NSLayoutConstraint.constraints(
                withVisualFormat: "H:|-20-[button]-20-|",
                options: [],
                metrics: nil,
                views: viewsDict
        )

        NSLayoutConstraint.activate(verticalConstraint)
        NSLayoutConstraint.activate(labelHorizontalConstraints)
        NSLayoutConstraint.activate(buttonHorizontalConstraints)
    }
}

Caso você tenha se interessado pela relíquia forma como é implementado, você pode aprender mais nesse post do Ray Wenderlich.

Layout Constraints

Essa é a API “bruta” para Auto Layout no iOS. As constraints são ferramentas poderosas do Auto Layout mas também um tanto quanto complexas. A implementação pode ser feita com a classe NSLayoutConstraint, definindo suas constraints a partir do método construtor dela. Nenhum dos parâmetros é opcional e você deve defini-los mesmo que não tenham impacto no seu layout. O resultado acaba sendo um código extenso e bastante repetitivo, como pode-se ver abaixo:

Implementação em Layout Constraints

    [...]
    
    private func setupLayoutConstraints() {
        let labelTopConstraint = NSLayoutConstraint(
            item: label,
            attribute: .top,
            relatedBy: .equal,
            toItem: view,
            attribute: .top,
            multiplier: 1,
            constant: 40
        )

        let labelLeadingConstraint = NSLayoutConstraint(
            item: label,
            attribute: .leading,
            relatedBy: .equal,
            toItem: view,
            attribute: .leading,
            multiplier: 1,
            constant: 20
        )

        let labelTrailingConstraint = NSLayoutConstraint(
            item: label,
            attribute: .trailing,
            relatedBy: .equal,
            toItem: view,
            attribute: .trailing,
            multiplier: 1,
            constant: -20
        )

        let buttonTopConstraint = NSLayoutConstraint(
            item: button,
            attribute: .top,
            relatedBy: .equal,
            toItem: label,
            attribute: .bottom,
            multiplier: 1,
            constant: 20
        )

        let buttonLeadingConstraint = NSLayoutConstraint(
            item: button,
            attribute: .leading,
            relatedBy: .equal,
            toItem: view,
            attribute: .leading,
            multiplier: 1,
            constant: 20
        )

        let buttonTrailingConstraint = NSLayoutConstraint(
            item: button,
            attribute: .trailing,
            relatedBy: .equal,
            toItem: view,
            attribute: .trailing,
            multiplier: 1,
            constant: -20
        )

        view.addConstraints([
            labelTopConstraint,
            labelLeadingConstraint,
            labelTrailingConstraint,
            buttonTopConstraint,
            buttonLeadingConstraint,
            buttonTrailingConstraint
        ])
    }
}

Layout Anchors

Toda UIView possui um conjunto de âncoras (anchors) que definem suas regras de posicionamento no layout. Temos topAnchor, bottomAnchor, leftAnchor, rightAnchor, leadingAnchor, trailingAnchor, widthAnchor, heightAnchor, centerXAnchor e centerYAnchor. E todos esses atributos são acessíveis a partir de qualquer componente que estenda de UIView.

A maioria desses anchors são fáceis de assimilar, mas você deve ter percebido que dois deles são um tanto quanto diferentes: leadingAnchor e trailingAnchor. Para mim, provavelmente para você também, leadingAnchor e leftAnchor representam o mesmo comportamento no layout, e trailingAnchor e rightAnchor também. Isso acontece por conta de nosso SO estar configurado para idiomas ocidentais, ou seja, a orientação de escrita é da esquerda-para-direita. Já para SOs configurados para idiomas orientais, por exemplo árabe e hebraico, o leadingAnchor terá o mesmo comportamento que o rightAnchor e trailingAnchor, o mesmo de leftAnchor.

Na prática, se você tem componentes que devem se adaptar para os idiomas da direita-para-esquerda, é recomendado que utilize leadingAnchor e tralingAnchor. Já se seus componentes devem ter o mesmo comportamento independente do idioma, utilize rightAnchor e leftAnchor.

A implementação com Layout Anchor é muito mais concisa do que a com Layout Constraints e, principalmente, do que a com Visual Format. O código não é extenso e bastante legível em comparação aos outros métodos.

Implementação em Layout Anchors

    [...]

    private func setupLayoutAnchors() {
        label.topAnchor.constraint(equalTo: view.topAnchor, constant: 40).isActive = true
        label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20).isActive = true
        label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20).isActive = true
    
        button.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 20).isActive = true
        button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20).isActive = true
        button.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20).isActive = true
    }
}

SnapKit

O SnapKit é uma biblioteca que encapsula o Layout Anchors e deixa o nosso desenvolvimento muito mais simples. A documentação deles é bem clara na forma de utilização e você pode aproveitar muitas ferramentas poderosas do Swift para otimizar ainda mais sua implementação.

A biblioteca já encapsula o translatesAutoresizingMaskIntoConstraints = false então, sempre que você utilizar o SnapKit para montar suas constraints, não precisa lembrar de adicionar este atributo em seus componentes.

As regras para trailing, leading, right e left também se aplicam ao SnapKit.

Implementação em SnapKit

    [...]

    private lazy var label: UILabel = {
        let label = UILabel()
        label.text = "Exemplo de View Code"
        label.textAlignment = .center
        return label
    }()

    private lazy var button: UIButton = {
        let button = UIButton()
        button.setTitle("Botão que faz nada", for: .normal)
        button.backgroundColor = .systemYellow
        button.layer.cornerRadius = 15        
        return button
    }()

    [...]
    
    private func setupSnapKitConstraints() {
        label.snp.makeConstraints { make in
            make.top.leading.equalToSuperview().offset(40)
            make.trailing.equalToSuperView().inset(20)
        }

        button.snp.makeConstraints { make in
            make.top.equalTo(label.snp.bottom).offset(20)
            make.leading.equalToSuperView().offset(20)
            make.trailing.equalToSuperView().inset(20)
        }
    }
}

TL;DR

  • Storyboard: Implementação simples com auxilio de interface gráfica, mas gera código incompreensível e amontoa várias ViewControllers em um mesmo arquivo;
  • XIBs: Views mais primitivas, implementação simples com interface gráfica mas não apresenta dinamismo no conteúdo;
  • SwiftUI: API declarativa Flutter para desenvolvimento de layout com Swift, futuro interessante mas atinge apenas usuários com iOS 13+;
  • View Code: Implementação de layout a partir da API de Auto Layout, facilita o trabalho em equipe e consolida conhecimento sobre a linguagem e o framework da Apple;
  • Visual Format: Strings complicadas demais para serem compreendidas por humanos;
  • Layout Constraints: API bruta para constraints, funcionam bem mas bastante verboso, Layout Anchors resumem bem;
  • Layout Anchors: Implementação simplificada do Layout Constraints, mais legível e concisa;
  • SnapKit: Seita de desenvolvedores cansados de “ativar” constraints.

Se interessou em aprender mais sobre view code? Ou até na nossa stack de desenvolvimento aqui no Elo7? Olhe nossas vagas!