¿Cuál es la mejor forma de comunicarse entre los controladores de visualización?

Siendo nuevo en Object-c, Cocoa y iPhone Dev 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 la clase CS193P de Stanford que han quedado en la web. Incluye notas de clase, asignaciones y código de muestra, y dado que el curso fue impartido por desarrolladores de Apple, definitivamente lo considero como “de la boca del caballo”.

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

La conferencia 08 está relacionada con una tarea para construir una aplicación basada en UINavigationController que tiene múltiples UIViewControllers insertados en la stack UINavigationController. Así es como funciona el UINavigationController. Eso es lógico. 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 a su delegado de la aplicación
  • Las dependencias directas hacen que su código sea menos reutilizable
    • Y más difícil de depurar y probar

De acuerdo. Estoy de acuerdo con eso. No arroje a ciegas todos los métodos que se utilizarán para comunicarse entre el viewcontroller y el delegado de su aplicación y haga referencia a las instancias de viewcontroller en los métodos delegates de la aplicación. Fair ’nuff.

Un poco más adelante, obtenemos esta diapositiva diciéndonos qué debemos hacer.

Página 18/51:

Mejores prácticas para el flujo de datos

  • Averiguar exactamente qué necesita ser comunicado
  • Definir los parámetros de entrada para su controlador de vista
  • Para comunicar una copia de seguridad de la jerarquía, use un acoplamiento flojo
    • Definir una interfaz genérica para observadores (como delegación)

Esta diapositiva es seguida por lo que parece ser una diapositiva de marcador de posición donde el profesor demuestra aparentemente las mejores prácticas usando un ejemplo con 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 última línea en la cita anterior. He estado haciendo mi parte justa de google 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 delegates como un método! Excepto … los objetos solo pueden establecer un delegado a la vez. Entonces, cuando tengo comunicación con múltiples viewcontrollers, ¿qué debo hacer?

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

Ayúdenme a “hacer lo correcto” respondiendo las siguientes preguntas:

  1. Cuando bash presionar un nuevo controlador de vista en la stack UINavigationController, ¿ quién debería estar haciendo esto? ¿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 hacerlo?
  3. Supongamos que solo podemos tener un delegado establecido a la vez en un objeto, cómo sería la implementación cuando el profesor diga “Defina una interfaz genérica para observadores (como delegación)” . Un ejemplo de pseudocódigo sería tremendamente útil si es posible.

Estas son buenas preguntas, y es genial ver que estás haciendo esta investigación y parece preocupado por aprender a “hacerlo bien” en lugar de simplemente hackearlo.

En primer lugar , 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 de MVC). Por lo general, desea evitar colocar información de estado dentro de un controlador, a menos que sea estrictamente información 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 progtwigción en el controlador de navegación. Para ver un ejemplo de cómo hacer esto “visualmente” usando 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 las considera en el contexto del patrón de diseño de “dependency injection”. 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 cambio, siempre debe “inyectar” esas dependencias en el controlador (es decir, pasar los objetos que necesita a través de los métodos).

Si sigue el patrón de dependency injection, su controlador será modular y reutilizable. Y si piensas de dónde vienen los presentadores de Stanford (es decir, como empleados de Apple su trabajo consiste en crear clases que puedan reutilizarse fácilmente), la reutilización y la modularidad son una prioridad. Todas las mejores prácticas que mencionan para compartir datos son parte de la dependency injection.

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

Ejemplo de uso de dependency injection con un controlador de vista

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

Para construir esto, puede crear una clase BookPickerViewController que controle y muestre la GUI / objetos de vista. ¿De dónde sacará todos los datos del libro? Digamos que depende de un objeto BookWarehouse para eso. Entonces, ahora su controlador básicamente está intermediando datos entre un objeto modelo (BookWarehouse) y la GUI / objetos de visualización. 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 deben ser inyectadas 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 “comunicar una copia de seguridad de la jerarquía”, todavía están hablando de la dependency injection. En este ejemplo, ¿qué debería hacer el BookPickerViewController una vez que el usuario ha elegido sus libros y está listo para realizar el check-out? Bueno, ese no es realmente su trabajo. Debe DELEGAR ese trabajo a algún otro objeto, lo que significa que DEPENDE de otro objeto. Entonces 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 puede darme su clase BookPickerViewController (y objetos relacionados de GUI / view) y puedo usarlo fácilmente en mi propia aplicación, suponiendo que BookWarehouse y CheckoutController son interfaces genéricas (es decir, protocolos) que puedo 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, 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 ... } 

Este tipo de cosas siempre es una cuestión de gusto.

Habiendo dicho eso, siempre prefiero hacer mi coordinación (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 indicarles con qué objetos de modelo deben trabajar. La mayoría de los cambios se comunican en la jerarquía utilizando NSNotificationCenter; disparar las notificaciones generalmente está incorporado al 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 cuenta con un botón “show all transactions”), 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 stack. Cuando toca el botón “mostrar todas las transacciones”, AccountController carga la lista de transacciones, la coloca en la propiedad TransactionsListController’stransactions y empuja el TransactionListController a la stack, 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 la transacción cambie, observará la notificación y se actualizará. TransactionListController presumiblemente lo haría; AccountController y AccountListController podrían, dependiendo de lo que intentaban hacer.

Para el n. ° 1, en mis primeras aplicaciones tenía algún tipo de método displayModel: withNavigationController: en el controlador secundario que configuraba las cosas y empujaba el controlador hacia la stack. Pero a medida que me siento más cómodo con el SDK, me alejé de eso y ahora, por lo general, el padre empuja al niño.

Para # 3, considere este ejemplo. Aquí estamos usando dos controladores, AmountEditor y TextEditor, para editar dos propiedades de una transacción. Los editores no deberían guardar la transacción que se está editando, ya que el usuario podría decidir abandonar la transacción. Entonces, en su lugar, ambos toman su controlador principal como delegado y llaman a un método que indica 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  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 

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.

Veo tu problema …

Lo que sucedió es que alguien ha confundido la idea de la architecture de MVC.

MVC tiene tres partes … modelos, vistas y controladores. El problema establecido parece haber combinado dos de ellos sin una buena razón. las vistas y los controladores son partes separadas de la lógica.

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)

las vistas NO deberían tomar decisiones. El (los) controlador (es) deberían hacer eso. De ahí la separación de tareas, lógica y formas de hacer su vida más fácil.

Entonces … asegúrate de que tu vista solo hace eso, saca 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 almacenar, acceder, modificar … otra pieza de lógica separada que podemos guardar y olvidar)

Supongamos que hay dos clases A y B.

instancia de la clase A es

A aInstance;

clase A hace e instancia de clase B, como

B bstancia;

Y en su lógica de clase B, en algún lugar se le requiere que comunique o active un método de clase A.

1) Manera incorrecta

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

Esto hubiera sido útil para su propósito, pero si se hubiese liberado, la memoria se habría bloqueado y no se habría liberado.

¿Cómo?

Cuando pasaste la aInstancia a bInstancia, aumentamos el recuento retenido de una instancia en 1. Al desasignar bInstancia, tendremos memoria bloqueada porque unaInstancia nunca puede ser llevada a 0 retención por bInstancia porque la bInstancia misma es un objeto de unaInstancia.

Además, debido a que unaInstancia está atascada, la memoria de bInstance también se atascará (se filtró). Así que incluso después de desasignar una instancia en sí cuando llega el momento más tarde, su memoria también será bloqueada porque bInstance no puede liberarse y bInstance es una variable de clase de una instancia.

2) Manera correcta

Al definir una instancia como el delegado de bInstance, no habrá un cambio en el recuento de retenciones ni un enredo de memoria de una instancia.

bInstance podrá invocar libremente los métodos delegates que se encuentran en la aInstance. En la desasignación de bInstance, todas las variables serán creadas por su cuenta y se liberarán. En la desasignación de una instancia, ya que no hay enredo de unaInstancia en bInstance, se lanzará limpiamente.