domingo, 27 de mayo de 2012

UIActionSheet & UIImagePickerController

Como usuarios de dispositivos iOS os habréis encontrado muchas veces con un Action Sheet. Están formadas por un título opcional y uno o varios botones, cada uno de los cuales se corresponde con una acción.
La clase UIActionSheet se  utiliza para proporcionar al usuario una serie de alternativas acerca de como proceder. También se puede utilizar para informar sobre como actuar ante una situación potencialmente peligrosa.
Vamos a ver un ejemplo muy útil de utilización de UIActionSheet, el que nos da opción de realizar una foto o elegirla de nuestro carrete, por ejemplo para asignarla a un contacto. Es decir, algo así:



Como podéis ver tenemos tres botones:
  1. Hacer Foto: nos abrirá la aplicación de la cámara para realizar una nueva fotografía.
  2. Elegir Foto: permitirá acceder al carrete para escoger una foto previamente realizada.
  3. Cancelar: no realizar ninguna acción y cerrar el action sheet.
En esta ocasión no vamos a poner ningún título.

Lo primero que tenemos que añadir cuando vamos a utilizar esta clase, es el protocolo UIActionSheetDelegate. Deberemos implementar el mensaje actionSheet:clickedButtonAtIndex: , que nos indicará que botón se ha pulsado. El índice va de 0 a n.


- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex

{
    switch (buttonIndex) {
        case 0: //cámara
            [self takePhoto:UIImagePickerControllerSourceTypeCamera];
            break;
        case 1:
            [self takePhoto:UIImagePickerControllerSourceTypeSavedPhotosAlbum];
            break;            
        default:
            break;
    }
}

En el código anterior, en función del índice, estamos enviando un mensaje pasando como parámetro UIImagePickerControllerSourceTypeCameraUIImagePickerControllerSourceTypeSavedPhotosAlbum. Estas dos variables son el tipo de fuente de una clase de la que no hemos hablado hasta ahora y que vamos a necesitar para tomar las fotos: UIImagePickerController. Esta clase se utiliza para gestionar el interface que nos proporciona el sistema para realizar fotografías o vídeos, bien sea desde la cámara del dispositivo o desde la librería de fotos y/o vídeos.
El tipo de fuente puede ser:
  • UIImagePickerControllerSourceTypeCamera: nos proporciona un interface de usuario para realizar fotos o vídeos. Es el que hemos utilizado en la opción 0 del action sheet.
  • UIImagePickerControllerSourceTypePhotoLibrary o UIImagePickerControllerSourceTypeSavedPhotosAlbum: nos proporciona un interface de usuario para elegir entre fotos o vídeos guardados. 
Añadiremos el protocolo UIImagePickerControllerDelegate. Además, para usar un image picker controller, hay que seguir estos pasos:
  • Verificar que es capaz de utilizar el tipo de fuente. Para ello se utiliza el class method isSourceTypeAvailable, que tiene como parámetro una constante de  la enumeración UIImagePickerControllerSourceType:.
  • Comprobar que tipos de medios hay disponibles para el tipo de fuente utilizado. Esto se hace con el class method availableMediaTypesForSourceType:. De esta forma podemos distinguir entre una cámara que se puede usar para vídeos y una que solo se puede usar para fotos.
  • Asignar la propiedad mediaTypes para ajustar el interfaz de usuario en función del tipo de medio.
  • Mostrar el interfaz de usuario llamando al método presentModalViewController:.
  • Cerrar el image picker cuando el usuario ha hecho la acción deseada.
Dicho esto, tenemos que ver como se ha implementado el método takePhoto.

- (void)takePhoto:(UIImagePickerControllerSourceType)sourceType
{
    if ([UIImagePickerController isSourceTypeAvailable:sourceType]) //Verificamos si el dispositivo permite el tipo de fuente
    {
        NSArray *mediaTypes = [UIImagePickerController availableMediaTypesForSourceType:sourceType]; //Comprobamos que tipos de medios hay disponibles para el tipo de fuente utilizado
        if ([mediaTypes containsObject:(NSString *)kUTTypeImage]) //Identificador de tipos abstractos para imágenes
        {
            UIImagePickerController *imagePicherController = [[UIImagePickerController alloc] init];
            imagePicherController.delegate = self;
            imagePicherController.sourceType = sourceType;
            imagePicherController.mediaTypes = [NSArray arrayWithObject:(NSString *)kUTTypeImage];
            imagePicherController.allowsEditing = YES;
            [self presentModalViewController:imagePicherController animated:YES];
        }
    }
    else //Si el tipo de fuente es UIImagePickerControllerSourceTypeCamera y el dispositivo no tiene, se muestra una alerta
    {
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Photo", "Pop Up window headerr")
                                                        message:NSLocalizedString(@"Su dispositivo no permite realizar fotos",@"Pop up window error message")
                                                       delegate:nil
                                              cancelButtonTitle:@"OK"
                                              otherButtonTitles:nil];
        [alert show]; 
    }
}

Por último, es necesario implementar dos métodos del protocolo UIImagePickerControllerDelegate: 
  • imagePickerController:didFinishPickingMediaWithInfo: para recuperar la foto
  • imagePickerControllerDidCancel: para cerrar el picker controller.


- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    UIImage *photo = [info objectForKey:UIImagePickerControllerEditedImage];
    if (!photo) photo = [info objectForKey:UIImagePickerControllerOriginalImage];

/*....Aquí haremos con la foto lo que necesitemos, guardarla en los defaults del usuario, crear un thumbnail, etc....*/

    [self dismisImagePicker];
}

- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
    [self dismisImagePicker];
}

Nada más por hoy.
@Fin

sábado, 12 de mayo de 2012

Core Data: Migraciones


Una aplicación que use Core Data, rechazará cargar datos creados con una versión antigua del modelo de datos. Por lo tanto si haces cambios en el modelo de datos se pueden dar dos situaciones: 
  1. Si la aplicación está en las primeras etapas de desarrollo: En este caso, probablemente no te importe perder los datos con los que hayas estado probando en tiempo de desarrollo. Así que puedes simplemente borrarla del dispositivo o el simulador y volverla a instalar. Cuando la vuelvas a instalar,  tendrá el nuevo modelo de datos. Pero recuerda que esto es solo una opción cuando estás en desarrollo y en la primera versión de tu App.
  2. Si has estado usando una versión del modelo por un tiempo y tienes información que no quieres perder o si ya tienes alguna versión previa en la App Store: deberás hacer una migración del modelo.

Vamos a ver como sería esta segunda opción, es decir, la migración de un modelo de datos en Core Data. 

Lo primero que se necesita es tener los dos modelos, el antiguo y el nuevo.
Para crear una nueva versión de nuestro modelo, seleccionamos Editor-->Add Model Version.


Se nos abrirá una ventana en la que debemos indicar el nombre del nuevo modelo y en que modelo está basado. Es decir, el modelo origen y el destino. 
Al terminar este paso tendremos una estructura de árbol colgando del xcdatamodel con las dos versiones. El esquema antiguo tendrá un check verde.

Si vas a hacer cambios simples en el modelo, como por ejemplo añadir nuevos Atributos a una Entidad, Core Data puede realizar una migración automática que se llama lightweight migration (migración ligera). Básicamente es una migración normal, pero en lugar de tener que proporcionar lo que se llama Mapping model, Core Data genera uno con las diferencias encontradas entre las versiones origen y destino.

Un Mapping model, concepto del que he hablado en el párrafo anterior, es una colección de objetos que especifica las transformaciones que se requieren para migrar un almacén Core Data de una versión a otra. Por ejemplo si añades una nuevo atributo, cambias de nombre una entidad, añades una nueva, etc.. Lo normal es que se realice con Xcode.

Lightweight migration

Para hacer la migración Core Data debe ser capaz de encontrar los modelos origen y destino. Además los cambios deben seguir un patrón obvio, por ejemplo, añadir o borrar un atributo, que un atributo obligatorio pasa a ser opcional, renombrar una entidad o propiedad, cambiar, añadir o renombrar relaciones,  cambiar la relación de uno a uno  a de uno a varios, etc.

Si renombras una entidad o una propiedad, se puede establecer el identificador para hacer el cambio de nombre en el modelo destino con el nombre en el modelo origen. Esto lo haremos en sus propiedades.



En el nuevo modelo que hemos creado, hacemos los cambios que necesitemos. Deberemos cambiar también las clases en función de las modificaciones hechas en el xcdatamodel. Esto quiere decir que si has cambiado la entidad Ejemplo, deberás modificar los archivos Ejemplo.h y Ejemplo.m.

Una vez hechas las modificaciones, hay que hacer un pequeño cambio en el código para aprovechar las funciones de migración. Necesitamos cambiar el código que configura  el persistent store para que habilite la migración automática. Normalmente modificaremos el metodo persistentStoreCoordinator del AppDelegate. Debemos activar los flags de migración automática, lo cual se hace en un NSDictionary.

NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
                             [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];

Y estas opciones se las pasaremos a addPersistentStoreWithType:configuration:URL:options:error:. El persistentStoreCoordinator quedará tal que así:

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (__persistentStoreCoordinator != nil)
    {
        return __persistentStoreCoordinator;
    }
    
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"MiModelo.sqlite"];
    
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
                             [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
    
    NSError *error = nil;
    __persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];

    if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error])
    {

        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }    
    
    return __persistentStoreCoordinator;
}


Por último ponemos el nuevo modelo como current version. Nos situamos en el padre del arbol con los xcdatamodel y vamos al apartado Versioned Core Data Model del File inspector, en donde pondremos como Current el nuevo modelo.


Si ahora ejecutamos la aplicación, ya utilizará el nuevo modelo.

Manual migration

Hay ocasiones en las que no se puede llevar a cabo una lightweight migration por qué Core Data no puede entender por si solo la transformación de un modelo a otro. En estos casos se necesita definir un modelo de transformación, que es el Mapping model del que hemos hablado anteriormente.

Para crearlo añadimos un nuevo fichero en el panel Core Date-->mapping model


En la ventana siguiente seleccionamos el modelo de datos fuente. A continuación el destino. Finalmente damos un nombre al fichero y salvamos. En ese momento Xcode crea el mapping model con todas las diferencias que ha conseguido deducir entre el modelo origen y el destino.

En el proceso de migración Core Data crea dos pilas, una para los datos fuente y otra para los datos destino. Después busca los objetos de la fila fuente e inserta su correspondiente apropiado en el destino.

En este tipo de migración se utiliza una instancia de NSMigrationManager. El migration manager necesita:
  • El modelo de datos destino: se trata del persistent store coordinator.
  • El modelo de datos usado para abrir el store origen.
  • El mapping model.
Es decir, la única diferencia con la lightweight migration es la utilización del mapping model.

Si el nuevo modelo simplemente añade propiedades o entidades, no será necesario añadir más código además del que ya vimos en el caso anterior. Pero si el cambio es más complejo, será necesario crear una subclase de NSEntityMigrationPolicy. Es lo que se llama Three-Stage Migration.



@Fin