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

165

Sendo novo no desenvolvimento de objetiva c, cacau e iPhone em geral, tenho um forte desejo de tirar o máximo proveito da linguagem e das estruturas.

Um dos recursos que estou usando são as notas da classe CS193P de Stanford que elas deixaram na web. Ele inclui notas de aula, tarefas e código de exemplo e, como o curso foi ministrado pelos desenvolvedores da Apple, eu definitivamente considero "da boca do cavalo".

Site da classe:
http://www.stanford.edu/class/cs193p/cgi-bin/index.php

A aula 08 está relacionada a uma atribuição para criar um aplicativo baseado em UINavigationController que possui vários UIViewControllers enviados para a 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 slide sério:
http://cs193p.stanford.edu/downloads/08-NavigationTabBarControllers.pdf

Page 16/51:

Como não compartilhar dados

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

Está bem. Eu estou afim disso. Não jogue cegamente todos os seus métodos que serão usados ​​para a comunicação entre o viewcontroller no delegado do aplicativo e faça referência às instâncias do viewcontroller nos métodos de delegação do aplicativo. Fair 'nuff.

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

Page 18/51:

Práticas recomendadas para fluxo de dados

  • Descobrir exatamente o que precisa ser comunicado
  • Defina os parâmetros de entrada para o seu controlador de exibição
  • Para comunicar o backup da hierarquia, use acoplamentos soltos
    • Definir uma interface genérica para observadores (como delegação)

Em seguida, este slide é seguido pelo que parece ser um slide de espaço reservado, onde o professor 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. Também estou um pouco confuso com a linha final na citação acima. Eu tenho pesquisado bastante sobre isso no Google e descobri o que parece ser um artigo decente falando sobre os vários métodos das técnicas de Observação / Notificação:
http://cocoawithlove.com/2008/06/five-approaches-to -listening-observing.html

O método nº 5 indica até delegados como um método! Exceto .... os objetos podem definir apenas um delegado por vez. Então, quando tenho uma comunicação múltipla com o controlador de exibição, o que devo fazer?

Ok, essa é a gangue criada. Eu sei que posso executar facilmente meus métodos de comunicação no delegado do aplicativo por referência às várias instâncias do controlador de exibição no meu appdelegate, mas quero fazer esse tipo de coisa da maneira certa .

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

  1. Quando estou tentando enviar por push um novo viewcontroller na pilha UINavigationController, quem deve fazer esse envio. Qual classe / arquivo no meu código é o local correto?
  2. Quando quero afetar alguns dados (valor de um iVar) em um dos meus UIViewControllers quando estou em um UIViewController diferente , qual é a maneira "certa" de fazer isso?
  3. Considere que só podemos ter um delegado definido por vez em um objeto, como seria a implementação quando o palestrante disser "Definir uma interface genérica para observadores (como delegação)" . Um exemplo de pseudocódigo seria muito útil aqui, se possível.
Quinn Taylor
fonte
Parte disso é abordada neste artigo pela Apple - developer.apple.com/library/ios/#featuredarticles/…
James Moore
Apenas uma observação rápida: os vídeos da classe Stanford CS193P já estão disponíveis no iTunes U. O mais recente (2012-13) pode ser visto em itunes.apple.com/us/course/coding-together-developing/… e espero que os vídeos e slides futuros serão anunciados em cs193p.stanford.edu
Thomas Watson

Respostas:

224

Essas são boas perguntas, e é ótimo ver que você está fazendo essa pesquisa e parece preocupado em aprender como "fazer o certo" em vez de apenas fazer um hack.

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

Segundo , consulte a página 10 da apresentação de Stanford para obter 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, consulte este tutorial .

Terceiro , e talvez o 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 sobre elas no contexto do padrão de design da "injeção de dependência". Em poucas palavras, isso significa que seu controlador não deve "procurar" os objetos necessários para realizar seu trabalho (por exemplo, referenciar uma variável global). Em vez disso, você deve sempre "injetar" essas dependências no controlador (ou seja, passar os objetos necessários por meio de métodos).

Se você seguir o padrão de injeção de dependência, seu controlador será modular e reutilizável. E se você pensar de onde vêm os apresentadores de Stanford (como funcionários da Apple, o trabalho deles é criar classes que possam ser facilmente reutilizadas), a reutilização e a modularidade são grandes prioridades. Todas as práticas recomendadas mencionadas para o compartilhamento de dados fazem parte da injeção de dependência.

Essa é a essência da minha resposta. Vou incluir 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 exibição

Digamos que você esteja criando uma tela na qual vários livros estão listados. O usuário pode escolher os livros que deseja comprar e tocar no botão "checkout" para ir para a tela de checkout.

Para criar isso, você pode criar uma classe BookPickerViewController que controla e exibe os objetos GUI / view. Onde ele obterá todos os dados do livro? Digamos que depende de um objeto BookWarehouse para isso. Portanto, agora seu controlador está basicamente intermediando dados entre um objeto de modelo (BookWarehouse) e os objetos GUI / view. Em outras palavras, BookPickerViewController DEPENDE no objeto 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 dependências 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 funcionários da Apple estão falando sobre o uso do padrão de delegação para "se comunicar de volta na 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 escolhe seus livros e está pronto para fazer o check-out? Bem, esse não é realmente o seu trabalho. DELEGATE que funcione para outro objeto, o que significa que DEPENDE de outro objeto. 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 final de tudo isso é que você pode me fornecer sua classe BookPickerViewController (e objetos relacionados da GUI / exibição) e eu posso usá-la facilmente em meu próprio aplicativo, assumindo que BookWarehouse e CheckoutController são interfaces genéricas (ou seja, protocolos) que eu posso implementar :

@interface MyBookWarehouse : NSObject <BookWarehouse> { ... } @end
@implementation MyBookWarehouse { ... } @end

@interface MyCheckoutController : NSObject <CheckoutController> { ... } @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];
}

Por fim, o BookPickerController não é apenas reutilizável, mas 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
   ...
}
Clint Harris
fonte
19
Quando vejo perguntas (e respostas) assim, criadas com tanto cuidado, não consigo deixar de sorrir. Parabéns merecido ao nosso intrépido interlocutor e a você !! Enquanto isso, eu queria compartilhar um link atualizado para o útil link invasivecode.com que você referenciou em seu segundo ponto: invasivecode.com/2009/09/… - Mais uma vez, obrigado por compartilhar suas idéias e práticas recomendadas, além de fazer o backup com exemplos!
9339 Joe D'Andrea
Concordo. A pergunta foi bem formada e a resposta foi simplesmente fantástica. Em vez de apenas ter uma resposta técnica, também incluía alguma psicologia por trás de como / por que é implementada usando DI. Obrigado! +1 em cima.
Kevin Elliott
E se você também desejar usar o BookPickerController para escolher um livro para uma lista de desejos ou um dos vários motivos possíveis para escolher um livro. Você ainda usaria a abordagem da interface CheckoutController (talvez renomeada para algo como BookSelectionController) ou talvez usasse o NSNotificationCenter?
Les
Isso ainda está bastante acoplado. Criar e consumir eventos de um local centralizado seria mais flexível.
Neil McGuigan
1
A ligação referenciado no ponto 2 parece ter mudado novamente - aqui está a trabalhar ligação invasivecode.com/blog/archives/322
vikmalhotra
15

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

Dito isto, eu sempre prefiro fazer minha coordenação (# 2) através de objetos de modelo. O controlador de exibiçã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 objetos de modelo eles precisam trabalhar. A maioria das alterações é comunicada de volta à hierarquia usando o NSNotificationCenter; disparar as notificações geralmente é incorporado ao próprio modelo.

Por exemplo, suponha que eu tenha um aplicativo com contas e transações. Eu também tenho um AccountListController, um AccountController (que exibe um resumo da conta com um botão "mostrar todas as transações"), 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 do AccountController e empurra o AccountController para a pilha. Quando você toca no botão "mostrar todas as transações", o AccountController carrega a lista de transações, a coloca na propriedade .transactions do TransactionListController e empurra o TransactionListController para a pilha, e assim por diante.

Se, por exemplo, o TransactionController edita a transação, ele faz a alteração no objeto de transação e chama o 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 se atualizará. TransactionListController presumivelmente faria; AccountController e AccountListController podem, dependendo do que eles estão tentando fazer.

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

Para o # 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 realmente salvar a transação que está sendo editada, pois o usuário pode decidir abandonar a transação. Em vez disso, os dois tomam o controlador pai como 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 <EditorDelegate> delegate;
}
@property (retain) Model * model;
@property (assign) id <EditorDelegate> 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 <EditorDelegate> {
    AmountEditor * amountEditor;
    TextEditor * textEditor;
    Transaction * transaction;
}
...properties and methods here...
@end

E agora alguns métodos 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 importante a ser observado é que definimos um protocolo genérico que os editores podem usar para se comunicar com seu próprio controlador. Ao fazer isso, podemos reutilizar os Editores em outra parte do aplicativo. (Talvez as contas também possam ter anotações.) É claro que o protocolo EditorDelegate poderia conter mais de um método; neste caso, esse é o único necessário.

Brent Royal-Gordon
fonte
1
Isso deveria funcionar como está? Estou tendo problemas com o Editor.delegatemembro. No meu viewDidLoadmétodo, estou recebendo Property 'delegate' not found.... Só não tenho certeza se estraguei outra coisa. Ou se isso for abreviado por questões de brevidade.
Jeff
Agora esse é um código bastante antigo, escrito em um estilo mais antigo com convenções mais antigas. Não copio e colo diretamente no seu projeto; Eu apenas tentaria aprender com os padrões.
Brent Royal-Gordon
Peguei vocês. Isso é exatamente o que eu queria saber. Consegui trabalhar com algumas modificações, mas fiquei um pouco preocupado por não corresponder literalmente.
Jeff
0

Eu vejo o seu problema ..

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

O MVC possui três partes. Modelos, visualizações e controladores. O problema declarado parece ter combinado dois deles sem uma boa razão. visualizações e controladores são partes separadas da lógica.

então ... você não deseja ter vários controladores de exibição ..

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

visualizações NÃO devem tomar decisões. O (s) controlador (es) devem fazer isso. Daí a separação de tarefas, lógica e maneiras de facilitar sua vida.

Portanto, certifique-se de que sua visão apenas faça isso, divulgue uma boa veiw 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 ... uma boa maneira padrão de ser atrapalhada, acessada, modificada .. outra parte da lógica separada que podemos separar e esquecer)

Bingy
fonte
0

Suponha que haja duas classes A e B.

instância da classe A é

Uma Instância;

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

B Instância;

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

1) Caminho errado

Você pode passar aInstance para bInstance. Agora, faça a chamada do método desejado [aInstance methodname] do local desejado em bInstance.

Isso serviria ao seu propósito, mas, embora a liberação, levasse a uma memória sendo bloqueada e não liberada.

Quão?

Quando você passou de aInstance para bInstance, aumentamos a contagem de retenção de aInstance em 1. Ao desalocar bInstance, teremos memória bloqueada porque aInstance nunca pode ser levada a 0 número de retenção por bInstance, pelo que a própria bInstance é um objeto de aInstance.

Além disso, devido a uma Instância estar bloqueada, a memória de bInstance também ficará bloqueada (vazada). Portanto, mesmo depois de desalocar a própria Instância quando chegar a hora, sua memória também será bloqueada porque bInstance não pode ser liberada e bInstance é uma variável de classe de aInstance.

2) Caminho certo

Ao definir aInstance como o delegado da bInstance, não haverá alteração na contagem de retenção ou emaranhamento de memória de aInstance.

bInstance poderá invocar livremente os métodos delegados que estão na aInstance. Na desalocação de bInstance, todas as variáveis ​​serão criadas por si próprias e serão liberadas Na desalocação de aInstance, pois não há emaranhamento de aInstance em bInstance, ela será liberada de forma limpa.

rd_
fonte