Este artigo foi publicado originalmente em: https://www.concrete.com.br/2018/09/07/como-usar-o-state-design-pattern-para-criar-um-sateful-viewcontroller/
***
O que diabos é um ViewController Stateful? Um ViewController Stateful é o nome dado por nós para um ViewController que tem muitos estados e deve ser capaz de alterar suas views ou componentes com base em seu estado atual.
Criar um pode ser complicado. Primeiro podemos ter diferentes telas de loadings e/ou telas de erro. Por exemplo, em nosso LoginViewController temos um loading dentro do nosso UIButton e as mensagens de erro devem ser tratadas com um alerta personalizado. Nosso HomeViewController deve mostrar uma tela de loading e uma de erro padrão que sabe como tratar com um Error.
Queremos construí-lo de forma que não precisemos repetir códigos ou comportamentos em muitos view controllers.
Podemos usar um State Design Pattern para lidar com isso. O State Pattern é, por definição:
Um padrão de design de software comportamental que permite que um objeto altere seu comportamento quando seu estado interno é alterado. O State Pattern pode ser interpretado como um padrão de Strategy capaz de alternar a estratégia atual, por meio de invocações de métodos definidos na interface.
Definindo nosso estado
Definimos alguns estados como .success, .loading e .error (error) e achamos que ia ser ótimo criar um protocolo e adicioná-lo ao nosso ViewController.
enum State {
case success
case loading
case error(error: Error)
}
protocol Statable {
func update(state: State)
}
class MyViewController: UIViewController, Statable {
func update(state: State) {
switch(state) {
case .success:
//show success view
case .loading:
//show loading view
case .error(error):
//show error view
}
}
}
O problema dessa abordagem é que todo o view controller vai ter um switch case gigante e provavelmente um código repetido.
- “Sim! Sim, mas pode resolver essa criação funcs”
- Ok! Mas isso não vai resolver nosso problema, apenas vai disfarçá-lo
Um outro problema é: “O que acontece se adicionarmos outro estado?”
Sim! Precisamos adicioná-lo a cada switch ou criar um default. Adicionar outro estado vai fazer com que tenhamos que modificar todas as classes que usam nosso protocolo Statable, quebrando o princípio de aberto-fechado.
Criando um protocolo de estado único
Pensamos que seria melhor se mudássemos nosso ‘enum’ e ‘protocol’ para um novo ‘protocol’:
protocol Statable {
func setSuccessState()
func setLoadingState()
func setErrorState(error: Error)
}
class MyViewController: UIViewController, Statable {
func setSuccessState() {
//set success state
}
func setLoadingState() {
//set loading state
}
func setErrorState(error: Error) {
//set error state
}
}
Muito melhor agora! Isso eliminou o problema do ‘switch case’, mas não era exatamente o que queríamos.
E agora todos os view controllers precisam ter esses três métodos, mesmo que tenham uma tela de loading ou erro padrão. Em outras palavras, ainda estamos repetindo o código.
Costumo dizer que: se você tiver um método de protocolo que tenha uma implementação default, ou que não seja usado em todas as classes/structs, esse método não deveria estar lá.
Quebrando o nosso protocolo
Alguns view controllers não precisam implementar ‘setLoadingState ()’ ou ‘setErrorState (error: Error)’, isso vai ser tratado por classes padrão. Então decidimos dividir o nosso protocolo em três.
protocol SuccessStatable {
func setSuccessState()
}
protocol LoadingStatable {
func setLoadingState()
}
protocol ErrorStatable {
func setErrorState(error: Error)
}
Isso vai ser ótimo, porque podemos criar visualizações de padrão de erros e de carregamento:
Default loading view
class DefaultLoadingState: LoadingStatable {
let viewController: UIViewController
init(viewController: UIViewController) {
self.viewController = viewController
}
func setLoadingState() {
viewController.view = DefaultLoadingView()
}
}
Default error view
class DefaultErrorState: ErrorStatable {
let viewController: UIViewController
init(viewController: UIViewController) {
self.viewController = viewController
}
func setErrorState(error: Error) {
viewController.view = DefaultErrorView(error: error)
}
}
Manipulando Default States
Finalmente podemos criar um Decorator no nosso ViewController para lidar com todos os estados e alternar entre eles sempre que precisarmos.
class StatableViewController: UIViewController {
var currentViewController: UIViewController
var loadingStatable: LoadingStatable?
var successStatable: SuccessStatable?
var errorStatable: ErrorStatable?
init(currentViewController: UIViewController,
loadingStatable: LoadingStatable? = nil,
successStatable: SuccessStatable? = nil,
errorStatable: ErrorStatable? = nil) {
self.currentViewController = currentViewController
self.successStatable = successStatable
self.loadingStatable = loadingStatable
self.errorStatable = errorStatable
super.init(nibName: nil, bundle: nil)
setup()
successStatable?.setSuccessState()
}
private func setup() {
if loadingStatable == nil {
loadingStatable = DefaultLoadingState(viewController: currentViewController)
}
if errorStatable == nil {
errorStatable = DefaultErrorState(viewController: currentViewController)
}
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setLoadingState() {
loadingStatable?.setLoadingState()
}
func setSuccessState() {
successStatable?.setSuccessState()
}
func setErrorState(error: Error) {
errorStatable?.setErrorState(error: error)
}
}
let allStateViewController = AllStateViewController()
let successViewController = SuccessViewController()
let fullStateViewController = StatableViewController(currentViewController = allStateViewController,
loadingStatable = allStateViewController,
successStatable = allStateViewController,
errorStatable = allStateViewController)
let successStateViewController = StatableViewController(currentViewController = allStateViewController,
successStatable = allStateViewController)
fullStateViewController.setLoadingState() //will call a custom loading
successStateViewController.setLoadingState() //will call our default loading
Conclusão
Com essa abordagem, podemos ter exibições de erros e loading personalizados quando precisarmos e implementações padrão quando uma personalizada não é necessária. Podemos criar muitas classes diferentes para lidar com loadings, tornando nosso DefaultLoadingState fechado para modificação e aberto para extensões.
Nós misturamos o State Design Pattern e o Decorator Pattern para criar um StatableViewController que vai ser responsável por manipular os estados do nosso view. Usando a injeção de dependência, podemos alterar os comportamentos de loading e o erro de nossos controllers em um único ponto de entrada.
Se você gostou deste artigo, compartilhe no Twitter, recomende ele, ou ambos. Isso realmente me ajuda a alcançar mais pessoas.
Muito obrigado!