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:
Após isso, o Xcode irá abrir uma janela para seleção do tipo de aplicativo a ser criado:
Para este tutorial, iremos escolher a plataforma iOS e a opção App, e então clicar em Next:
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).
Após clicar em Next, o Xcode irá apresentar uma tela para escolher onde deseja salvar os arquivos do seu projeto.
Bast selecionar a pasta desejada e clicar no botão Create. Feito isso, seu projeto estará criado e será aberto pelo Xcode:
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:
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:
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:
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
labelebuttonà nossaView - Posicionar o
labelcentralizado horizontalmente(centerXAnchor) e verticalmente(centerYAnchor) naView - Posicionar o
buttoncentralizado horizontalmente(centerXAnchor) e, na vertical, a um espaçamento de8a partir da parte inferior(bottomAnchor) do nossolabel - Definir a cor de fundo(backgroundColor) da
Viewcomo 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:
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 aViewconsiga notificar quem a esteja usando que uma ação aconteceu, como o toque no botão@objc: Essa propriedade do métododidTapButtonpermite 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#selectorpara adicioná-lo à interação do botãoaddTarget(_ 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, nofor: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:
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:
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 👋🏽.












Top comments (8)
Gostei muito do artigo, muito explicativo e completo. Vou te chamar no Telegram pra trocar uma ideia sobre outras dúvidas que tenho. :)
Ótimo artigo, primo!
Seu sumário ficou bonitão também! Dica pra dar um up: coloca numeração em cada tópico pra termos uma sesanção de inicio/meio/fim de artigo.
Cê é d+ <3
Valeu demais primo! 💜
Excelente dica da numeração, já editei aqui. Principalmente num artigo grande assim, acho que alivia um pouco a sensação de "Não acaba nunca?"
Didatica muito boa, um otimo artigo!
Obrigado! :)
Ótimo artigo! Sua didática é muito boa.
Achei bem bacana a criação do protocolo ViewCode.
Ajuda na padronização e evita que esqueçamos de implementar algum deles.
Obrigado!
Obrigado, Bruno!
Programação orientada a protocolos é uma maravilha pra padronização. Claro, tem que ser usada com sabedoria, mas é uma ótima ferramenta pra ajudar a garantir um código mais limpo, organizado e até mesmo testável!
Artigo excelente! Obrigado pela ajuda! E parabéns pelo trabalho! 👏👏👏👏