¿Cuál es la mejor manera de comunicarse entre los controladores de vista?


165

Siendo nuevo en el desarrollo de Objective-C, Cocoa y iPhone en general, tengo un fuerte deseo de aprovechar al máximo el lenguaje y los marcos.

Uno de los recursos que estoy usando son las notas de clase CS193P de Stanford que han dejado en la web. Incluye notas de clase, tareas y código de muestra, y dado que el curso fue impartido por los desarrolladores de Apple, definitivamente lo considero "de boca del caballo".

Sitio web de la clase:
http://www.stanford.edu/class/cs193p/cgi-bin/index.php

La lección 08 está relacionada con una tarea para construir una aplicación basada en UINavigationController que tiene múltiples UIViewControllers introducidos en la pila de UINavigationController. Así es como funciona el UINavigationController. Eso es logico. Sin embargo, hay algunas advertencias severas en la diapositiva sobre la comunicación entre sus UIViewControllers.

Voy a citar de esta serie de diapositivas:
http://cs193p.stanford.edu/downloads/08-NavigationTabBarControllers.pdf

Página 16/51:

Cómo no compartir datos

  • Variables globales o singletons
    • Esto incluye su delegado de aplicación
  • Las dependencias directas hacen que su código sea menos reutilizable
    • Y más difícil de depurar y probar

Okay. Estoy abajo con eso. No arroje ciegamente todos sus métodos que se utilizarán para comunicarse entre el viewcontroller en su delegado de aplicaciones y haga referencia a las instancias de viewcontroller en los métodos de delegado de aplicaciones. Justo 'nuff.

Un poco más adelante, obtenemos esta diapositiva nos dice lo que debemos hacer.

Página 18/51:

Mejores prácticas para el flujo de datos

  • Averigüe exactamente lo que debe comunicarse
  • Defina los parámetros de entrada para su controlador de vista
  • Para comunicar una copia de seguridad de la jerarquía, use acoplamiento flexible
    • Definir una interfaz genérica para observadores (como delegación)

Luego, esta diapositiva es seguida por lo que parece ser una diapositiva de marcador de posición donde el profesor aparentemente demuestra las mejores prácticas utilizando un ejemplo con el UIImagePickerController. ¡Ojalá los videos estuvieran disponibles! :(

Ok, entonces ... me temo que mi objc-fu no es tan fuerte. También estoy un poco confundido por la línea final en la cita anterior. He estado haciendo una buena parte de googlear sobre esto y encontré lo que parece ser un artículo decente que habla sobre los diversos métodos de técnicas de Observación / Notificación:
http://cocoawithlove.com/2008/06/five-approaches-to -listening-observing.html

¡El método # 5 incluso indica delegados como método! Excepto ... los objetos solo pueden establecer un delegado a la vez. Entonces, cuando tengo comunicación de múltiples controles de vista, ¿qué debo hacer?

Ok, esa es la pandilla preparatoria. Sé que puedo hacer fácilmente mis métodos de comunicación en el delegado de la aplicación por referencia a las múltiples instancias del controlador de vista en mi delegado de la aplicación, pero quiero hacer este tipo de cosas de la manera correcta .

Por favor, ayúdame a "hacer lo correcto" respondiendo las siguientes preguntas:

  1. Cuando estoy tratando de empujar un nuevo viewcontroller en la pila UINavigationController, quién debería estar haciendo este empuje. ¿Qué clase / archivo en mi código es el lugar correcto?
  2. Cuando quiero afectar algún dato (valor de un iVar) en uno de mis UIViewControllers cuando estoy en un UIViewController diferente , ¿cuál es la forma "correcta" de hacer esto?
  3. Tenga en cuenta que solo podemos tener un delegado establecido a la vez en un objeto, ¿cómo sería la implementación cuando el profesor diga "Definir una interfaz genérica para los observadores (como la delegación)" . Un ejemplo de pseudocódigo sería muy útil aquí si es posible.

Algo de esto se aborda en este artículo de Apple - developer.apple.com/library/ios/#featuredarticles/…
James Moore

Solo un comentario rápido: los videos de la clase Stanford CS193P ahora están disponibles a través de iTunes U. La última versión (2012-13) se puede ver en itunes.apple.com/us/course/coding-together-developing/… y espero que los videos y diapositivas futuras serán anunciados en cs193p.stanford.edu
Thomas Watson

Respuestas:


224

Estas son buenas preguntas, y es genial ver que estás haciendo esta investigación y pareces preocupado por aprender cómo "hacerlo bien" en lugar de simplemente hackearlo.

Primero , estoy de acuerdo con las respuestas anteriores que se centran en la importancia de poner datos en los objetos del modelo cuando sea apropiado (según el patrón de diseño MVC). Por lo general, desea evitar poner información de estado dentro de un controlador, a menos que sean estrictamente datos de "presentación".

En segundo lugar , consulte la página 10 de la presentación de Stanford para ver un ejemplo de cómo insertar un controlador mediante programación en el controlador de navegación. Para ver un ejemplo de cómo hacer esto "visualmente" con Interface Builder, eche un vistazo a este tutorial .

En tercer lugar , y quizás lo más importante, tenga en cuenta que las "mejores prácticas" mencionadas en la presentación de Stanford son mucho más fáciles de entender si piensa en ellas en el contexto del patrón de diseño de "inyección de dependencia". En pocas palabras, esto significa que su controlador no debe "buscar" los objetos que necesita para hacer su trabajo (por ejemplo, hacer referencia a una variable global). En su lugar, siempre debe "inyectar" esas dependencias en el controlador (es decir, pasar los objetos que necesita a través de métodos).

Si sigue el patrón de inyección de dependencia, su controlador será modular y reutilizable. Y si piensa de dónde provienen los presentadores de Stanford (es decir, como empleados de Apple, su trabajo es crear clases que puedan reutilizarse fácilmente), la reutilización y la modularidad son las principales prioridades. Todas las mejores prácticas que mencionan para compartir datos son parte de la inyección de dependencia.

Esa es la esencia de mi respuesta. Incluiré un ejemplo de uso del patrón de inyección de dependencia con un controlador a continuación en caso de que sea útil.

Ejemplo de uso de inyección de dependencia con un controlador de vista

Digamos que está creando una pantalla en la que se enumeran varios libros. El usuario puede elegir los libros que quiere comprar, y luego tocar el botón "pagar" para ir a la pantalla de pago.

Para compilar esto, puede crear una clase BookPickerViewController que controle y muestre los objetos GUI / view. ¿Dónde obtendrá todos los datos del libro? Digamos que depende de un objeto BookWarehouse para eso. Entonces, ahora su controlador básicamente está intercambiando datos entre un objeto modelo (BookWarehouse) y los objetos GUI / view. En otras palabras, BookPickerViewController DEPENDE en el objeto BookWarehouse.

No hagas esto:

@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];
   ...
}

En cambio, las dependencias deberían inyectarse así:

@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];
   ...
}

Cuando los chicos de Apple están hablando de usar el patrón de delegación para "comunicarse de nuevo con la jerarquía", todavía están hablando de la inyección de dependencia. En este ejemplo, ¿qué debe hacer BookPickerViewController una vez que el usuario ha elegido sus libros y está listo para retirar? Bueno, ese no es realmente su trabajo. Debe DELEGAR ese trabajo a otro objeto, lo que significa que DEPENDE de otro objeto. Por lo tanto, podemos modificar nuestro método de inicio BookPickerViewController de la siguiente manera:

@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];
   ...
}

El resultado neto de todo esto es que me puede dar su clase BookPickerViewController (y objetos relacionados con la GUI / vista) y puedo usarla fácilmente en mi propia aplicación, suponiendo que BookWarehouse y CheckoutController sean interfaces genéricas (es decir, protocolos) que puedo 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];
}

Finalmente, no solo su BookPickerController es reutilizable sino también más fácil de probar.

-(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
   ...
}

19
Cuando veo preguntas (y respuestas) como esta, elaboradas con tanto cuidado, no puedo evitar sonreír. ¡Felicitaciones bien merecidas a nuestro intrépido interrogador y a ti! Mientras tanto, quería compartir un enlace actualizado para ese práctico enlace invasivecode.com al que hizo referencia en su segundo punto: invasivecode.com/2009/09/… - ¡Gracias nuevamente por compartir su visión y mejores prácticas, además de respaldarlo con ejemplos!
Joe D'Andrea

Estoy de acuerdo. La pregunta estaba bien formada, y la respuesta fue simplemente fantástica. En lugar de solo tener una respuesta técnica, también incluyó algo de psicología detrás de cómo / por qué se implementa usando DI. ¡Gracias! +1 arriba
Kevin Elliott

¿Qué sucede si también desea utilizar BookPickerController para elegir un libro para una lista de deseos, o una de varias posibles razones para elegir libros? ¿Seguiría utilizando el enfoque de la interfaz CheckoutController (quizás renombrado a algo como BookSelectionController) o tal vez usar NSNotificationCenter?
Les

Esto todavía está bastante bien acoplado. Generar y consumir eventos desde un lugar centralizado sería más flexible.
Neil McGuigan

1
El enlace al que se hace referencia en el punto 2 parece haber cambiado nuevamente: aquí está el enlace de trabajo invasivecode.com/blog/archives/322
vikmalhotra

15

Este tipo de cosas siempre es cuestión de gustos.

Dicho esto, siempre prefiero hacer mi coordinación (# 2) a través de objetos modelo. El controlador de vista de nivel superior carga o crea los modelos que necesita, y cada controlador de vista establece propiedades en sus controladores secundarios para decirles con qué objetos de modelo necesitan trabajar. La mayoría de los cambios se comunican una copia de seguridad de la jerarquía mediante NSNotificationCenter; disparar las notificaciones generalmente está integrado en el modelo en sí.

Por ejemplo, supongamos que tengo una aplicación con Cuentas y Transacciones. También tengo un AccountListController, un AccountController (que muestra un resumen de la cuenta con un botón "mostrar todas las transacciones"), un TransactionListController y un TransactionController. AccountListController carga una lista de todas las cuentas y las muestra. Cuando toca un elemento de la lista, establece la propiedad .account de su AccountController y empuja el AccountController a la pila. Cuando toca el botón "mostrar todas las transacciones", AccountController carga la lista de transacciones, la coloca en su propiedad .transactions de TransactionListController y empuja el TransactionListController a la pila, y así sucesivamente.

Si, por ejemplo, TransactionController edita la transacción, realiza el cambio en su objeto de transacción y luego llama a su método 'guardar'. 'guardar' envía una TransactionChangedNotification. Cualquier otro controlador que necesite actualizarse cuando cambien las transacciones observará la notificación y se actualizará. TransactionListController presumiblemente lo haría; AccountController y AccountListController podrían, dependiendo de lo que intentaran hacer.

Para el n. ° 1, en mis primeras aplicaciones tenía algún tipo de displayModel: withNavigationController: método en el controlador secundario que configuraría las cosas y empujaría el controlador a la pila. Pero a medida que me siento más cómodo con el SDK, me he alejado de eso, y ahora generalmente hago que los padres empujen al niño.

Para el n. ° 3, considere este ejemplo. Aquí estamos usando dos controladores, AmountEditor y TextEditor, para editar dos propiedades de una Transacción. Los editores en realidad no deberían guardar la transacción que se está editando, ya que el usuario podría decidir abandonar la transacción. Entonces, en cambio, ambos toman su controlador principal como delegado y llaman a un método que dice si han cambiado algo.

@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

Y ahora algunos métodos de 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];
}

Lo que hay que notar es que hemos definido un protocolo genérico que los Editores pueden usar para comunicarse con su controlador propietario. Al hacerlo, podemos reutilizar los Editores en otra parte de la aplicación. (Quizás las cuentas también pueden tener notas). Por supuesto, el protocolo EditorDelegate podría contener más de un método; en este caso ese es el único necesario.


1
¿Se supone que esto funciona tal cual? Estoy teniendo problemas con el Editor.delegatemiembro. En mi viewDidLoadmétodo, me estoy poniendo Property 'delegate' not found.... No estoy seguro de si arruiné algo más. O si esto se abrevia por brevedad.
Jeff

Este es ahora un código bastante antiguo, escrito en un estilo antiguo con convenciones más antiguas. No lo copiaría y pegaría directamente en su proyecto; Solo trataría de aprender de los patrones.
Brent Royal-Gordon

Gotcha Eso es exactamente lo que quería saber. Lo hice funcionar con algunas modificaciones, pero estaba un poco preocupado de que no coincidiera literalmente.
Jeff

0

Veo tu problema

Lo que ha sucedido es que alguien ha confundido la idea de la arquitectura MVC.

MVC tiene tres partes ... modelos, vistas y controladores ... El problema declarado parece haber combinado dos de ellos sin una buena razón. Las vistas y los controladores son piezas lógicas separadas.

entonces ... no quieres tener múltiples controladores de vista ...

desea tener múltiples vistas y un controlador que elija entre ellas. (también podría tener múltiples controladores, si tiene múltiples aplicaciones)

los puntos de vista NO deberían tomar decisiones. Los controladores deberían hacer eso. De ahí la separación de las tareas, la lógica y las formas de facilitarle la vida.

Entonces ... asegúrese de que su vista solo haga eso, muestre una buena vista de los datos. deje que su controlador decida qué hacer con los datos y qué vista usar.

(y cuando hablamos de datos, estamos hablando del modelo ... una buena forma estándar de ser almacenado, accedido, modificado ... otra lógica separada que podemos parcelar y olvidar)


0

Supongamos que hay dos clases A y B.

instancia de clase A es

Una asistencia;

marcas de clase A e instancia de clase B, como

B bInstance;

Y en su lógica de clase B, en algún lugar debe comunicarse o activar un método de clase A.

1) camino equivocado

Puede pasar la asistencia a bInstance. ahora coloque la llamada del método deseado [aInstance methodname] desde la ubicación deseada en bInstance.

Esto habría servido para su propósito, pero mientras que el lanzamiento habría llevado a un recuerdo bloqueado y no liberado.

¿Cómo?

Cuando pasó el aInstance a bInstance, aumentamos la retención de aInstance en 1. Al desasignar bInstance, se bloqueará la memoria porque aInstance nunca se puede llevar a 0 retacount por razón de bInstance, ya que bInstance en sí es un objeto de aInstance.

Además, debido a que aInstance está atascado, la memoria de bInstance también estará atascada (se filtró). Por lo tanto, incluso después de desasignar aInstance en sí cuando llegue el momento, su memoria también se bloqueará porque bInstance no se puede liberar y bInstance es una variable de clase de aInstance.

2) forma correcta

Al definir aInstance como el delegado de bInstance, no habrá cambios en el recuento ni enredos de memoria de aInstance.

bInstance podrá invocar libremente los métodos de delegado que se encuentran en la entrada. En la desasignación de bInstance, todas las variables se crearán por sí mismas y se liberarán. En la desasignación de aInstance, ya que no hay enredos de aInstance en bInstance, se lanzará limpiamente.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.