(Você também pode ler esse post no meu blog SwiftRocks. (em inglês))
A escolha entre usar Storyboards e escrever views programaticamente em um projeto iOS é muito subjetiva. Tendo lidado com ambas no passado, eu pessoalmente, apoio projetos que são completamente escritos com views programáticas – pela sua capacidade de permitir que várias pessoas façam alterações na mesma tela, sem conflitos impossíveis de se resolver, e pelo code review facilitado.
Quando alguém começa a escrever views programaticamente, um problema comum de ser enfrentado é: onde colocar o código da view. Se você seguir o padrão dos Storyboards, onde tudo relacionado às views é colocado em um ViewController, é muito fácil acabar com uma gigantesca classe “deus”:
final class MyViewController: UIViewController { | |
private let myButton: UIButton = { | |
// | |
}() | |
private let myView: UIView = { | |
// | |
}() | |
//Mais umas 10 views aqui… | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
setupViews() | |
} | |
private func setupViews() { | |
setupMyButton() | |
setupMyView() | |
//Setup para todas as outras views… | |
} | |
private func setupMyButton() { | |
view.addSubview(myButton) | |
//Umas 10 linhas de constraints…. | |
} | |
private func setupMyView() { | |
view.addSubview(myView) | |
//Outras 10 linhas de constraints… | |
} | |
//Todos os outros setups… | |
//Toda lógica de ViewModel (se aplicável)… | |
//Toda a lógica de toque de botões e afins… | |
} |
Você pode melhorar essa situação ao mover as views para um arquivo separado e adicionar uma referência de volta ao seu ViewController, mas você ainda estará preenchendo o seu ViewController com coisas que não são necessariamente responsabilidade dele, como constraints e outras formas de setup das views – sem mencionar que agora você terá duas propriedades de view diferentes (myView e a view nativa) sem um bom motivo:
final class MyViewController: UIViewController { | |
let myView = MyView() | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
setupMyView() | |
} | |
private func setupMyView() { | |
view.addSubview(myView) | |
//Umas 10 linhas de constraints | |
myView.delegate = self | |
//Agora temos tanto a 'view' quanto a 'myView'… | |
} | |
} |
“Giant View Controllers” e ViewControllers que “sabem demais” são muito difíceis de manter e escalar. Em arquiteturas como MVVM, o View Controller deve atuar basicamente como um roteador entre a View em si e seu ViewModel; não é tarefa dele saber como desenhar as views ou como posicioná-las. Ele deve apenas repassar as informações para quem irá lidar com elas de verdade.
Para que um projeto onde as views são escritas programaticamente ser escalável, é muito importante ter uma clara separação entre os aspectos da sua arquitetura. Sua View deve ser completamente separada do seu View Controller, e felizmente existe uma maneira muito simples de sobrescrever a view original de um UIViewController, permitindo que você mantenha arquivos separados para suas views, enquanto tem a certeza de que seu ViewController não precise fazer nenhum tipo de setup nas views em si.
loadView()
loadView() é um método do UIViewController que você não vê frequentemente, mas que é muito importante para o ciclo de vida de um ViewController, por ser o responsável por fazer a propriedade view existir em primeiro lugar. Em um projeto com Storyboards, esse é o método que irá carregar seu Nib e atrelá-lo à view, mas se a tela for feita com views programáticas, tudo o que ele faz é criar uma UIView vazia. Você pode sobrescrever ele e adicionar qualquer tipo de view para a propriedade view do seu ViewController.
final class MyViewController: UIViewController { | |
override func loadView() { | |
let myView = MyView() | |
myView.delegate = self | |
view = myView | |
} | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
print(view) // Uma instância de 'MyView'! | |
} | |
} |
Note que a view é automaticamente presa às bordas do ViewController, então constraints não são necessárias para a myView externa!
Agora, view é uma referência para minha view customizada (MyView, nesse caso). Você pode construir toda a funcionalidade da sua view em um arquivo separado, sem que o ViewController tenha que saber qualquer coisa sobre ela!
Para acessar o conteúdo da MyView, você pode castar view para seu tipo customizado:
var myView: MyView { | |
return view as! MyView | |
} |
Isso parece um pouco estranho, mas se dá pelo motivo da propriedade view continuar sendo uma UIView independente do tipo que você usar. Como o tipo da view é garantido no loadView(), o force cast é completamente seguro nesse caso.
Para evitar ter que duplicar essa lógica através dos ViewControllers, os projetos da Movile definem esse comportamento dentro de um protocolo CustomView com um associated type:
/// The HasCustomView protocol defines a customView property for UIViewControllers to be used in exchange of the regular view property. | |
/// In order for this to work, you have to provide a custom view to your UIViewController at the loadView() method. | |
public protocol HasCustomView { | |
associatedtype CustomView: UIView | |
} | |
extension HasCustomView where Self: UIViewController { | |
/// The UIViewController's custom view. | |
public var customView: CustomView { | |
guard let customView = view as? CustomView else { | |
fatalError("Expected view to be of type \(CustomView.self) but got \(type(of: view)) instead") | |
} | |
return customView | |
} | |
} |
O que resulta em:
final class MyViewController: UIViewController, HasCustomView { | |
typealias CustomView = MyView | |
override func loadView() { | |
let customView = CustomView() | |
customView.delegate = self | |
view = customView | |
} | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
customView.render() //Algum método da MyView. | |
} | |
} |
Se definir o tipo de uma CustomView todas as vezes é algo que te incomodaria, você pode ir além e definir esse comportamento dentro de uma classe genérica:
class CustomViewController<CustomView: UIView>: UIViewController { | |
var customView: CustomView { | |
return view as! CustomView | |
} | |
override func loadView() { | |
view = CustomView() | |
} | |
} | |
final class MyViewController: CustomViewController<MyView> { | |
override func loadView() { | |
super.loadView() | |
customView.delegate = self | |
} | |
} |
Eu pessoalmente não sou fã da aproximação de usar classes genéricas, já que o compilador ainda não permite que elas possuam extensões com métodos @objc, – o que te impede de ter protocolos como UITableViewDataSource em extensões. Por outro lado, elas permitem que você não precise sobrescrever loadView(), a não ser que algo especial precise ser feito, – o que realmente ajuda a manter seus ViewControllers limpos.
Conclusão
Sobrescrever loadView() é uma ótima maneira de fazer um projeto com views programáticas mais fáceis de ler e de se manter. Muitos projetos das empresas doo grupo Movile estão usando HasCustomView especificamente com ótimos resultados. View Coding pode não ser a sua praia, mas é algo que oferece muitas vantagens para seus projetos. Tente fazer um projeto com ele e veja o que funciona melhor para a sua realidade.
Siga-me no Twitter – @rockthebruno, e compartilhe comigo qualquer sugestão ou correção que você tiver!