A Injeção de Dependências é um padrão bastante popular que permite configurar o sistema de maneira flexível e criar corretamente as dependências dos componentes deste sistema. Graças à digitação, o Swift permite usar estruturas convenientes com as quais você pode descrever brevemente o gráfico de dependência. Hoje eu quero falar um pouco sobre um desses frameworks - DITranquillity
.
Este tutorial abordará os seguintes recursos da biblioteca:
- Registo de tipo
- Implantação de inicialização
- Incorporando em uma variável
- Dependências cíclicas de componentes
- Usando a biblioteca com
UIStoryboard
Descrição do componente
O aplicativo consistirá nos seguintes componentes principais: ViewController
, Router
, Presenter
, Networking
- esses são componentes bastante comuns em qualquer aplicativo iOS.
ViewController
e o Router
serão introduzidos um no outro ciclicamente.
Preparação
Primeiro, crie um Aplicativo de exibição única no Xcode, adicione DITranquillity usando CocoaPods . Crie a hierarquia necessária de arquivos, adicione um segundo controlador ao Main.storyboard e conecte-o usando o StoryboardSegue
. Como resultado, a seguinte estrutura de arquivo deve ser obtida:
Crie dependências nas classes da seguinte maneira:
Declaração de componente protocol Presenter: class { func getCounter(completion: @escaping (Int) -> Void) } class MyPresenter: Presenter { private let networking: Networking init(networking: Networking) { self.networking = networking } func getCounter(completion: @escaping (Int) -> Void) {
protocol Networking: class { func fetchData(completion: @escaping (Result<Int, Error>) -> Void) } class MyNetworking: Networking { func fetchData(completion: @escaping (Result<Int, Error>) -> Void) {
protocol Router: class { func presentNewController() } class MyRouter: Router { unowned let viewController: ViewController init(viewController: ViewController) { self.viewController = viewController } func presentNewController() {
class ViewController: UIViewController { var presenter: Presenter! var router: Router! }
Limitações
Diferentemente de outras classes, o ViewController
não é criado por nós, mas pela biblioteca UIKit dentro da implementação UIStoryboard.instantiateViewController
, portanto, usando o storyboard, não podemos injetar dependências nos herdeiros do UIViewController
usando o inicializador. O mesmo acontece com os herdeiros do UIView
e do UITableViewCell
.
Observe que os objetos ocultos atrás dos protocolos são incorporados em todas as classes. Essa é uma das principais tarefas da implementação de dependências - criar dependências não nas implementações, mas nas interfaces. Isso ajudará no futuro a fornecer diferentes implementações de protocolo para reutilizar ou testar componentes.
Injeção de Dependência
Depois que todos os componentes do sistema são criados, prosseguimos para a conexão dos objetos entre si. No DITranquillity, o ponto de partida é o DIContainer
, que adiciona o registro usando o método container.register(...)
. Para separar as dependências em partes, DIFramework
e DIPart
, os quais devem ser implementados. Por conveniência, criaremos apenas uma classe ApplicationDependency
, que implementará o DIFramework
e servirá como local de registro para todas as dependências. A interface DIFramework
requer que você implemente apenas um método - load(container:)
.
class ApplicationDependency: DIFramework { static func load(container: DIContainer) {
Vamos começar com a inscrição mais simples que não tem dependências - MyNetworking
container.register(MyNetworking.init)
Este registro usa a implementação através do inicializador. Apesar do fato de o componente em si não ter dependências, o inicializador deve ser fornecido para deixar claro para a biblioteca como criar o componente.
Da mesma forma, registre MyPresenter
e MyRouter
.
container.register1(MyPresenter.init) container.register1(MyRouter.init)
Nota: Observe que não é o register
usado, mas o register1
. Infelizmente, isso é necessário para indicar se o objeto possui uma e apenas uma dependência no inicializador. Ou seja, se houver 0 ou duas ou mais dependências, você só precisará usar o register
. Essa restrição é um bug do Swift versão 4.0 e superior.
É hora de registrar nosso ViewController
. Ele injeta objetos não através do inicializador, mas diretamente na variável, portanto a descrição do registro será um pouco mais.
container.register(ViewController.self) .injection(cycle: true, \.router) .injection(\.presenter)
A sintaxe do formulário \.presenter
é SwiftKeyPath, graças à qual é possível implementar de forma concisa as dependências. Como o Router
e o ViewController
ciclicamente dependentes um do outro, você deve indicar explicitamente isso usando cycle: true
. A própria biblioteca pode resolver essas dependências sem uma indicação explícita, mas esse requisito foi introduzido para que a pessoa que está lendo o gráfico entenda imediatamente que existem ciclos na cadeia de dependências. Observe também que NOT ViewController.init
, mas ViewController.self
. Isso foi descrito acima na seção Limitações .
Você também deve registrar o UIStoryboard
usando um método especial.
container.registerStoryboard(name: "Main")
Agora, descrevemos todo o gráfico de dependência para uma tela. Mas ainda não há acesso a este gráfico. Você deve criar um DIContainer
que permita acessar os objetos nele.
static let container: DIContainer = { let container = DIContainer()
- Inicialize o contêiner
- Adicione uma descrição do gráfico a ele.
- Verificamos que fizemos tudo certo. Se um erro for cometido, o aplicativo falhará não durante a resolução de dependências, mas imediatamente ao criar um gráfico
Então você precisa tornar o contêiner o ponto de partida do aplicativo. Para fazer isso, implementamos o método didFinishLaunchingWithOptions
no didFinishLaunchingWithOptions
em vez de especificar Main.storyboard
como o ponto de Main.storyboard
nas configurações do projeto.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) let storyboard: UIStoryboard = ApplicationDependency.container.resolve() window?.rootViewController = storyboard.instantiateInitialViewController() window?.makeKeyAndVisible() return true }
Lançamento
No primeiro início, ocorrerá uma queda e a validação falhará pelos seguintes motivos:
- O contêiner não encontrará os tipos
Router
, Presenter
, Networking
, porque registramos apenas objetos. Se queremos dar acesso não a implementações, mas a interfaces, devemos especificar explicitamente as interfaces - O contêiner não entende como resolver a dependência cíclica, porque é necessário indicar explicitamente quais objetos não devem ser recriados toda vez que o gráfico for resolvido.
É simples corrigir o primeiro erro - existe um método especial que permite especificar em quais protocolos o método no contêiner está disponível.
container.register(MyNetworking.init) .as(check: Networking.self) {$0}
Descrevendo o registro da seguinte forma, dizemos: o objeto MyNetworking
ser acessado via protocolo de Networking
. Isso deve ser feito para todos os objetos ocultos nos protocolos. {$0}
para a verificação correta dos tipos pelo compilador.
O segundo erro é um pouco mais complicado. É necessário usar o chamado scope
, que descreve com que frequência o objeto é criado e quanto vive. Para cada registro participando de uma dependência circular, você deve especificar um scope
igual a objectGraph
. Isso deixará claro para o contêiner que, durante a resolução, é necessário reutilizar os mesmos objetos criados, em vez de criar a cada vez. Assim, verifica-se:
container.register(ViewController.self) .injection(cycle: true, \.router) .injection(\.presenter) .lifetime(.objectGraph) container.register1(MyRouter.init) .as(check: Router.self) {$0} .lifetime(.objectGraph)
Após reiniciar, o contêiner passa na validação com êxito e nosso ViewController é aberto com as dependências criadas. Você pode colocar um ponto de interrupção no viewDidLoad
e garantir.
Transição entre telas
Em seguida, crie duas classes pequenas, SecondViewController
e SecondPresenter
, adicione SecondViewController
ao storyboard e crie um Segue
entre elas com o identificador "RouteToSecond"
, que permite abrir o segundo controlador a partir do primeiro.
Adicione mais dois registros ao nosso ApplicationDependency
para cada uma das novas classes:
container.register(SecondViewController.self) .injection(\.secondPresenter) container.register(SecondPresenter.init)
.as
há necessidade de especificar .as
, porque não SecondPresenter
o SecondPresenter
por trás do protocolo, mas usamos a implementação diretamente. Então, no método viewDidAppear
do primeiro controlador, chamamos performSegue(withIdentifier: "RouteToSecond", sender: self)
, start, o segundo controlador é aberto, no qual a dependência do secondPresenter
deve ser afixada. Como você pode ver, o contêiner viu a criação de um segundo controlador a partir do UIStoryboard
e UIStoryboard
êxito as dependências.
Conclusão
Essa biblioteca permite que você trabalhe convenientemente com dependências cíclicas, storyboards e use totalmente a inferência automática de tipo no Swift, que fornece uma sintaxe muito curta e flexível para descrever o gráfico de dependência.
Referências
Código de amostra completo na biblioteca do github
DITranquillity no github
Artigo em inglês