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
:
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 elassetupConstraints()
: Define as constraints a serem usadas para posicionar os elementos na viewsetupStyle()
: 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
ebutton
à nossaView
Posicionar o
label
centralizado horizontalmente(centerXAnchor
) e verticalmente(centerYAnchor
) naView
Posicionar o
button
centralizado horizontalmente(centerXAnchor
) e, na vertical, a um espaçamento de8
a partir da parte inferior(bottomAnchor
) do nossolabel
Definir a cor de fundo(
backgroundColor
) daView
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:
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 aView
consiga notificar quem a esteja usando que uma ação aconteceu, como o toque no botão@objc
: Essa propriedade do métododidTapButton
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ã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:
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 👋🏽.