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
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:
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
Para especificar que una clase sigue un protocolo, ponemos su nombre entre <...>
tras el nombre de la superclase
Si miras el
.h
delAppDelegate
de alguna aplicación verás que se especifica que implementa el protocoloUIApplicationDelegate
Podemos definir una variable de la que no sabemos todavía el tipo concreto (id
) pero sí que implementa un determinado protocol:
Si no implementamos algún método el compilador generará warnings, no errores.
Podemos especificar que ciertos métodos son opcionales y otros obligatorios
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
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:
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:
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 change
del 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:
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
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.
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.
API de notificaciones
Para obtener acceso al singleton que es el centro de notificaciones de nuestra app hacemos:
Para enviar una notificación:
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:
El
selector
es el mensaje que se va a enviar al objeto destinatario (elobserver
)El
name
es el identificador de la notificaciónEl
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 unNSDictionary
con el payload. Siguiendo con el ejemplo anterior:
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:
El parámetro
queue
permite especificar una cola de operaciones para procesar la notificación en background (en otro thread). Si lo ponemos anil
será una notificación síncrona. Es decir, se recibirá antes de que termine elpostNotification
.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ámetrosname
yobject
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ónSi 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 aaddObserverForName:...
, 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
.
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 enViewController.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 propiedadunEuroEnUSD
.
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 estiloprintf
de C:
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