Como criar um App para iOS em ViewCode

reisdev

Matheus dos Reis de Jesus

Posted on July 21, 2023

Como criar um App para iOS em 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 alguns 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.

Sumário

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
}
Enter fullscreen mode Exit fullscreen mode

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 
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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")
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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")

    }
}
Enter fullscreen mode Exit fullscreen mode

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

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()
}
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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")
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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:

GitHub logo reisdev / viewcode-example-ios

Repositório com o app criando neste artigo:

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

💖 💪 🙅 🚩
reisdev
Matheus dos Reis de Jesus

Posted on July 21, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related