iOS

11 out, 2018

Como usar o State Design Pattern para criar um Stateful ViewController

Publicidade

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!