Como criar um App para iOS em ViewCode
Tags: swift, beginners, braziliandevs, ios
Published: 2023-07-21 14:35:17

##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

- [1. Contexto](#1-contexto)
- [2. Criando o app](#2-criando-o-app)
- [3. Removendo o Storyboard](#3-removendo-o-storyboard)
- [4. ViewCode](#4-viewcode)
    - [4.1. Configurando o ViewCode](#41-configurando-o-viewcode)
    - [4.2. Criando um protocolo ViewCode](#42-criando-um-protocolo-viewcode)
    - [4.3. Criando uma View com ViewCode](#43-criando-uma-view-com-viewcode)
    - [4.4. Usando a View na ViewController](#44-usando-a-view-na-viewcontroller)
    - [4.5. Adicionando uma ação ao botão](#45-adicionando-uma-ação-ao-botão)
- [5. Conclusão](#5-conclusão)
    
## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gc00t73yt74seovgi4la.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w8fcx0ubmke7q5v18veg.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j80ahg9ct46zpk72q39b.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pc3au7fgxxrujae4shc3.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2chpq3ikt5z48fu6c50h.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f0qlvjpimmn62rh25a83.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kpqg4cczot1u1f1ws3ad.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c8v9os7ft19fkj1a512x.png)

![Captura de tela destacando o item Storyboard name e com uma seta indicando o botão de menos, para remover o item](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/loc4p02oi7xd44sloqbk.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kv73gb2yyts4dbuyzwzy.png)

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:

```swift
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:

```swift
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`:

```swift
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:

```swift
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`:

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

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

```swift
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:

```swift
// 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`:

```swift
// 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:

```swift
// 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`.

```swift
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`:

```swift
// 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:

```swift
// 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"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z8j6wx6shxhirj180qzt.png)

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`:

```swift
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:

```swift
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:

```swift
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"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c7ifr86274nh61sx9vml.png)

## 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: 

{% embed https://github.com/reisdev/viewcode-example-ios %}

Ficou com alguma dúvida? Deixe nos comentários ou me procure em alguma das minhas redes, que você encontra [aqui](reisdev.com.br)

Até o próximo artigo 👋🏽.

Capa por <a href="https://unsplash.com/@uxstore?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">UX Store</a> no <a href="https://unsplash.com/photos/jJT2r2n7lYA?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>