Como criar um App para iOS em ViewCode

Como criar um App para iOS em ViewCode

Aprenda a criar um app para iOS do zero usando o padrão ViewCode

1. Contexto

ViewCode é um termo usado para descrever a construção de interfaces de usuário utilizando apenas código. Inicialmente as telas eram criadas usando Storyboard/XIB, através da interface gráfica do próprio Xcode. Porém, existiam algunss problemas:

  • Acesso limitado a propriedades dos elementos

  • Conflitos de modificação de arquivos (merge hell)

  • Quando o app crescia muito, os arquivos ficavam pesados e causavam lentidão no Xcode (Storyboard)

Então, foi se criando um novo padrão, apelidado de ViewCode. Neste artigo, veremos brevemente como ele funciona e como criar um app do zero baseado nesse formato.

2. Criando o app

O primeiro passo, obviamente, é criar um app. Para criar este tutorial estou usando a versão 14.3.1 do Xcode. Caso você esteja usando uma versão diferente, alguns detalhes podem mudar.

Primeiramente, abrimos o Xcode e clicamos em Create a new Xcode Project:

Captura de tela da tela inicial do Xcode, com a lista de ações possíveis e os projetos recentes

Após isso, o Xcode irá abrir uma janela para seleção do tipo de aplicativo a ser criado:

Captura de tela da etapa de criação de um novo projeto no Xcode, exibindo as possíveis opções para cada plataforma

Para este tutorial, iremos escolher a plataforma iOS e a opção App, e então clicar em Next:

Captura de tela do Xcode na etapa de criação de projeto com a aba iOS e a opção App selecionadas

A próxima etapa consiste em nomear o projeto e escolher algumas configurações básicas, como nome do app, time, Bundle identifier, tipo de interface(UI) e linguagem. Também é possível optar por usar CoreData sincronizado com a nuvem e incluir testes. Para este tutorial, o mais importante é selecionar modificar as seguintes opções:

  • Interface → Storyboard

  • Language → Swift

Assim, garantimos que nosso app estará configurado para usar a linguagem Swift(e não Obj-C) e, por padrão, a interface será criada com Storyboard(e não SwiftUI).

Captura de tela do Xcode na etapa de escolha de opções para o novo projeto

Após clicar em Next, o Xcode irá apresentar uma tela para escolher onde deseja salvar os arquivos do seu projeto.

Captura de tela do Xcode exibindo o Finder para selecionar onde deseja salvar o novo projeto

Bast selecionar a pasta desejada e clicar no botão Create. Feito isso, seu projeto estará criado e será aberto pelo Xcode:

Captura de tela do Xcode com o projeto ExampleApp criado aberto

Agora que já temos um novo projeto, vamos para o próximo passo: remover o Storyboard.

3. Removendo o Storyboard

Nas propriedades do projeto ExampleApp, que já são abertas logo após a criação do mesmo, precisamos acessar a aba Info Caso você, por acaso, feche essa janela, é possível acessá-la novamente clicando duas vezes no item do projeto na barra lateral esquerda:

Captura de tela do Xcode destacando item do projeto e a aba Info

Na aba Info, na seção Custom iOS Target Properties, encontre os itens Main Storyboard file base name e Application Scene Manifest → Scene Configuration → Default Configuration > Storyboard name e remova-os da lista, usando o ícone de menos próximo ao nome do item ou selecionando-o e apertando a tecla delete/backspace:

Captura de tela destacando o item Main Storyboard file base name e uma seta indicando o botão de menos, para remover o item

Captura de tela destacando o item Storyboard name e com uma seta indicando o botão de menos, para remover o item

Após excluir estes itens, podemos remover o arquivo Main.storyboard do nosso projeto, já que ele não será mais necessário. Para isso, basta encontrá-lo na barra lateral esquerda e apagá-lo, usando o atalho Cmd+Delete ou pelo menu de atalhos, como abaixo:

Captura de tela do Xcode com o arquivo Main.storyboard selecionado, com a caixa de opções aberta e o item Delete em destaque

Agora que nosso projeto não possui mais um arquivo Storyboard, podemos começar a configurar nosso app para ser construído usando ViewCode.

4. ViewCode

4.1. Configurando o ViewCode

Já que não temos mais um Storyboard para apresentar nossa tela, precisamos definir um novo responsável por essa apresentação. Para isso, vamos acessar o arquivo SceneDelegate e fazer algumas modificações no método scene. Inicialmente, ele já vem com alguns comentários, mas, pra encurtar o trecho de código, eles foram removidos nesse exemplo. Inicialmente, esse é o conteúdo do método:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene,
               willConnectTo session: UISceneSession,
               options connectionOptions: UIScene.ConnectionOptions) {

        guard let _ = (scene as? UIWindowScene) else {
            return 
        }

    // [...] Outros métodos do SceneDelegate
}

Primeiro, iremos obter a scene que nosso método recebe e tentar convertê-la em uma UIWindowScene. Isso já está escrito no método, porém o valor da conversão está sendo descartado, por conta do let _ = [...]. Para isso, basta nomearmos a atribuição para obter o valor:

guard let windowScene = (scene as? UIWindowScene) else {
    return 
}

Precisamos fazer essa conversão e obter o valor pois iremos usá-lo criação da nossa UIWindow usada no SceneDelegate:

self.window = UIWindow(windowScene: windowScene)

Com uma window definida, o próximo passo é instanciar uma UINavigationController para ser apresentada, para que nosso app já seja construído com suporte para navegação.

O projeto ExampleApp já foi criado com uma ViewController de exemplo, e ela será a tela inicial(rootViewController) da navegação do nosso app:

let navigationController = UINavigationController(rootViewController: ViewController())

Agora que já temos uma UINavigationController, podemos defini-la como sendo a raiz(rootViewController) das nossas telas na window. Em seguida, fazemos com ela seja definida como a principal e apresentada na tela através do método makeKeyAndVisible:

self.window?.rootViewController = navigationController
self.window?.makeKeyAndVisible()

Após cada passo citado acima, teremos o seguinte código:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        guard let windowScene = (scene as? UIWindowScene) else {
            return
        }

        self.window = UIWindow(windowScene: windowScene)

        let navigationController = UINavigationController(rootViewController: ViewController())

        self.window?.rootViewController = navigationController
        self.window?.makeKeyAndVisible()
    }

    // [...] Outros métodos do SceneDelegate
}

A próxima etapa consiste em criar um padrão para construção das views através de um protocolo.

4.2. Criando um protocolo ViewCode

Uma padronização muito interessante é criar um protocolo que irá definir o formato que uma UIView construída em ViewCode deve ter. Dessa forma, todas as views terão um mesmo padrão de construção e ficará mais fácil de encontrar informações importantes.

A seguir, uma sugestão de protocolo que possui o básico necessário para manter a construção de uma view bem organizada:

// Arquivo ViewCode.swift
protocol ViewCode {
    func addSubviews()
    func setupConstraints()
    func setupStyle()
}

extension ViewCode {
    func setup() {
        addSubviews()
        setupConstraints()
        setupStyle()
    }
}

Uma breve explicação de cada método definido acima:

  • addSubviews(): Adiciona as views como subviews e define a hierarquia entre elas

  • setupConstraints(): Define as constraints a serem usadas para posicionar os elementos na view

  • setupStyle(): Define os estilos da view, como cor, bordas e etc.

  • setup(): Executa os três métodos anteriores como parte do processo padrão de inicialização de uma view

OBS: Criamos o método setup em uma extension do protocolo porque não é possível criar implementações de métodos diretamente no protocolo. Mas, fazendo isso, conseguimos resumir o setup em uma única chamada de método, setup(). No próximo passo veremos isso na prática.

Com nosso protocolo pronto, podemos criar uma view que conforme com ele e entender melhor como essa estrutura funciona.

4.3. Criando uma View com ViewCode

Como exemplo, iremos criar uma view para a nossa ViewController que tenha um texto e um botão. Para isso, iremos precisar dos elementos UILabel e UIButton, contidos no framework UIKit.

Primeiramente, vamos um arquivo View.swift com a nossa classe View, que herda as características de uma UIView:

// Arquivo View.swift

// Importamos o UIKit
import UIKit

class View: UIView {
    init() {
        // Chamamos um método da UIView para inicialização
        super.init(frame: .zero)
    }

    // O método a seguir é obrigatório na classe UIView
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Agora que temos uma View, podemos criar as views que serão exibidas dentro dela. Como citado anteriormente, um texto e um botão:

// Arquivo View.swift

// Importamos o UIKit
import UIKit

class View: UIView {

    private lazy var label: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()

    private lazy var button: UIButton = {
        let button = UIButton()
        button.translatesAutoresizingMaskIntoConstraints = false
        button.setTitleColor(.blue, for: .normal)
        return button
    }()

    init() {
        // Chamamos um método da UIView para inicialização
        super.init(frame: .zero)
    }

    // O método a seguir é obrigatório na classe UIView
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setup(labelText: String, buttonTitle: String) {
        label.text = labelText
        button.setTitle(buttonTitle, for: .normal)
    }
}

Talvez o trecho de código acima tenha embaralhado um pouco sua mente com tantos conceitos diferentes e talvez desconhecidos. Abaixo, algumas respostas pra perguntas que provavelmente tenham surgido:


  • Por quê usar o modificador de acesso private?

Para evitar que a view seja modificada. Seguindo o princípio de encapsulamento, expomos apenas o que for estritamente necessário, evitando que propriedades das nossas views sejam modificadas indevidamente. Para permitir que a view seja configurada, podemos criar um método setup que tenha como parâmetros as informações necessárias.

  • Por quê usar lazy?

O termo lazy, do inglês, descreve nossa view como "preguiçosa". E é literalmente isso que ela é. Usando essa palavra-chave definimos que o valor da nossa propriedade label, por exemplo, só será definido na primeira vez que ela for acessada. E, após a definição, esse valor não poderá ser alterado. Em que contexto isso é útil? Quando temos renderização condicional. Se uma view só é adicionada caso uma condição seja atendida, evitamos que ela ocupe espaço na memória desnecessariamente.

  • O que é label: UILabel= { /* ... */ }()

O trecho acima é chamado de self-executing closure. É similar à funções anônimas em outras linguagens. Nesse caso, é como se declarássemos uma função e ela fosse chamada imediatamente. Em Swift, significa criar uma closure e chamá-la logo em seguida.

  • Para que serve translatesAutoresizingMaskIntoConstraints?

Essa propriedade é definida como false para permitir que as constraints que iremos definir não entrem em conflito com constraints que são geradas automaticamente pelo sistema. Assim, podemos definir nossas próprias constraints e posicionar os elementos como desejado.

  • Por quê o método setup(labelText:,buttonTitle:)?

Para permitir configurarmos as informações da nossa View sem a necessidade de expor cada uma das suas subviews. Dessa forma, limitamos a personalização a apenas o texto do label e o título do button.


Os conceitos acima seguem as práticas mais recomendadas para criação de views usando ViewCode. Agora que estão todos esclarecidos, podemos seguir para o próximo passo: fazer com que nossa view conforme com o protocolo ViewCode.

extension View: ViewCode {
    func addSubviews() {
        addSubview(label)
        addSubview(button)
    }

    func setupConstraints() {
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: centerXAnchor),
            label.centerYAnchor.constraint(equalTo: centerYAnchor),

            button.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 8),
            button.centerXAnchor.constraint(equalTo: centerXAnchor)
        ])
    }

    func setupStyle() {
        backgroundColor = .white
    }
}

No trecho acima, usamos os métodos definidos no protocolo ViewCode para:

  • Adicionar as views label e button à nossa View

  • Posicionar o label centralizado horizontalmente(centerXAnchor) e verticalmente(centerYAnchor) na View

  • Posicionar o button centralizado horizontalmente(centerXAnchor) e, na vertical, a um espaçamento de 8 a partir da parte inferior(bottomAnchor) do nosso label

  • Definir a cor de fundo(backgroundColor) da View como branca(.white)

Agora, precisamos apenas chamar o setup da View no init:

// Arquivo View.swift

// Importamos o UIKit
import UIKit

class View: UIView {

    private lazy var label: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()

    private lazy var button: UIButton = {
        let button = UIButton()
        button.translatesAutoresizingMaskIntoConstraints = false
        button.setTitleColor(.blue, for: .normal)
        return button
    }()

    init() {
        // Chamamos um método da UIView para inicialização
        super.init(frame: .zero)
        // Chamamos o setup da nossa view
        setup()
    }

    // O método a seguir é obrigatório na classe UIView
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setup(labelText: String, buttonTitle: String) {
        label.text = labelText
        button.setTitle(buttonTitle, for: .normal)
    }
}

extension View: ViewCode {
    func addSubviews() {
        addSubview(label)
        addSubview(button)
    }

    func setupConstraints() {
        NSLayoutConstraint.activate([
            label.centerXAnchor.constraint(equalTo: centerXAnchor),
            label.centerYAnchor.constraint(equalTo: centerYAnchor),

            button.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 8),
            button.centerXAnchor.constraint(equalTo: centerXAnchor)
        ])
    }

    func setupStyle() {
        backgroundColor = .white
    }
}

Após construir nossa View, podemos usá-la na nossa ViewController para ser exibida.

4.4. Usando a View na ViewController

Agora, retornamos para a nossa ViewController criada anteriormente e podemos usar a View para ser exibida na tela:

// Arquivo ViewController.swift
class ViewController: UIViewController {

    private lazy var myView: View = {
        return View()
    }()

    // Método do ciclo de vida que carrega a view
    override func loadView() {
        super.loadView()

        self.view = myView
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // Configuramos a View usando o método setup
        myView.setup(labelText: "Olá, mundo!", buttonTitle: "Testar")
    }
}

Esse foi o último passo para conseguirmos exibir a nossa View criada utilizando o padrão ViewCode. Se você executar sua aplicação, verá o seguinte resultado:

Captura de tela do simulador do iPhone 14 Pro com uma tela com fundo branco. Ao centro, um botão com os dizeres "Olá mundo!" e um botão escrito "Testar"

Pronto, agora temos uma tela construída utilizando ViewCode. Porém, falta um último detalhe: criamos um botão, mas sem nenhuma interação. Nosso próximo passo será criar essa interação.

4.5. Adicionando uma ação ao botão

Primeiramente, vamos criar um Delegate para nossa View, que é o padrão adotado dentro do próprio UIKit quando precisamos "encaminhar" uma ação/informação para fora de uma View, "delegando" a responsabilidade de lidar com ela. Iremos chamá-lo de ViewDelegate:

protocol ViewDelegate: AnyObject {
    func didTapButton()
}

Algo estranho apareceu nesse trecho, né? Por quê nosso protocolo conforma com o protocolo AnyObject? Para permitir que nosso delegate seja uma referência do tipo weak, que só objetos podem ter, e evitando assim retain cycles. Esse é um detalhe mais complexo e que não caberia nesse artigo(que já está extenso), mas é importante saber a motivação.

Agora, precisamos criar uma propriedade delegate na nossa View e um método para lidar com a ação do nosso botão:

protocol ViewDelegate: AnyObject {
    func didTapButton()
}

class View: UIView {
    // ...

    private lazy var button: UIButton = {
        let button = UIButton()
        button.translatesAutoresizingMaskIntoConstraints = false
        button.setTitleColor(.blue, for: .normal)
        button.addTarget(self, selector: #selector(didTapButton), for: .touchUpInside)
        return button
    }()

    weak var delegate: ViewDelegate?

    // ...    

    @objc 
    private func didTapButton() {
        delegate?.didTapButton()
    }
}

Explicando melhor o trecho acima:

  • delegate: É uma propriedade com referência fraca(weak) e opcional(?) usada para que a View consiga notificar quem a esteja usando que uma ação aconteceu, como o toque no botão

  • @objc: Essa propriedade do método didTapButton permite que ele interaja com código em Objective-C, que é o caso de boa parte do UIKit. No caso, ele é necessário para que o método possa ser usado no #selector para adicioná-lo à interação do botão

  • addTarget(_ target:, action:, for:): É o método usado para adicionar a ação ao botão. O primeiro parâmetro, target, recebe a referência da classe onde está o método, o segundo recebe a ação em si, e para isso usamos o #selector(). Por fim, no for: informamos que o método será chamado para o toque dentro do botão, por isso .touchUpInside.

Feito isso, já temos toda a configuração do lado da View para lidar com uma ação. Agora, falta designarmos para a ViewController a responsabilidade de processar a ação:

class ViewController: UIViewController {

    private lazy var myView: View = {
        let view = View()
        // Atribuimos a ViewController como delegate
        view.delegate = self
        return view
    }()

    // ...
}

extension ViewController: ViewDelegate {
    func didTapButton() {
       // Nossa ação irá atualizar a View
       myView.setup(labelText: "Sucesso!", buttonTitle: "Testar novamente")
    }
}

No trecho acima, modificamos a nossa ViewController para ser atribuída como delegate da View e para conformar com o protocolo ViewDelegate para processar a ação de toque no botão. Ao tocar no botão, nossa ViewController atualiza a View com novas informações, ficando assim:

Captura de tela do simulador do iPhone 14 Pro com os dizeres "Sucesso!" e "Tentar novamente"

5. Conclusão

Passando por cada etapa desse tutorial, você terá conhecimento suficiente para começar seus estudos sobre criação de telas seguindo o padrão ViewCode.

O código completo desse artigo pode ser encontrado neste repositório:

Ficou com alguma dúvida? Deixe nos comentários ou me procure em alguma das minhas redes, que você encontra aqui

Até o próximo artigo 👋🏽.

Capa por UX Store no Unsplash

Did you find this article valuable?

Support Matheus dos Reis de Jesus by becoming a sponsor. Any amount is appreciated!