Comunicación entre objetos

Casi todos los conceptos que vamos a exponer aquí son más de Cocoa que de Objective C propiamente dicho, por tanto muchos son aplicables también a Swift.

Target-action

Ya hemos visto que es el mecanismo típico para comunicar eventos de la vista al controlador

Recapitulando: cuando se produzca un evento (por ejemplo un toque) sobre un objeto (vista) queremos avisar a otro objeto (controlador) llamando a un método determinado

Hasta ahora lo hemos hecho con el Interface Builder pero también se puede hacer por código

[unBoton addTarget:self action:@selector(botonPulsado)
         forControlEvents:UIControlEventTouchUpInside];
...
(void)botonPulsado {
    NSLog(@"Pulsado!");
}
  • El método del “action” puede tener varias signaturas. La más sencilla es la del ejemplo anterior, sin parámetros. También es admisible pasar la fuente del evento, o la fuente y el propio evento, como en el siguiente ejemplo:

[unBoton addTarget:self
         action:@selector(botonPulsado:paraEvento:)
         forControlEvents:UIControlEventTouchUpInside];
...
(void)botonPulsado:(id) sender paraEvento:(UIEvent *)evento{
    NSLog(@"Objeto: %@", [sender debugDescription]);
    NSLog(@"Evento: %@", [evento debugDescription]);
}

Lo que no podemos hacer es usar una signatura arbitraria, y por tanto no podemos usar este mecanismo cuando queramos pasar información “personalizada”.

¿Qué pasa cuando el target es nil?. El evento se sigue comunicando, pero a quién se comunica lo veremos en la asignatura de interfaz gráfico.

Delegates y protocols

Supongamos que queremos comunicar a otro objeto que suceden ciertos eventos, y además queremos pasar información arbitraria en cada evento.

Posible solución: especificar formalmente qué signatura tienen los métodos que informarán que se ha producido cada evento. Debemos estar seguros de que el objeto receptor implementa dichos métodos si no queremos un bonito error de ejecución (al ser Obj-C un lenguaje dinámico, daría solo un warning al compilar).

¿Cómo asegurarse de que una clase implementa ciertos métodos? Es la misma idea de los interfaces en Java, por ejemplo. En Obj-C se consigue lo mismo con los protocols.

Protocols

Definimos un protocolo en un fichero .h

@protocol UACalificable
- (void)setNota: (CGFloat) nota;
- (CGFloat)nota;
@end

Para especificar que una clase sigue un protocolo, ponemos su nombre entre <...> tras el nombre de la superclase

@interface UAAsignatura : NSObject<UACalificable>
...
@end

Si miras el .hdel AppDelegatede alguna aplicación verás que se especifica que implementa el protocolo UIApplicationDelegate

Podemos definir una variable de la que no sabemos todavía el tipo concreto (id) pero sí que implementa un determinado protocol:

id<UACalificable> algoCalificable;

Si no implementamos algún método el compilador generará warnings, no errores.

Podemos especificar que ciertos métodos son opcionales y otros obligatorios

@protocol MiProtocol
- (void)metodoObligatorio;
@optional
- (void)metodoOpcional;
- (void)otroMetodoOpcional;
@required
- (void)otroMetodoObligatorio;
@end

Cuestión de estilo: se recomienda que los nombres de los protocolos sean adjetivos (como en el ejemplo anterior) o gerundios (por ejemplo, Cocoa tiene un protocolo NSCopying, que indica que se puede hacer una copia del objeto llamando a copyWithZone:).

Delegate

Una vez tenemos definido un protocol, y un objeto conforme con él, ya sabemos que hay una serie de métodos a los que podemos llamar para comunicarnos con el objeto.

Al objeto receptor lo llamaremos delegate, ya que en él “delegamos” la responsabilidad de procesar los eventos.

Si el nombre delegate te suena es porque aparece en iOS en múltiples ocasiones (por ejemplo el AppDelegate). Es un patrón de diseño ampliamente usado en la plataforma.

Key-Value Observing

Un punto engorroso de los delegates es que hay que llamar explícitamente a los métodos del protocolo para avisar al objeto receptor. ¿Podríamos hacer que el aviso fuera automático cuando pasara “algo interesante”?

El mecanismo de KVO, o key-value observing nos permite avisar automáticamente al receptor cuando cambie una propiedad del emisor. En realidad es el receptor el que se “suscribe” a los cambios.

Para que se pueda usar KVO, es necesario que las propiedades a observar sean “KVC-compliant”

Para convertir al objeto “receptor” en el observador de los cambios de la propiedad “nombre” de un “emisor”, en su forma más sencilla haríamos algo como

[emisor addObserver:receptor
  forKeyPath:@"nombre"
  options:0
  context: nil];

Por ahora no necesitamos los dos últimos parámetros así que los ponemos a 0 y nil respectivamente. Luego veremos para qué se usan.

Cuando se produzca el cambio en la propiedad, el receptor recibirá un mensaje observeValueForKeyPath:ofObject:change:context: (o dicho más al estilo Java/C++, se llamará a este método). Tendremos que implementar este método en nuestro receptor:

- (void)observeValueForKeyPath:(NSString *)keyPath
             ofObject:(id)object
             change:(NSDictionary *)change
             context:(void *)context {
    NSLog(@"Cambia la propiedad %@ a %@",
          keyPath, [object valueForKey:keyPath]);
}

Podemos indicarle a KVO que nos pase el nuevo valor, o que nos pase también el antiguo, o algo más sofisticado, como que nos avise antes y después del cambio,… Para esto se usa el parámetro options, que es una máscara de bits de opciones. Por ejemplo:

[emisor addObserver:receptor
  forKeyPath:@"nombre"
  options: (NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
  context: nil];

Con lo anterior indicamos que queremos tanto el nuevo valor como el antiguo. Para acceder a él podemos usar el parámetro de tipo diccionario changedel observeValueForKeyPath:ofObject:change:context:. En el API hay varias claves que representan los distintos valores. Siguiendo con el ejemplo anterior, para obtener el valor antiguo y el nuevo haríamos algo como:

NSLog(@"Cambia la propiedad %@ de %@ a %@", keyPath, change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]);

El parámetro context puede usarse si necesitamos que una misma clase (o sus subclases) observe una propiedad por varios motivos distintos. Llamaríamos al addObserver varias veces con distintos valores de context, que se pasa tal cual cuando se llama al observeValueForKeyPath. Comprobando allí el valor de este parámetro podemos saber qué addObserver ha “generado” este aviso.

Hay que “desregistrar” al observador antes de que desaparezca el observado, de lo contrario podríamos tener una fuga de memoria. Si no lo hacemos antes, podemos aprovechar el dealloc del receptor para hacerlo

- (void) dealloc {
    NSLog(@"Des-registrando observador...");
    [emisor removeObserver:receptor forKeyPath:@"nombre"];
}

Nótese que con KVO podemos más o menos hacer “broadcasting”, ya que podemos tener varios observadores para la misma propiedad de un objeto.

Las notificaciones de KVO son síncronas y se producen en el mismo hilo que el cambio de la propiedad. Los observadores de la propiedad serán notificados mediante la llamada a su observeValueForKeyPath inmediatamente después de que se ejecute el setter y antes de la siguiente sentencia.

objeto.propiedadObservada = @"Nuevo valor";
//antes de que se ejecute la siguiente sentencia
//ya se habrá notificado a los observadores
NSLog(@"Seguro que ya se han notificado los cambios");

Notificaciones

Cuando usamos KVO, hay un cierto acoplamiento entre emisor y receptor, materializado en la llamada a addObserver. Si nos interesesa desacoplar totalmente emisor y receptor podemos usar notificaciones.

Las notificaciones implementan un sistema de tipo “publicar-suscribir”: Cada aplicación iOS tiene un “centro de notificaciones”. A ese centro se pueden enviar las notificaciones (mensajes con un identificador y un payload). Cualquier objeto puede suscribirse a las notificaciones especificando el identificador que le interesa.

API de notificaciones

Para obtener acceso al singleton que es el centro de notificaciones de nuestra app hacemos:

[NSNotificationCenter defaultCenter]

Para enviar una notificación:

NSDictionary *payload = @{@"empresa": @"AAPL", @"valor": @95.55};
[[NSNotificationCenter defaultCenter] postNotificationName:@"cotizacion" object:self userInfo:payload];

Para suscribirnos a las notificaciones que nos interesan podemos usar addObserver:selector:name:object: (luego veremos otra alternativa). Por ejemplo, en una app de bolsa estaríamos interesados en recibir notificaciones sobre los cambios en las cotizaciones:

[[NSNotificationCenter defaultCenter] addObserver:self
       selector: @selector(nuevaCotizacion:)
       name:@"cotizacion"
       object:nil]
  • El selector es el mensaje que se va a enviar al objeto destinatario (el observer)

  • El name es el identificador de la notificación

  • El object es el objeto del que nos interesa recibir notificaciones. Lo habitual será que nos dé igual cuál sea el emisor, en cuyo caso ponemos aquí nil

  • Para recibir la notificación tendremos que implementar el selector especificado al registrarnos. Debe tener como único parámetro un objeto de la clase NSNotification, que “empaqueta” tanto el nombre de la notificación como un NSDictionary con el payload. Siguiendo con el ejemplo anterior:

- (void) nuevaCotizacion: (NSNotification *) notificacion {
    NSLog(@"Recibida notificación: %@, payload: %@",
           notificacion.name, notificacion.userInfo);
}

El método que hemos usado para suscribirnos a notificaciones nos obliga a implementar un selector especial en el receptor únicamente para recibir la notificación. Con addObserverForName:object:queue:usingBlock: podemos pasar directamente el bloque de código a ejecutar al recibir la notificación. Esto hace el código mucho más “autocontenido” y compacto:

[[NSNotificationCenter defaultCenter] addObserverForName:@"cotizacion" object:nil queue:nil usingBlock:^(NSNotification *notificacion) {
        NSLog(@"Notificación %@, payload: %@", notificacion.name, notificacion.userInfo);
    }];
  • El parámetro queue permite especificar una cola de operaciones para procesar la notificación en background (en otro thread). Si lo ponemos a nil será una notificación síncrona. Es decir, se recibirá antes de que termine el postNotification.

  • Nótese que el bloque recibe como parámetro un NSNotification, igual que sucedía con el selector “receptor” en el método anterior de suscripción. Los parámetros name y object también tienen el mismo significado que antes.

  • El método devuelve un objeto que nos servirá para eliminar la suscripción, como luego veremos.

Finalmente, es importante acordarse de eliminar una suscripción existente : si un objeto deja de existir y el centro de notificaciones lo tiene como suscriptor de una notificación intentará enviársela cuando llegue, lo que seguramente hará que la aplicación aborte.

Hay varios métodos de la clase NSNotificationCenter que podemos usar para eliminar una suscripción, los que comienzan por removeObserver:

  • Si queremos eliminar todas las suscripciones usaremos removeObserver:.

  • Si queremos eliminar solo algunas suscripciones de un objeto usaremos removeObserver:name:object

  • Para eliminar la suscripción hay que pasar el objeto que está recibiendo las notificaciones como primer parámetro de removeObserver:.

  • Si nos suscribimos con el primer método (addObserver:selector:...) está claro cuál es ese objeto, lo especificamos al hacer la suscripción

  • Si usamos el segundo método (addObserverForName:...) entonces el objeto que está recibiendo las notificaciones no es uno implementado por nosotros, sino por iOS, y que es el que acaba llamando al bloque que procesa las notificaciones. Este objeto lo obtenemos como resultado de la llamada a addObserverForName:..., por lo que deberíamos guardar esa referencia.

Notificaciones del sistema

Podemos usar notificaciones para recibir también eventos del sistema. Muchos objetos de Cocoa pueden enviar notificaciones. Por ejemplo cuando pulsamos el botón de inicio del iPhone/iPad para salir de la aplicación actual se llama al método applicationDidEnterBackground del AppDelegate, pero también podemos suscribir cualquier objeto a la notificación del sistema UIApplicationDidEnterBackgroundNotification.

[[NSNotificationCenter defaultCenter]
   addObserverForName:UIApplicationDidEnterBackgroundNotification
   object:nil queue:nil
   usingBlock:^(NSNotification *notif){
            NSLog(@"Me han dicho que entramos en background");
   }];

Hay decenas de notificaciones del sistema distintas, aunque por eficiencia no todas se envían por defecto, algunas hay que activarlas, por ejemplo las del reproductor de música (indicando cambio de canción, modificación del volumen,… ).

Ejercicios de comunicación entre objetos

  • En las plantillas de la sesión hay un proyecto de Xcode llamado “Comunicación”. Se trata de un conversor entre euros y dólares que actualiza el tipo de cambio en tiempo real (lo debería actualizar de la red, pero lo que hace es calcular un valor aleatorio cada 5 segundos)

  • Si escribimos por ejemplo una cantidad en euros y pulsamos el botón “obtener” que hay al lado del campo para los dólares, veremos la cantidad en dólares (y viceversa).

  • El problema es que aunque el cálculo se hace con la cotización actual, la etiqueta con la cotización (“1€=…$”) no se actualiza automáticamente cada vez que cambie esta. Tenemos que solucionarlo

    • El outlet que nos da acceso a la etiqueta se llama tipoCambioLabel, definido en ViewController.h

    • El conversor está definido como una variable de instancia del mismo nombre en ViewController.m

    • Cada 5 segundos se dispara un “timer” que llama al método actualizarTipoDeCambio del conversor. Este método a su vez cambia el valor la propiedad unEuroEnUSD.

  • Implementar una primera versión usando KVO, de modo que el View Controller se registre como observador de la propiedad unEuroEnUSD. Cada vez que cambie se debería actualizar la etiqueta para reflejar el tipo de cambio actual.

  • Para generar el texto de la etiqueta a partir del valor de cambio podéis usar el stringWithFormat, que funciona al estilo printf de C:

self.tipoCambioLabel.text = [NSString stringWithFormat:@"1 € = %1.3f $",
                                         conversor.unEuroEnUSD];
  • Implementar otra versión con notificaciones:

    • Primero comentad la línea del ejercicio anterior que actualiza la etiqueta en pantalla, ya que ahora la vamos a actualizarla con notificaciones

    • Desde el método actualizarTipoDeCambio se debe enviar una notificación (dadle el nombre que queráis) y el View Controller debería suscribirse a ellas para poder actualizar la etiqueta en pantalla.

Last updated