Todo View Controller deveria ter um delegate
20 Oct 2015Qualquer pessoa que frequente o slack do iOS Dev BR sabe que ando insatisfeito com Storyboard
. Os motivos são vários mas hoje vou falar apenas de um, as segues
.
As segues
facilitam a visualização do fluxo do app para uma pessoa que não está habituada com o projeto. É só abrir o storyboard
(ou storyboards
) e está tudo lá, todas as setinhas ligando seus controllers. Ai você me pergunta: “Mas isso é lindo, porque te incomoda?”.
O que você acha desse trecho de código? Não me refiro a aquela bela string mágica, mas onde essa linha normalmente fica. Essa instrução está contida no controller A
e é executada quando o mesmo terminou seu propósito e o controller B
deve ser instanciado para continuar o fluxo.
Isso implica que o controller A
tem algum conhecimento do que deve acontecer depois dele, se eu quiser trocar o controller B
por um controller C
eu poderia manter o nome da segue
e fazer a alteração somente no storyboard
, isso seria deselegante mas não um problema. Agora imagine que o controller a ser instanciado a seguir dependa de algum resultado anterior a segue
, quem deve decidir qual segue
deve ser chamada? Na grande maioria dos códigos que vi (talvez não sejam tantos assim) o próprio controller A
é responsável por tomar essa decisão. Isso não me cheira bem (vulgo code smell), mas vamos continuar…
Bom, em qualquer app, alguma hora, você vai precisar passar informação entre os controllers e como você faz isso?
Se antes não estava cheirando bem, agora o cheiro está pior que o Rio Pinheiros ali na estação Vila Olímpia! As strings mágicas continuam por aí, temos um sender
que pode ser qualquer coisa e, por último mas o pior de todos, o controller A
sabe sobre o controller B
e controller C
. “Mas porque isso é tão ruim?” Esse acoplamento dificulta muito a substituição de qualquer uma das três classes, faz com que seja muito mais complexo para testar essa classe pois não há como fazer injeção de dependência (Dependency Injection) e esse if else if ...
interminável é deselegante (sim, eu sei que em Swift seria um switch
menos deselegante).
Depois de todo esse discurso anti-Storyboard
imagino os defensores dessa “tecnologia” estejam incomodados, para eles tenho duas coisas a dizer: Primeiro eu acredito que existem situações em que Storyboards
são adequados, projetos grandes e que vão durar muitos anos não se enquadram nessas situações (estou disposto a discutir esse assunto em uma outra ocasião); Segundo, fazendo a transição de controllers sem segue
vemos o mesmo acoplamento:
Nesse caso algo ruim que era resolvido pelo segue
acontece, o controller A
tem a responsabilidade de escolher como os outros controllers serão apresentados e ainda esperar que ele próprio esteja dentro de um UINavigationController
, nada bom.
Existe um conceito chamado de Princípio de responsabilidade única (não sei se essa seria a tradução mais adequada, Single responsibility principle), ele diz cada classe deve ter apenas uma responsabilidade ou, como diria o Agent Smith, um propósito. Imaginemos que o controller A
tenha o propósito de obter a idade do usuário, então dele deve ser criado quando o app precisar obter essa informação e a única coisa que o controller precisa fazer é obter esse informação. Não faz parte do propósito dele ter conhecimento (importar) classes que não tenham relação direta com seu propósito, em particular, classes que venham antes ou depois dele no fluxo. Muito menos decidir, com base nessa informação, qual seria o próximo passo no fluxo do app, instanciar o próximo controller e passar a informação para ele, decidir como esse controller será apresentado e apresentá-lo.
Esse é um problema que vem me incomodando faz algum tempo, há um ano li um artigo falando sobre Flow Controllers, achei interessante, ele resolve o problema da injeção de dependência, mas o controller ainda tem a responsabilidade de dizer qual é o próximo passo no fluxo do app e eu acredito que isso não faz parte do propósito dele.
Uma possível solução para isso é postular que:
- Todo
view controller
tem que ter umdelegate
; - Um
view controller
não deve usar referências aparentViewController
,navigationController
,tabBarController
,splitViewControoler
, oupresentingViewController
ou qualquer outro parent controller que inventarem; - Um
view controller
só pode fazer (#include
) de outrosview controllers
se esses forem necessários para cumprir seu propósito; - Quando um
view controller
completar seu propósito ele notifica seudelegate
e esse é responsável por continuar o fluxo; - Um
view controller
nunca deve usarsegues
, isso não faz parte do seu propósito.
Uma maneira de satisfazer essas condições é ter uma classe que é delegate
de todos os view controllers
, que sabe instanciar todos os view controller
e inclusive quais modelos são necessários para isso. Isso não me cheira muito bem, mas é melhor que antes. Como ainda é uma das primeiras interações alguma hora deve aparecer alguma idéia (aceito sugestões).
Penso que esse delegate
deve ser o primeiro controller
do app, por exemplo, uma subclasse do UINavigationController
.
Essa abordagem tem algumas vantagens:
- Pode ser uma maneira de começar a migrar um app para
Swift
pois, normalmente, a essa controller inicial é padrão e não tem muita interação com outros controllers. Além disso os novos controllers irão interagir somente com esse, além dos modelos e a camada de rede; - Todo o fluxo do app fica em apenas uma classe e não espalhado por vários lugares;
- Como o delegate sabe instanciar todos os controllers, ele pode receber o roteamento vindo de deep links ou
NSUserActivity
sem dificuldade; - Se você quiser fazer uma classe especial para mostrar notificações ou
popups
personalizados, o delegate seria o cara ideal para gerenciar quando e como eles devem ser apresentados.
Um exemplo pode deixar as coisas mais claras. Um app tem dois view controllers. O responsável pelo primeira tela (DTRootViewController
) em Objective-c
, o propósito dele é obter do usuário um texto, sua interface seria:
O segundo view controller, em Swift
, (OtherViewController
) tem como propósito mostrar um texto, e sua interface pública seria:
O view controller primário desse app (NavigationController
) é uma subclasse do UINavigationController
(também em Swift
) e sua implementação seria:
O projeto completo se encontra no github, mas olhando apenas a implementação do NavigationController
vemos que toda a lógica de fluxo de app está contida em apenas uma classe, dessa forma é possível apresentar o OtherViewController
tanto modalmente quanto dentro do navigation controller de maneira transparente, sem ter que alterar o controller apresentado.
Como eu disse anteriormente, eu ainda estou começando a utilizar essa abordagem e novos problemas e dificuldades devem aparecer com o uso. Minha intenção e fazer outros artigos sobre esse assunto conforme eu for desenvolvendo o tema, críticas, comentários e sugestões são bem vindos, o Twitter e o slack do iOS Dev BR são os canais mais fáceis.
Update 2015/10/20 13h - O Igor levantou um ponto que eu não tinha pensado, é possível se livrar sem grande dificuldade do acoplamento no prepareForSegue:sender:
usando uma subclasse da UIStoryboardSegue
, fazendo o acoplamento do controller A
com o controller B
dentro dessa classe. Acho uma solução bem razoável.
Update 2015/10/20 23h - O Fabri comentou que existem várias iniciativas como o Natalie para resolver o problema das strings mágicas, acho válido, mas preferia que houvesse alguma coisa nativa.
Update 2014/11/22 - O Tales apontou um ponto importante, nem sempre é possível usar um delegate
. Por exemplo quando o view controller é subclasse de UITableViewController
. Casos em que um delegate
não é conveniente seria melhor usar propriedades que contém blocos que são chamados no lugar dos métodos do protocolo do delegate
. Nada impede que as duas maneiras sejam implementadas.
Como o Invariante foi citado no Podcast do CocoaHeads Brasil, acho justo retribuir a gentileza ;-)
Essa semana saiu a terceira edição do Podcast semanal do CocoaHeads Brasil, nessa edição eu, Bruno Koga, Douglas Fischer e Tales Pinheiro conversamos sobre as novidades do iOS 9, o podcast está disponível no iTunes e no SoundCloud.
Diogo Tridapalli
@diogot