domingo, 15 de marzo de 2015

Asset Catalogs

La existencia de dispositivos iOS con diferente resolución, hace que sea necesario gestionar la resolución de las imágenes utilizadas en las Apps según el dispositivo en el que se lance para evitar que salgan pixeladas. Anteriormente se utilizaban diferentes nombres, como file@2x.png, file-568h@2x.png, etc. para una misma imagen.

Asset Catalogs nos permite simplificar la gestión de las imágenes que utiliza nuestra aplicación. Ya no es necesario utilizar diferentes nombres o comprobar mediante código en que dispositivo se está ejecutando la aplicación.

Para añadir nuevos elementos al Asset Catalog, se selecciona el xcassets y, pulsando con el botón derecho del ratón en la ventana donde aparece la lista de conjuntos de imágenes, aparecerá un menú emergente mediante el que podemos añadir un nuevo conjunto de imágenes, el icono de la aplicación, la imagen que aparece al lanzar la aplicación o un icono OS X.

Fig. 1



En la siguiente imagen se ha creado un conjunto de imágenes  llamado Clock. Al haber seleccionado en el Attributes Inspector que el tipo de dispositivo es Universal, nos permite añadir imágenes con resolución 1x, 2x y 3x.

Fig. 2

Para hacer referencia a este conjunto de imágenes, basta con utilizar su nombre, en este caso Clock, sin necesidad de extensión. El nombre de los archivos que contienen las imágenes puede ser cualquiera, no hace falta que tenga ningún tipo de sufijo del tipo @2x o @3x.

En el caso de que sea necesaria una determinada imagen según el tipo de dispositivo, es necesario seleccionar la opción Device Specific y marcar los checks de los dispositivos para los que se crea el conjunto de imágenes, tal y como se muestra en la siguiente figura.

Fig. 3

@Fin

sábado, 14 de marzo de 2015

Conceptos de Core Data

Vamos a ver algunos de los conceptos más importantes relacionados con Core Data.

 Lo primero, ¿que es Core Data? Core Data es un framework que permite trabajar con tus datos como si fueran objetos, independientemente de cómo están almacenados en disco. Se situa entre tu aplicación y un persistent store, que es el término genérico para un fichero de datos, el cual puede ser una base de datos SQLite, un fichero XML o un almacen binario. A estos ficheros se les llama persistent por que permanecen a pesar de que cierres la aplicación, reinicies el dispositivo, etc.

Core Data utiliza un Managed Object Model para configurar los datos de tu aplicación utilizando un object graph. Cada objeto del object graph se llaman entity. Una vez que tienes definidos managed objects, se puede manipular nativamente en Objective-C sin necesidad de utilizar SQL (o cualquier otro tipo de tratamiento de un persistent store).

En la siguiente imagen vemos un object graph muy simple con dos entidades Client y Purchase. Como veis, es similar a un esquema de base de datos, en donde las entities serían las tablas.

Fig. 1


Un managed object contiene una copia de datos de un persistent store. Si tu persistent store es una base de datos, un managed objet representaría una fila de una tabla de la base de datos. Un managed object es una instancia de NSManagedObject, aunque usualmente es una instancia de una subclase de NSManagedObject.

Todos los managed objects existen en un managed object context, que existe en memoria RAM. De esta manera se accede a los datos que han sido previamente leidos de disco de una manera muy rápida sin necesidad de estar continuamente accediendo a disco. El managed object necesitará llamar a save: para escribir los cambios en disco.

En la imagen Fig.2 se ilustra como encajan las piezas principales de Core Data:

Fig. 2

En Fig.2, el Persistent Store Coordinator se muestra conteniendo un persistent store con filas de una tabla. Cuando se configura un persistent store coordinator, normalmente se elige una base de datos SQLite como persistent store. Un persistent store coordinator puede tener varios persistent store. Por ejemplo, cuando Core Data se integra con iCloud, poniendo datos que no pertenecen a iCloud en un persistent store y los que si pertenecen en otro, se optimiza el ancho de banda y el espacio de almacenamiento usado en iCloud. Se utilizan instancias de NSPersistentStore y NSPersistentStoreCoordinator para crear persistent stores y persistent store coordinators respectivamente.

En el medio de Fig.2 se representa en el medio el Managed Object Model. Es la representación gráfica de la estructura de datos. Las entities no contienen datos, solo definen las propiedades que tienen los managed objects basados en ellas. Un managed object model se crea mediante una instancia de NSManagedObjectModel.

Finalmente, en la derecha de la Fig. 2 se muestra el managed object context que se encarga de gestionar el ciclo de vida de los managed objects incluidos en él y proporciona características para hacer fetch de los datos, validaciones y seguimiento de los cambios. También se pueden definir varios managed object context, lo cual se utiliza sobre todo para realizar procesamiento en background, como salvar o importar datos. Para crearlo se instancia NSManagedObjectContext.

@Fin

sábado, 16 de noviembre de 2013

Sonidos del sistema en iOS

Para añadir sonidos del sistema a nuestra App, lo primero que hay que hacer es añadir el framework AudioToolbox.


En aquellos View Controllers en los que se vaya a utilizar, se deberá importar:

#import <AudioToolbox/AudioToolbox.h>

Para generar el sonido utilizaremos el método AudioServicesPlaySystemSound(SystemSoundID inSystemSoundID). Podéis encontrar una lista de todos los system sounds en iOS aquí.

Lo más recomendable es definir los IDs que se vayan a utilizar con un nombre significativo, por ejemplo algo así:

#define systemSoundKeyPressed   1104
#define systemSoundUnlock       1101
#define systemSoundFailedUnlock 1102


Y hacerlos sonar así:

AudioServicesPlaySystemSound(systemSoundKeyPressed);


Hay que tener en cuenta que en el simulador no se oyen, deberéis probarlo en un dispositivo iOS.

@Fin

sábado, 28 de septiembre de 2013

UISearchBar & UITableView: Añadir una barra de búsqueda a una tabla.

Cuando una tabla tiene mucha información, hacer scroll por la ella buscando un elemento determinado puede llegar a ser frustante para el usuario. Una buena solución para mejorar esta situación es añadir un UISearchBar, que se integra con el UITableView y permite aplicar filtros y realizar búsquedas.

Vamos a ver un ejemplo sencillo de como crear una barra para hacer búsquedas y aplicar filtros sobre la información que muestra la tabla.

Crearemos un nuevo proyecto y en el storyboard añadiremos un table view controller embebido en un navigation controller. Colocaremos otro view controller y cablearemos desde la celda prototipo de la tabla hacia él para crear el segue, que será, por ejemplo, de tipo push. Hecho esto, tendremos que tener algo así:

Este último view controller será donde se muestra el detalle de lo seleccionado en la tabla. 

En esta UITableView vamos a mostrar deportes, así que vamos a definir la clase Sport, que será una subclase de NSObject y tendrá dos propiedades una con el nombre del deporte y otra con el tipo (motor, de pelota, acuático, etc.). Además definiremos un Class Method para crear deportes:

Sport.h: el interface por tanto quedará así:

#import <Foundation/Foundation.h>

@interface Sport : NSObject
@property (nonatomic, strong) NSString *type;
@property (nonatomic, strong) NSString *name;

+ (Sport *)newSport:(NSString *)name ofType:(NSString *)type;
@end

Sport.m: y su implementación será esta:

#import "Sport.h"

@implementation Sport
@synthesize name = _name;
@synthesize type = _type;

+ (Sport *)newSport:(NSString *)name ofType:(NSString *)type
{
    Sport *sport = [[self alloc] init];
    sport.name = name;
    sport.type = type;
    
    return sport;
}
@end


A continuación, vamos a añadir una subclase de UITableViewController que llamarermos SportTableViewController.




Esta nueva clase, será la Custom Class de nuestro Table View Controller, por tanto, en el storyboard seleccionaremos el UITableViewController creado inicialmente y en el Identity Inspector la elegiremos:



También vamos a asignar a la celda prototipo el identifier SportCell.



Y a dar un título a la tabla:


En SportTableViewController.h vamos a añadir la propiedad sports, que va a ser un NSArray. En este array meteremos posteriormente los deportes, va a ser el DataSource de la tabla:


#import <UIKit/UIKit.h>

@interface SportTableViewController : UITableViewController
@property (strong, nonatomic) NSArray *sports;
@end

Escribiremos su synthesize en SportTableViewController.m justo debajo de implementation y definiremos su get. También importaremos la clase Sport. Por ahora tendremos algo así:

#import "SportTableViewController.h"
#import "Sport.h"

@interface SportTableViewController ()
@end

@implementation SportTableViewController
@synthesize sports = _sports;



- (NSArray *)sports
{
    if (_sports == nil) {
        _sports = [NSArray arrayWithObjects:
                   [Sport newSport:@"Surf" ofType:@"Acuático"],
                   [Sport newSport:@"Fútbol" ofType:@"Pelota"],
                   [Sport newSport:@"Tenis" ofType:@"Pelota"],
                   [Sport newSport:@"Fórmula 1" ofType:@"Motor"],
                   [Sport newSport:@"kitesurf" ofType:@"Acuático"],
                   [Sport newSport:@"Motocicilismo" ofType:@"Motor"],
                   nil];
    }
    return _sports;
}


En una aplicación real, tendrías vuestros NSArray, NSDictinonary o lo que fuese para cargar en el UITableView, pero para simplificar el ejemplo hemos añadido algunos deportes en nuestro NSArray

Todos los métodos que están comentados y el numberOfSectionsInTableView: los podemos borrar ya que no los vamos a a utilizar.

El que si vamos a utilizar es tableView: numberOfRowsInSection:  que, por ahora, implementaremos así:


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [self.sports count];
}



Con esto estamos indicando que la tabla va a tener tantas filas como elementos tiene nuestro NSArray de deportes.

En el método tableView:cellForRowAtIndexPath: configuramos la celda:



- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"SportCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if ( cell == nil ) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
    }
    
    Sport *sport = [self.sports objectAtIndex:indexPath.row];
    cell.textLabel.text = sport.name;
    
    return cell;
}

Es el momento de ejecutar el proyecto para ver que todo va bien. Deberéis conseguir esto:



Todo lo hecho hasta ahora ha sido para crear una tabla con datos de ejemplo. A continuación veremos como añadir la barra de búsqueda.

En el Storyboard, seleccionaremos y arrastraremos  un Search Bar and Search Display Controller a nuestro Table View Controller.


El Search Bar nos proporciona un text field en el que introducir el texto a buscar, un botón de búsqueda y otro de cancel y, el search display controller, proporciona  una barra de búsqueda y una tabla que muestra el resultado de la búsqueda de datos manejados por la otra tabla.


Debemos indicar que nuestro UITableViewController va a implementar dos delegates, el de Search Bar y el de Search Display. Luego modificaremos nuestro SportTableViewController, que ahora será asÍ: 



#import <UIKit/UIKit.h>

@interface SportTableViewController : UITableViewController <UISearchBarDelegate, UISearchDisplayDelegate>
@property (strong, nonatomic) NSArray *sports;
@end

También crearemos dos nuevas propiedades para SportTableViewController; una será un array mutable que contendrá los deportes resultantes de aplicar el filtro de búsqueda y la otra será un IBOutlet de la barra de búsqueda. Este segundo lo vamos a añadir cableando desde el storyboard:







Estas dos últimas propiedades las he creado locales, el SportTableViewController.m:


@interface SportTableViewController ()
@property (weak, nonatomic) IBOutlet UISearchBar *searchBar;
@property (strong, nonatomic) NSMutableArray *filteredSports;
@end

@implementation SportTableViewController
@synthesize sports = _sports;
@synthesize searchBar = _searchBar;
@synthesize filteredSports = _filteredSports;




Ahora añadiremos el siguiente método para realizar el filtro.


#pragma mark Filter
-(void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope {
    [self.filteredSports removeAllObjects];

    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF.name contains[c] %@",searchText];
    self.filteredSports = [NSMutableArray arrayWithArray:[self.sports filteredArrayUsingPredicate:predicate]];
}

Básicamente primero reinicia el array de deportes filtrados, por si tuviera cargado algo anteriormente, y luego utiliza NSPredicate para ver que deportes de los que existen en el NSArray de deportes coinciden con el texto introducido. 

Implementamos un método de UISearchDisplayControllerDelegate que será el encargado de llamar al filtro anterior cada vez que el usuario introduce un  nuevo carácter en la barra.


#pragma mark - UISearchDisplayController Delegate
-(BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString {
    [self filterContentForSearchText:searchString scope:
     [[self.searchDisplayController.searchBar scopeButtonTitles] objectAtIndex:[self.searchDisplayController.searchBar selectedScopeButtonIndex]]];

    return YES;
}



Dentro de tableView:cellForRowAtIndexPath: habrá que hacer un cambio para comprobar si estamos mostrando la tabla de deportes o la tabla con los resultados del filtro. Cambiaremos la linea


Sport *sport = [self.sports objectAtIndex:indexPath.row];


por


Sport *sport;
    
if (tableView == self.searchDisplayController.searchResultsTableView) {
        sport = [self.filteredSports objectAtIndex:indexPath.row];
} else {
        sport = [self.sports objectAtIndex:indexPath.row];
}


Lo mismo en tableView:numberOfRowsInSection:, habrá que comprobar que tabla se está mostrando:


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    NSInteger result;

    if (tableView == self.searchDisplayController.searchResultsTableView) {
        result = [self.filteredSports count];
    } else {
        result = [self.sports count];
    }
    
    return result;
}



Y con esto ya lo tendríamos:




Si queremos que, inicialmente, la barra de búsqueda esté oculta, podemos añadir este código en el viewDidLoad:


CGRect newBounds = self.tableView.bounds;
newBounds.origin.y = newBounds.origin.y + self.searchBar.bounds.size.height;
self.tableView.bounds = newBounds;



Solo nos quedaría mandar los datos al viewcontroller que va a contener el detalle. Bastaría con implementar el tableView:didSelectRowAtIndexPath

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 

 // Perform segue to sport detail 
 [self performSegueWithIdentifier:@"sportDetailSegue" sender:tableView]; }
Y luego en prepareForSegue: pasar la información necesaria al ViewController del detalle, pero eso ya os lo dejo a vosotros.

@Fin





sábado, 31 de agosto de 2013

Animación de imágenes en iOS

En iOS se pueden programar animaciones de imágenes de manera sencilla. Vamos a ver un ejemplo en el que dos UIImageView intercambian sus posiciones, deslizándose de una a otra, cuando el usuario toca una de ellas. Además veremos las diferencias de usar el sistema de springs y struts o Autolayout.

Primero vamos a crear los dos UIImageView en el storyboard, image1 e image2 y cablearemso hacía el código para crear las propiedades.



De esta forma en el interface tendremos algo así:


@interface AnimacionViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *image1;
@property (weak, nonatomic) IBOutlet UIImageView *image2;
@end

Desde el inspector de atributos a una la pondremos como Background amarillo y a la otra rojo para poder diferenciarlas. Hay que tener también en cuenta que se debe permitir la interacción del usuario, por lo que el check deberá estar marcado tal y como se ve en la siguiente figura.




Ahora añadiremos el reconocimiento de gestos. Como lo que queremos es que la animación se produzca al tocar las imágenes, necesitamos el UITapGestureRecognizer. Lo crearemos en el viewDidLoad y se lo añadimos a las dos imágenes.



- (void)viewDidLoad
{
    [super viewDidLoad];
    
UITapGestureRecognizer *tapGesture1 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap)];

UITapGestureRecognizer *tapGesture2 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap)];
    
    [self.image1 addGestureRecognizer:tapGesture1];
    [self.image2 addGestureRecognizer:tapGesture2];
}

Como veis, la acción que se va a ejecutar es tap, que todavía no hemos definido. En ella lo que vamos a hacer es ejecutar el mensaje interchangImageView:imageView1 withImageView:imageView2, que también tenemos que definir.


- (void)tap
{
    [self interchangImageView:self.image1
                withImageView:self.image2];
    
}


Y es en interchangImageView:imageView1 withImageView:imageView2 donde veremos la diferencia de usar Autolayout o no.

Sin Autolayout


- (void)interchangImageView:(UIImageView *)imageView1 withImageView:(UIImageView *)imageView2
{
    CGPoint image1CenterPoint = imageView1.center;
    CGPoint image2CenterPoint = imageView2.center;
    
    [UIView animateWithDuration:1.0 animations:^{
        imageView1.center = image2CenterPoint;
        imageView2.center = image1CenterPoint;
    }];
}

Vemos que simplemente obtenemos el centro de cada imagen y luego los intercambiamos utilizando el class method animateWithDuration:animations de UIView. La duración de la animación será un segundo y en en bloque animations es donde intercambiamos los centros de las imágenes.

Con Autolayout
El método anterior no funciona si autolayout está activado. Si se moverían las imágenes pero, debido a las constraint definidas, volverían a su posición original. Por eso la animación debe basarse en las constraints. 

Añadiremos las constraints necesarias a ambas imágenes. En este caso son Top Space, Leading Space, Width y Height.


Cablearemos las dos Vertical Space para crear las dos propiedades NSLayoutConstraint, con lo que el interface ahora deberá quedar así:


@interface AnimacionViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *image1;
@property (weak, nonatomic) IBOutlet UIImageView *image2;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *verticalConstraintImage1;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *verticalConstraintImage2;
@end


Y el interchangImageView:imageView1 withImageView:imageView2 en este caso lo crearemos así:


- (void)interchangImageView:(UIImageView *)imageView1 withImageView:(UIImageView *)imageView2
{
    CGFloat img1ConstraintConstant = self.verticalConstraintImage1.constant;
    CGFloat img2ConstraintConstant = self.verticalConstraintImage2.constant;
    
    self.verticalConstraintImage1.constant = img2ConstraintConstant;
    self.verticalConstraintImage2.constant = img1ConstraintConstant;
    
    [imageView1 setNeedsUpdateConstraints];
    [imageView2 setNeedsUpdateConstraints];
    
    [UIView animateWithDuration:1.0 animations:^{
        [imageView1 layoutIfNeeded];
        [imageView2 layoutIfNeeded];
    }];
}

Lo que conseguimos con esto es intercambiar la distancia de ambas al top layout, por eso una subirá y la otra bajará. Al haber cambiado las constraints de las imágenes, es necesario llamar al mensaje setNeedsUpdateConstraints. Y, en el animateWithDuration, llamamos a layoutIfNeeded.

@Fin


martes, 13 de agosto de 2013

Utilización de diferentes versiones del Storyboard

Con el nuevo aspecto del interface en el iOS 7, probablemente os habréis encontrado con que vuestras aplicaciones diseñadas para versiones anteriores no quedan bien.

Para evitar tener que rediseñar toda la aplicación para que tenga la misma apariencia en todas las versiones de iOS, se pueden crear dos (o los necesarios) storyboards, uno para la versión 7 y posteriores y otro para las anteriores. De esta forma se consigue que los usuarios que migren a la nueva versión del iOS tengan un interfaz acorde con el aspecto del iOS 7 pero, los que no migren, mantengan la apariencia de las versiones anteriores con el que ya están familiarizados.

Lo primero que tenemos que hacer es asegurarnos que, dentro de la pestaña General,  el Main interface está vacío. De esta forma no asignamos ningún storyboard por defecto.



Después haremos una copia del storyboard actual a la que vamos a llamar, por ejemplo iPhoneiOS7.storyboard. A la versión actual la renombraremos a iPhoneiOS6.storyboard.



Es conveniente también que en el File inspector, se seleccionen las siguientes opciones para cada storyboard.

iPhoneiOS6.storyboard

iPhoneiOS7.storyboard

En el AppDelegate, añadiremos el siguiente código dentro del application:willFinishLaunchingWithOptions:


UIStoryboard *mainStoryboard = nil;

if ([[[UIDevice currentDevice] systemVersion] compare:@"7.0" options:NSNumericSearch] == NSOrderedAscending)
{
    // Versiones anteriores a iOS 7
    // Instaciamos un objeto storyboard usando el archivo iPhoneiOS6.storyboard
    mainStoryboard = [UIStoryboard storyboardWithName:@"iPhoneiOS6" bundle:nil];
    
}
else
{
    // Versiones iguales o posteriores a iOS 7
    // Instaciamos un objeto storyboard usando el archivo iPhoneiOS7.storyboard
    mainStoryboard = [UIStoryboard storyboardWithName:@"iPhoneiOS7" bundle:nil];
    
}

// Instanciamos un objeto UIWindow y lo inicializamos con el tamaño de pantalla del dispositivo
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

// Hacemos que el initialViewController sea el rootviewcontroller
self.window.rootViewController = [mainStoryboard instantiateInitialViewController];

Además, en el application:didFinishLaunchingWithOptions: añadiremos la siguiente línea de código justo antes del return:

[self.window makeKeyAndVisible];


Con esto, lo que faltaría sería modificar el iPhoneiOS7.storyboard para adaptarlo al interfaz de la nueva versión del iOS. Una vez modificado se ejecutaran diferentes storyboards según la versión de iOS en la que se ejecute la App.

@Fin

domingo, 24 de marzo de 2013

Localización de imagenes en iOS

En esta ocasión os voy a contar como podemos hacer la localización para diferentes idiomas de imagenes que incluyen texto sin utilizar ni una sola línea de código.

Basta con añadir la imagen a nuestro proyecto Xcode y, seleccionándola ..


... accedemos al menú Utilities, sección Localization y pulsamos Localize.


Aparecerá una ventana en la que debemos indicar el idioma de la imagen:


De esta forma, indicaremos el idioma de la imagen seleccionada y, automáticamente, en la sección Localization aparecerán todos los idiomas de nuestra App.


Deberemos seleccionar las localizaciones deseadas y copiar el archivo correspondiente en su directorio lproj. Por ejemplo, si vamos a añadir la imagen en español, marcaremos el check Spanish y copiaremos el archivo con la imagen en el directorio es.lproj.

Y eso es todo.

@Fin