Qual é a melhor maneira de se comunicar entre os controladores de visualização?

Sendo novo no objective-c, cacau e iPhone dev em geral, eu tenho um forte desejo de tirar o máximo proveito da linguagem e dos frameworks.

Um dos resources que estou usando é o da class CS193P, da Stanford, que eles deixaram na web. Ele inclui notas de aula, tarefas e código de exemplo, e desde que o curso foi dado pela Apple dev, eu definitivamente considero que seja “da boca do cavalo”.

Website da turma:
http://www.stanford.edu/class/cs193p/cgi-bin/index.php

Aula 08 está relacionada a uma atribuição para criar um aplicativo baseado em UINavigationController que tenha vários UIViewControllers colocados na pilha UINavigationController. É assim que o UINavigationController funciona. Isso é lógico. No entanto, existem alguns avisos severos no slide sobre a comunicação entre seus UIViewControllers.

Vou citar este grave dos slides:
http://cs193p.stanford.edu/downloads/08-NavigationTabBarControllers.pdf

Página 16/51:

Como não compartilhar dados

  • Variáveis ​​globais ou singletons
    • Isso inclui seu delegado de aplicativo
  • Dependências diretas tornam seu código menos reutilizável
    • E mais difícil de depurar e testar

Está bem. Eu estou triste com isso. Não lance cegamente todos os seus methods que serão usados ​​para comunicação entre o viewcontroller e o delegado do aplicativo e faça referência às instâncias do viewcontroller nos methods delegates do aplicativo. Nuff justo.

Um pouco mais adiante, recebemos este slide nos dizendo o que devemos fazer.

Página 18/51:

Melhores práticas para stream de dados

  • Descubra exatamente o que precisa ser comunicado
  • Definir parâmetros de input para seu controlador de visualização
  • Para comunicação de backup da hierarquia, use o acoplamento solto
    • Definir uma interface genérica para observadores (como delegação)

Este slide é seguido pelo que parece ser um slide de espaço reservado, onde o palestrante aparentemente demonstra as melhores práticas usando um exemplo com o UIImagePickerController. Eu gostaria que os vídeos estivessem disponíveis! 🙁

Ok, então … Eu tenho medo que meu objc-fu não seja tão forte. Eu também estou um pouco confuso com a linha final na citação acima. Eu tenho feito o meu quinhão de googling sobre isso e eu encontrei o que parece ser um artigo decente falando sobre os vários methods de técnicas de observação / notificação:
http://cocoawithlove.com/2008/06/five-approaches-to- listening -observing.html

O método 5 indica até delegates como um método! Exceto …. objects só podem definir um delegado de cada vez. Então, quando tenho várias comunicações do viewcontroller, o que devo fazer?

Ok, essa é a gangue criada. Eu sei que posso facilmente fazer meus methods de comunicação no delegado app por referência a várias instâncias de viewcontroller no meu appdelegate, mas eu quero fazer esse tipo de coisa do jeito certo .

Por favor, ajude-me a “fazer a coisa certa”, respondendo às seguintes perguntas:

  1. Quando eu estou tentando empurrar um novo viewcontroller na pilha UINavigationController, quem deve estar fazendo esse push. Qual class / arquivo no meu código é o lugar correto?
  2. Quando eu quero afetar algum pedaço de dados (valor de um iVar) em um dos meus UIViewControllers quando estou em um UIViewController diferente , qual é a maneira “certa” de fazer isso?
  3. Damos que só podemos ter um delegado definido de cada vez em um object, como seria a implementação quando o palestrante dissesse “Definir uma interface genérica para observadores (como delegação)” . Um exemplo de pseudocódigo seria muito útil aqui, se possível.

Essas são boas perguntas, e é ótimo ver que você está fazendo essa pesquisa e parece preocupado em aprender como “fazer certo”, em vez de apenas juntá-las.

Primeiro , concordo com as respostas anteriores, que se concentram na importância de colocar dados em objects de modelo quando apropriado (de acordo com o padrão de design do MVC). Normalmente, você quer evitar colocar informações de estado dentro de um controlador, a menos que sejam dados estritamente de “apresentação”.

Segundo , veja a página 10 da apresentação de Stanford para um exemplo de como empurrar programaticamente um controlador para o controlador de navegação. Para um exemplo de como fazer isso “visualmente” usando o Interface Builder, dê uma olhada neste tutorial .

Terceiro , e talvez mais importante, observe que as “melhores práticas” mencionadas na apresentação de Stanford são muito mais fáceis de entender se você pensar nelas no contexto do padrão de design de “injeção de dependência”. Em suma, isso significa que seu controlador não deve “procurar” os objects necessários para fazer seu trabalho (por exemplo, referenciar uma variável global). Em vez disso, você deve sempre “injetar” essas dependencies no controlador (isto é, passar os objects necessários por methods).

Se você seguir o padrão de injeção de dependência, seu controlador será modular e reutilizável. E se você pensar sobre de onde os apresentadores de Stanford estão vindo (ou seja, como os funcionários da Apple seu trabalho é construir classs que possam ser facilmente reutilizadas), reusabilidade e modularidade são prioridades altas. Todas as práticas recomendadas mencionadas para compartilhar dados fazem parte da injeção de dependência.

Essa é a essência da minha resposta. Vou include um exemplo de uso do padrão de injeção de dependência com um controlador abaixo, caso seja útil.

Exemplo de Uso de Injeção de Dependência com um Controlador de Visualização

Digamos que você esteja construindo uma canvas na qual vários livros estão listados. O usuário pode escolher os livros que deseja comprar e, em seguida, tocar em um botão “checkout” para acessar a canvas de checkout.

Para construir isso, você pode criar uma class BookPickerViewController que controle e exiba os objects GUI / view. Onde irá obter todos os dados do livro? Vamos dizer que isso depende de um object BookWarehouse para isso. Portanto, agora seu controlador está basicamente intermediando dados entre um object de modelo (BookWarehouse) e os objects GUI / view. Em outras palavras, BookPickerViewController DEPENDS no object BookWarehouse.

Não faça isso:

@implementation BookPickerViewController -(void) doSomething { // I need to do something with the BookWarehouse so I'm going to look it up // using the BookWarehouse class method (comparable to a global variable) BookWarehouse *warehouse = [BookWarehouse getSingleton]; ... } 

Em vez disso, as dependencies devem ser injetadas assim:

 @implementation BookPickerViewController -(void) initWithWarehouse: (BookWarehouse*)warehouse { // myBookWarehouse is an instance variable myBookWarehouse = warehouse; [myBookWarehouse retain]; } -(void) doSomething { // I need to do something with the BookWarehouse object which was // injected for me [myBookWarehouse listBooks]; ... } 

Quando os caras da Apple estão falando sobre o uso do padrão de delegação para “comunicar de volta a hierarquia”, eles ainda estão falando sobre injeção de dependência. Neste exemplo, o que o BookPickerViewController deve fazer depois que o usuário escolher seus livros e estiver pronto para fazer check-out? Bem, isso não é realmente o seu trabalho. Deve DELEGAR esse trabalho para algum outro object, o que significa que DEPENDE de outro object. Portanto, podemos modificar nosso método init BookPickerViewController da seguinte maneira:

 @implementation BookPickerViewController -(void) initWithWarehouse: (BookWarehouse*)warehouse andCheckoutController:(CheckoutController*)checkoutController { myBookWarehouse = warehouse; myCheckoutController = checkoutController; } -(void) handleCheckout { // We've collected the user's book picks in a "bookPicks" variable [myCheckoutController handleCheckout: bookPicks]; ... } 

O resultado líquido de tudo isso é que você pode me dar sua class BookPickerViewController (e objects GUI / view relacionados) e posso facilmente usá-la em meu próprio aplicativo, supondo que BookWarehouse e CheckoutController sejam interfaces genéricas (ou seja, protocolos) que eu possa implementar :

 @interface MyBookWarehouse : NSObject  { ... } @end @implementation MyBookWarehouse { ... } @end @interface MyCheckoutController : NSObject  { ... } @end @implementation MyCheckoutController { ... } @end ... -(void) applicationDidFinishLoading { MyBookWarehouse *myWarehouse = [[MyBookWarehouse alloc]init]; MyCheckoutController *myCheckout = [[MyCheckoutController alloc]init]; BookPickerViewController *bookPicker = [[BookPickerViewController alloc] initWithWarehouse:myWarehouse andCheckoutController:myCheckout]; ... [window addSubview:[bookPicker view]]; [window makeKeyAndVisible]; } 

Finalmente, o BookPickerController não só é reutilizável como também mais fácil de testar.

 -(void) testBookPickerController { MockBookWarehouse *myWarehouse = [[MockBookWarehouse alloc]init]; MockCheckoutController *myCheckout = [[MockCheckoutController alloc]init]; BookPickerViewController *bookPicker = [[BookPickerViewController alloc] initWithWarehouse:myWarehouse andCheckoutController:myCheckout]; ... [bookPicker handleCheckout]; // Do stuff to verify that BookPickerViewController correctly called // MockCheckoutController's handleCheckout: method and passed it a valid // list of books ... } 

Esse tipo de coisa é sempre uma questão de gosto.

Dito isto, eu sempre prefiro fazer a minha coordenação (# 2) através de objects de modelo. O controlador de visualização de nível superior carrega ou cria os modelos necessários, e cada controlador de exibição define propriedades em seus controladores filhos para informar com quais objects de modelo eles precisam trabalhar. A maioria das alterações é comunicada de volta à hierarquia usando o NSNotificationCenter; triggersr as notifications geralmente é incorporado ao próprio modelo.

Por exemplo, suponha que eu tenha um aplicativo com contas e transactions. Eu também tenho um AccountListController, um AccountController (que exibe um resumo de conta com um botão “show all transactions”), um TransactionListController e um TransactionController. AccountListController carrega uma lista de todas as contas e as exibe. Quando você toca em um item da lista, ele define a propriedade .account de seu AccountController e coloca o AccountController na pilha. Quando você toca no botão “mostrar todas as transactions”, o AccountController carrega a lista de transactions, coloca-a na propriedade .transactions do TransactionListController e envia o TransactionListController para a pilha, e assim por diante.

Se, digamos, TransactionController edita a transação, ela faz a alteração em seu object de transação e, em seguida, chama seu método ‘save’. ‘save’ envia uma TransactionChangedNotification. Qualquer outro controlador que precise se atualizar quando a transação for alterada observará a notificação e a própria atualização. TransactionListController presumivelmente faria; AccountController e AccountListController podem, dependendo do que eles estavam tentando fazer.

Para o número 1, nos meus primeiros aplicativos eu tinha algum tipo de método displayModel: withNavigationController: no controlador filho que configurava as coisas e colocava o controlador na pilha. Mas, à medida que me sinto mais à vontade com o SDK, afastei-me disso e agora geralmente faço com que os pais pressionem o filho.

Para o nº 3, considere este exemplo. Aqui estamos usando dois controladores, AmountEditor e TextEditor, para editar duas propriedades de uma transação. Os editores não devem salvar a transação que está sendo editada, pois o usuário pode decidir abandonar a transação. Então, ao invés disso, ambos pegam seu controlador pai como um delegado e chamam um método dizendo se eles mudaram alguma coisa.

 @class Editor; @protocol EditorDelegate // called when you're finished. updated = YES for 'save' button, NO for 'cancel' - (void)editor:(Editor*)editor finishedEditingModel:(id)model updated:(BOOL)updated; @end // this is an abstract class @interface Editor : UIViewController { id model; id  delegate; } @property (retain) Model * model; @property (assign) id  delegate; ...define methods here... @end @interface AmountEditor : Editor ...define interface here... @end @interface TextEditor : Editor ...define interface here... @end // TransactionController shows the transaction's details in a table view @interface TransactionController : UITableViewController  { AmountEditor * amountEditor; TextEditor * textEditor; Transaction * transaction; } ...properties and methods here... @end 

E agora alguns methods do TransactionController:

 - (void)viewDidLoad { amountEditor.delegate = self; textEditor.delegate = self; } - (void)editAmount { amountEditor.model = self.transaction; [self.navigationController pushViewController:amountEditor animated:YES]; } - (void)editNote { textEditor.model = self.transaction; [self.navigationController pushViewController:textEditor animated:YES]; } - (void)editor:(Editor*)editor finishedEditingModel:(id)model updated:(BOOL)updated { if(updated) { [self.tableView reloadData]; } [self.navigationController popViewControllerAnimated:YES]; } 

O que devemos notar é que definimos um protocolo genérico que os Editores podem usar para se comunicar com seu controlador de propriedade. Ao fazer isso, podemos reutilizar os Editores em outra parte do aplicativo. (Talvez as contas também possam ter notas.) É claro que o protocolo EditorDelegate pode conter mais de um método; Neste caso, é o único necessário.

Eu vejo o seu problema

O que aconteceu é que alguém confundiu a ideia da arquitetura MVC.

O MVC tem três partes: modelos, visualizações e controladores. O problema declarado parece ter combinado dois deles sem um bom motivo. visualizações e controladores são partes separadas da lógica.

então … você não quer ter múltiplos controladores de visão ..

você deseja ter várias visualizações e um controlador que escolha entre elas. (você também pode ter vários controladores, se você tiver vários aplicativos)

as visualizações não devem tomar decisões. O (s) controlador (es) devem fazer isso. Daí a separação de tarefas, lógica e formas de tornar sua vida mais fácil.

Então, certifique-se de que sua visualização apenas faça isso, coloque um bom texto dos dados. deixe seu controlador decidir o que fazer com os dados e qual visualização usar.

(e quando falamos sobre dados, estamos falando sobre o modelo … um bom modo padrão de ser estocado, acessado, modificado … outro pedaço separado de lógica que podemos dividir e esquecer)

Suponha que haja duas classs A e B.

instância da class A é

Ainstância;

class A faz e instância da class B, como

BInstance;

E na sua lógica da class B, em algum lugar você é obrigado a se comunicar ou acionar um método da class A.

1) caminho errado

Você poderia passar o ainstance para instâncias. agora coloque a chamada do método desejado [a method methodname] do local desejado em bInstance.

Isso serviria ao seu propósito, mas enquanto a liberação levaria a uma memory sendo bloqueada e não liberada.

Como?

Quando você passou do aInstance para Instância, aumentamos a retenção de uma Instância para 1. Ao desalocarmos, teremos a memory bloqueada, porque a Instância em si não pode ser trazida para zero, porque a própria Instância é um object de uma Instância.

Além disso, por causa de uma Instância que está sendo presa, a memory do bInstance também estará presa (vazada). Assim, mesmo depois de desalocar a própria Instância quando sua hora chegar mais tarde, sua memory também será bloqueada porque ela não pode ser liberada e a instancia é uma variável de class de Instância.

2) caminho certo

Definindo-se uma Instância como o delegado de Instância, não haverá mudança de retenção ou entrelaçamento de memory de uma Instância.

bInstance será capaz de invocar livremente os methods delegates que se encontram no aInstance. Na desalocação da empresa, todas as variables ​​serão criadas por conta própria e serão liberadas pela desalfandegamento da Instituição, pois não há emaranhamento de uma Instância, pois ela será liberada de forma limpa.