View communication in iOS
Unos de los problemas más comunes que nos encontramos cuando estamos desarrollando una aplicación para iOS es cómo pasar información de una vista a otra. Pongamos por ejemplo las pantallas de Settings.app que se muestra en la imagen.
Este es un ejemplo de controladores que deben comunicarse entre ellos. La primera vista tiene una celda donde aparece el nombre del dispositivo. Al pulsar la celda, se muestra la siguiente vista donde se puede editar el nombre del dispositivo. Una vez se termine la edición, el texto nuevo debe volver a la primera vista.
Existen varios métodos para resolver este problema:
- Singleton
- Delegate + protocol
- Bloques
En el curso hay ejemplos de código que utilizan los dos primeros métodos, vamos a repasarlos y vamos a ver cómo funciona el método de comunicación de controladores mediante bloques.
Singleton
El patrón Singleton consiste en restringir la creación de una única instancia para una clase determinada.En nuestro podemos compartir esta instancia única entre las dos vistas de forma que podamos comunicar las vistas hacia delante y hacia detrás.
Tal como se comenta en este post existe varias formas de crear un Singleton. Utilizando el autocompletado de AppCode (se ha convertido en mi IDE habitual, lo recomiendo totalmente) se genera el siguiente código:
@interface NameInfo : NSObject
@property(strong, nonatomic) NSString *name;
+ (NameInfo *)instance;
@end
La clase NameInfo es un Singleton que permitirá la comunicación entre las dos vistas.
@implementation NameInfo
+ (NameInfo *)instance {
static NameInfo *_instance = nil;
@synchronized (self) {
if (_instance == nil) {
_instance = [[self alloc] init];
}
}
return _instance;
}
@end
La implementación se asegura de que se llama una única vez al método init.
El código para utilizar el singleton es
NameInfo *nameInfo = [NameInfo instance];
nameInfo.name = @"Axel";
NSLog(nameInfo.name);
El código es bastante sencillo. El principal problema que tiene este método es que nos obliga a crear una clase nueva cada vez que queramos realizar comunicación entre vistas. Además, la segunda vista debe escribir directamente en la propiedad name, por lo tanto podemos tener problemas para reutilizar la vista.
Delegate + protocol
Este patrón es el recomendado en la documentación de Apple. Un ejemplo de uso suponiendo la utilización de segues es:
@implementation AViewController
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if([segue.identifier isEqualToString:@"toB"]) {
BViewController *controller = segue.destinationViewController;
controller.name = self.name;
controller.delegate = self;
}
}
En el método prepareForSegue es donde se comunica la vista A con la vista B. En este caso se está pasando el valor de la propiedad name. Además se esta asignando la propiedad delegate, de forma que la vista B tiene una referencia a la vista A. Esto permitirá hacer la comunicación en el sentido contrario.
@interface BViewController : UIViewController
@property(strong, nonatomic) NSString *name;
@property(weak, nonatomic) id delegate;
@end
El código para el controlador B tiene la definición de la propiedad delegate. Esta propiedad es importante que sea weak, para que no se produzca una referencia cíclica y que la memoria pueda liberarse correctamente.
La comunicación desde la vista B hasta la vista A se puede hacer en el momento en que la vista B va a desaparecer.
- (void)viewWillDisappear:(BOOL)animated {
SEL setNameSelector = @selector(setName:);
if([self.delegate respondsToSelector:setNameSelector]) {
[self.delegate performSelector:setNameSelector withObject:self.name];
}
}
El código llama al método setName del objeto delegate en el caso de que exista. Se utiliza el método performSelector porque no hemos definido el tipo de dato del delegate.
En el caso de que quisiéramos que el código fuese type checked podemos utilizar un protocolo.
@protocol BViewControllerDelegate <NSObject>
- (void)setName:(NSString *)name;
@end
El protocolo define un único método setName
@interface AViewController : UIViewController <BViewControllerDelegate>
@property(strong, nonatomic) NSString *name;
@end
Ahora la clase AViewController debe implementar el protocolo BViewControllerDelegate.
@interface BViewController : UIViewController
@property(strong, nonatomic) NSString *name;
@property(weak, nonatomic) id<BViewControllerDelegate> delegate;
@end
Se modifica la definición del delegate para especificar que es el tipo de dato una clase que implementa el protocolo BViewControllerDelegate.
El código del controlador B se reduce a invocar el método setName directamente.
- (void)viewWillDisappear:(BOOL)animated {
[self.delegate setName:self.name];
}
Este método está muy extendido en las librerías que hace Apple, por ejemplo el controlador MFMailComposeViewController que permite el envío de correos utiliza el protocolo MFMailComposeViewControllerDelegate para comunicarse con la vista anterior e informa si el correo se envió correctamente o se produjo un error.
Bloques
La última opción para comunicar dos controladores es utilizar bloques. Un bloque es lo que en otros lenguajes de programación se conoce en como una closure. En pocas palabras se trata de una función definida de forma inline y que tiene acceso a las variables locales que están en el mismo scope en el que se ha definido la closure. Los bloques son una característica nueva del lenguaje que fue introducida en iOS 4.
Aquí tienes dos artículos que explican cómo trabajar con bloques.
Si prefieres puedes consultar la documentación oficial de introducción a los bloques
Los bloques permiten resolver el problema de comunicar dos vistas de una manera muy elegante.
typedef void(^BViewControllerCallback)(NSString *name);
@interface BViewController : UIViewController
@property(strong, nonatomic) NSString *name;
@property(copy, nonatomic) BViewControllerCallback callback;
@end
En el código se define el tipo BViewControllerCallback como un bloque que recibe un parámetro name.
- (void)viewWillDisappear:(BOOL)animated {
if(self.callback != nil) {
self.callback(self.name);
}
}
Cuando la vista va a desaparecer, si hay definido un callback se invoca pasando como parámetro el valor actual.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if([segue.identifier isEqualToString:@"toB"]) {
BViewController *controller = segue.destinationViewController;
controller.name = self.name;
__weak AViewController *weakSelf = self;
controller.callback = ^(NSString *name) {
weakSelf.name = name;
};
}
}
El método prepareForSegue debe definir el código que se debe ejecutar cuando la vista B haya terminado.
La clase TWTweetComposeViewController que permite compartir contenido en Twitter utiliza esta técnica. Define una propiedad completionHandler que es el bloque se invoca cuando el controlador ha terminado de enviar el tweet.
La principal ventaja de utilizar bloques es que permite reutilizar la vista B más fácilmente. Lo único que hay que hacer es definir un callback distinto cada una de las veces que se invoque la vista.
Conclusiones
En el post hemos visto varias formas de resolver el problema de la comunicación entre dos controladores. Cada una de los métodos tiene sus ventajas e inconvenientes y puede ser más adecuada para ciertas situaciones. Últimamente me estoy decantando por definir la comunicación utilizando bloques porque me permite reutilizar el código más fácilmente. También puede ser que de trabajar mucho con javascript, diseñar apis utilizando callbacks se esta convirtiendo en una práctica muy habitual en mi código.