iOS Tutorial: Webservices y JSON con Youtube API Objective-C y Swift. Parte 4: UITableView y UISearchBar

Esta es la cuarta parte (y el final) del tutorial de webservices: Búsqueda Asíncrona con Youtube API V3, JSON, Grand Dispatch Central y MVC. En la primera parte configuramos el proyecto, presentamos la arquitectura así como el patrón MVC. En la segunda parte vimos como usar el patrón de diseño Adapter para encapsular los datos del web service. En la tercera parte vimos cómo llamar al webservice de manera asíncrona con GDC y usando NSJSONSerialization para convertir a objetos de Foundation.

Bájate el código de swift de github.
Bájate el código de Objective-c de github.

En esta cuarta parte conectaremos los elementos del MVC con el cliente del web service y presentaremos los datos en una UITableView. Como ya les vi mucha hueva, mejor comenzamos.

Conectando el MVC.

Agreguemos una propiedad privada a la clase del modelo para guardar la lista de Resultados de la búsqueda en un Array; en objective c agreguemos una categoría del view model justo debajo del import para hacerlo privado, no te olvides de importar la clase SearchItem:
Objective-C
ViewModel.m

//..
#import "SearchItem.h"
@interface ViewModel ()
@property (strong,nonatomic)NSArray *items;
@end

@implementation ViewModel

@end

Swift
En Swift agreguemos una propiedad privada:
ViewModel.swift

import Foundation

class ViewModel {
  private var items :[YoutubeClient.SearchItem]?
  
}

Como comenté en la primera parte, usaremos un patrón delegado para notificar al Controller de los cambios en el modelo, pa mantener a cada wey en su lugar y que no se mezclen sus responsabilidades, entonces usaremos un protocol para definir el delegado, checa que declaramos las propiedades como weak para evitar retain cycles:
Objective-C
ViewModel.h

//..
@protocol ViewModelDelegate 
- (void)searchResultsDidChange;
@end

@interface ViewModel : NSObject
@property (weak,nonatomic)id delegate;
@end
..//

Swift.

import Foundation
protocol ViewModelDelegate: class {
  func searchResultsDidChange()
  func searchDidFailWithError(error: NSError)
}

class ViewModel {
  private var items :[YoutubeClient.SearchItem]?
  weak var delegate :ViewModelDelegate?
}

Vamos a enchufar el modelo y el controlador con el método delegado. Como el chiste del MVC es que sea un trio, deben estar loosely coupled, es decir, no tan ensartados, puedes pensar en el protocolo como un condón, les sirve para cooperar, pero no están tan integrados, el protocolo les permite conectarse con otras instancias teoréticas. Así que ya sabes, cuando vayas a la farmacia, le pides que te venda un protocolo, no un condón.

Primero hacemos que nuestro controller sea conforme con el protocolo ViewModelDelegate, agregamos la instancia del modelo y la inicializamos de manera huevona (lazily initialization)en objective-C, también creamos el método del protocolo.
Objective-C
ViewController.m

#import "ViewController.h"
#import "ViewModel.h"
@interface ViewController () 
@property (strong,nonatomic) ViewModel *model;
@end

@implementation ViewController

- (ViewModel*)model {
  if (_model == nil) {
    _model = [[ViewModel alloc] init];
    _model.delegate = self;
  }
  return _model;
}

- (void)searchResultsDidChange {
  
}

- (void)searchDidFailWithError:(NSError *)error {
}
//..
@end

Swift.
ViewController.swift

class ViewController: UITableViewController, ViewModelDelegate {

  let model: ViewModel!
  
  override init() {
    super.init()
    self.model = ViewModel(delegate: self)
  }

  required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    self.model = ViewModel(delegate: self)
  }
  
  func searchResultsDidChange() {
    
  }

  func searchDidFailWithError(error: NSError) {

  }
  //..

}

Lo que haremos es que cuando se notifique al controller que los resultados han cambiado, debemos recargar los datos de la tabla, en caso de error, que muestre una alerta:

Objc
ViewController.m

- (void)searchResultsDidChange {
  dispatch_async(dispatch_get_main_queue(), ^{
    [self.tableView reloadData];
  });
}

- (void)searchDidFailWithError:(NSError *)error {
  UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Error" message:error.description preferredStyle:UIAlertControllerStyleAlert];
  [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
  [self presentViewController:alert animated:YES completion:nil];
}

Swift.
ViewController.swift

func searchResultsDidChange() {
  dispatch_async(dispatch_get_main_queue(), { () -> Void in
  self.tableView.reloadData()
})
}

func searchDidFailWithError(error: NSError) {
  let alert = UIAlertController(title: "Error", message: error.description, preferredStyle: UIAlertControllerStyle.Alert)
  alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
  self.presentViewController(alert, animated: true, completion: nil)
}

Ahora implementaremos en el controlador los métodos delegados del UISearchBar que agregamos en el UITableView al inicio del tutorial, así mantendremos al modelo actualizado a través de una propiedad que le agregaremos:
Objective-C
ViewModel.h

//..
@interface ViewModel : NSObject
@property (weak,nonatomic)id delegate;
@property (strong,nonatomic)NSString *query;
@end

ViewModel.m

@interface ViewModel ()
@property (strong,nonatomic)NSArray *items;
@property (strong,nonatomic)YoutubeClient *youtubeClient;
@end

@implementation ViewModel
- (void)setQuery:(NSString*)query {
  _query = query;
  if ([query length]>0) {
    [self.youtubeClient searchWithQuery:_query success:^(NSArray *results) {
      self.items = results;
      [self.delegate searchResultsDidChange];
    } failure:^(NSError *error) {
      [self.delegate searchDidFailWithError:error];
    }];
  }
}
@end

Swift.
ViewModel.swift

//..
class ViewModel {
  private var youtubeClient = YoutubeClient()
  private var items :[YoutubeClient.SearchItem]?
  weak var delegate :ViewModelDelegate!
  
  private var _query: NSString?
  var query: NSString? {
    get {
      return _query
    }
    set {
      _query = newValue
      if (_query?.length > 0) {
        self.youtubeClient.search(_query!, success: { (searchResults) -> Void in
          self.items = searchResults
          self.delegate.searchResultsDidChange()
          }, failure: { (error) -> Void in
            self.delegate.searchDidFailWithError(error)
        })
      }
    }
  }
  
  init (delegate:ViewModelDelegate) {
    self.delegate = delegate
  }
}

Ahora implementamos el método delegado de UISearchBarDelegate en los controllers:
Objc.
ViewController.m

@interface ViewController ()
@property (strong,nonatomic) ViewModel *model;
@end

@implementation ViewController
//..
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
  self.model.query = searchBar.text;
  [searchBar resignFirstResponder];
}
//..
}

Swift.

class ViewController: UITableViewController, ViewModelDelegate, UISearchBarDelegate {
//..
  func searchBarSearchButtonClicked(searchBar: UISearchBar) {
    self.model.query = searchBar.text
    searchBar.resignFirstResponder()
  }
  //..
}

Cuando el usuario haga tap sobre el botón de búsqueda, el controlador recibirá el texto y lo pondrá en la propiedad query del modelo, antons el modelo ejecutará la búsqueda en el YoutubeClient, y el cual retornará una lista de resultados o un error, los resultados serán guardados en la propiedad items del model y el delegado (el controlador) será notificado de los cambios, y martin besará a su mamá y ella se enamorará de él y no podrá nacer, causando un paradoja en el continum-espacio!

Exponer los resultados del modelo

Para terminar con el modelo vamos a implementar dos métodos para sacarle los results al model y al mismo tiempo mantener los datos encapsulados. Software Engineering, bitch!:

Objc.
ViewModel.h

@class SearchItem;
@protocol ViewModelDelegate 
- (void)searchResultsDidChange;
- (void)searchDidFailWithError:(NSError*)error;
@end

@interface ViewModel : NSObject
@property (weak,nonatomic)id delegate;
@property (strong,nonatomic)NSString *query;

- (NSInteger)itemCount;
- (SearchItem*)itemAtIndex:(NSInteger)index;
@end

ViewModel.m

#import "SearchItem.h"
//..
@implementation ViewModel
//..
- (NSInteger)itemCount {
  return [self.items count];
}

- (SearchItem*)itemAtIndex:(NSInteger)index {
  return self.items[index];
}
@end

Swift.
class ViewModel {

//..
  func itemCount()-> Int {
    if let count = self.items?.count {
      return count
    }
    return 0
  }
  
  func itemAtIndex(index: Int) -> YoutubeClient.SearchItem {
    return self.items![index]
  }
}

 

Implementando los métodos del delegado y datasource de UITableView.

Ahora lo que resta es implementar el UITableView para que use el modelo, entonces lo primero es agregar los métodos del Delegate y Data Source de la table:
Objective-C
ViewController.m

@implementation ViewController
//..
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
  return 1;
}

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

- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:UITableViewCellReuseIdentifier];
  if (cell == nil) {
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:UITableViewCellReuseIdentifier];
  }
  SearchItem *item = [self.model itemAtIndex:[indexPath row]];
  cell.textLabel.text = item.title;
  cell.detailTextLabel.text = item.videoDescription;
  return cell;
}

@end

Swift.
ViewController.swift

class ViewController: UITableViewController, ViewModelDelegate, UISearchBarDelegate {
//..
  override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return 1
  }
  
  override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return self.model.itemCount()
  }
  
  override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    var cell = tableView.dequeueReusableCellWithIdentifier(Constants.CellIdentifier) as UITableViewCell?
    if cell == nil {
      cell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: Constants.CellIdentifier)
    }
    let item = self.model.itemAtIndex(indexPath.row)
    cell?.textLabel.text = item.title
    cell?.detailTextLabel?.text = item.videoDescription
    return cell!
  }
//..
}

EL código es bastante sencillo we, wacha, le decimos al tableview que solo hay una sección, que contiene el número de elementos contenidos en el model, y cuando el tableview solicite elementos al controller, a su vez se las va a pedir al model, porque el controller es un pinche huevo que solo da ordenes y los demás hacen la chamba por él, de eso se trata el MVC.

Listo!!
Córrelo y puedes introducir algún query que quieras, y ve los resultados de tu esfuerzo.

Con esto termino el tutorial, ahora ya debes ser un chingón en MVC, Adapter Pattern, Grand Dispatch Central (GDC), JSON y NSJSONSerialization, etc… espero que haya quedado verga y te haya dado gusto, si eres de los dudosos, pon tu pregunta en los comentarios, y recuerda que puedes bajar el código completo desde github, encuentra los links al inicio.

Cheers!

Webservices y JSON con Youtube API. Parte 3: Llamadas asíncronas al Webservice y NSJSONSerialization.

Esta es la tercera parte del Tutorial de Youtube; si aún no lo haces, checa cómo está la arquitectura y el patrón MVC del proyecto, y  como usaremos el patrón adaptador para simplificar el desarrollo.

Bájate el código de swift de github.
Bájate el código de Objective-c de github.

En esta parte del tutorial veremos como usar colas de GDC (Grand Dispatch Central) para hacer las llamadas asíncronas al Webservice, y cómo usar NSJSONSerialization para poder consumir los resultados.

Llamadas asincronas???.

Sí wey, cuando integras tu app con un webservice hay un espacio en el que tu app está esperando una respuesta. Con todo y que las conexiones de estos tiempos son muy rápidas, si hiciéramos las llamadas al servicio en el hilo principal, nuestra UI se bloquearía hasta que tuviera la respuesta del service, no es buena idea.

Lo que podemos hacer es programación concurrente, o multihilo, o como quieras llamarle. En iOS existen varias opciones, están los NSThread, NSOperation y GDC. Con cualquiera de las tres opciones le podemos dar en la madre al problema, pero los primeros dos representan un poco más de complejidad en el código a cambio de mayor control (puedes cancelar operaciones o establecer prioridades de los hilos), pero en nuestro caso no necesitamos de tanta configuración, por lo que vamos a usar Grand Dispatch Central.

GDC y la cola (pasas).

Vamos a nuestra clase YoutubeClient. Lo que haremos a continuación es agregar un método de clase para realizar la llamada al Webservice de manera asíncrona. Pero primero necesitamos crear una cola (rolas) para poner las órdenes de búsqueda:

Objective-C
Vamos a modificar el archivo YoutubeClient.m para agregar la cola (me das miedo), y de una vez deja te la inicializo.
 

@implementation YoutubeClient {
  dispatch_queue_t searchQueue;
}

- (instancetype) init {
  if (self = [super init]) {
    searchQueue = dispatch_queue_create("YoutubeClient.SearchQueue", 0);
  }
  return self;
}
@end

Swift
En swift hacemos mas o menos lo mismo pero en una sola línea:

import Foundation
class YoutubeClient {
  private let searchQueue = dispatch_queue_create("YoutubeClient.SearchQueue", nil)
// todo el demás código...
}

En esta línea te declaro la cola (medallas) y te la estoy inicializando. No te olvides de importar Foundation para usar GDC.

Ya que tienes la cola desquintillada podemos darle uso, con un método vergas para hacer la búsqueda. El método va a tener tres parámetros, primero, vamos a estar metiendo textos en el webservice en el parámetro query, luego dos blocks, uno que se va a ejecutar cuando la búsqueda tenga éxito y que va a recibir el array de SearchItem, y otro que se ejecutará cuando falle y recibirá un NSError*:
Objective-C
YoutubeClient.h

#import 

@interface YoutubeClient : NSObject
- (void)searchWithQuery:(NSString*)query success:(void (^)(NSArray* results))successBlock failure:(void (^)(NSError *error))failureBlock;
@end

Para usar tu cola como te gusta, vamos a usar la función dispatch_async, esta función necesita dos parámetros, el primero es la cola (te florea), segundo es un block que es el que se va a ejecutar:
YoutubeClient.m

@implementation YoutubeClient {
//...
- (void)searchWithQuery:(NSString *)query success:(void (^)(NSArray *))successBlock failure:(void (^)(NSError *))failureBlock {
  dispatch_async(searchQueue, ^{
    
  });
}
@end

Swift.

class YoutubeClient {
//...
  
  func search(query:String,success : [SearchItem] -> Void, failure : NSError -> Void) {
    dispatch_async(self.searchQueue, { () -> Void in
      
    })
  }
}

En ambos lenguajes vamos a agregar dos métodos auxiliares, uno para descargar los datos y otro para convertirlos con la clase NSJSONSerialization, esta clase toma datos JSON desde una String o NSData y los convierte en instancias de Diccionarios o Arrays:

Objective-C

@implementation YoutubeClient 
//..
- (NSData*)fetchURL:(NSURL*)url error:(NSError * __autoreleasing *)error {
  NSData *data = [NSData dataWithContentsOfURL:url options:0 error:error];
  if (*error != nil) {
    return nil;
  }
  return data;
}

- (id)parseJSONFromData:(NSData*)data error:(NSError * __autoreleasing *)error {
  id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:error];
  if (*error!= nil) {
    return nil;
  }
  return jsonObject;
}
@end

Swift.
En Swift a demás de estos métodos, vamos a agregar un Enum RequestResult que nos va a ayudar a saber internamente si la solicitud tuvo éxito o un error.

class YoutubeClient {
  private enum Result {
    case success(result: Any)
    case failure(error: NSError)
  }
//...
  
  private func fetchURL(url: NSURL) -> Result {
    var serviceError: NSError?
    let data: NSData! = NSData(contentsOfURL: url, options: NSDataReadingOptions.allZeros, error: &serviceError)
    if serviceError != nil {
      return Result.failure(error: serviceError!)
    }
    return Result.success(result: data)
  }
  
  private func parseJSON(data: NSData) -> Result {
    var jsonError : NSError?
    let dataDictionary = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.allZeros, error: &jsonError) as [String:AnyObject]?
    if jsonError != nil {
      return Result.failure(error: jsonError!)
    }
    return Result.success(result: dataDictionary!)
  }
}

El método de la búsqueda.
Entonces te vamos a poner en la cola una instancia del block que contendrá la llamada al webservice, esta llamada la haremos con el método de la clase NSData para crear una instancia desde una URL:

Objc

@implementation YoutubeClient
//..
- (void)searchWithQuery:(NSString *)query success:(void (^)(NSArray *))successBlock failure:(void (^)(NSError *))failureBlock {
  dispatch_async(searchQueue, ^{
    NSString *escapedQuery = [query stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
    NSString *urlString = [NSString stringWithFormat:@"https://www.googleapis.com/youtube/v3/search?part=snippet&q=%@&type=video&key=%@",escapedQuery,YoutubeClientAPIKey];
    NSError *serviceError = nil;
    NSData *searchData = [self fetchURL:[NSURL URLWithString:urlString] error:&serviceError];
    if (serviceError) {
      failureBlock(serviceError);
      return;
    }
    NSError *jsonError = nil;
    NSDictionary *jsonDictionary = [self parseJSONFromData:searchData error:&jsonError];
    if (jsonError != nil) {
      failureBlock(jsonError);
      return;
    }
    assert([jsonDictionary isKindOfClass:[NSDictionary class]]);
    NSArray *items = jsonDictionary[YoutubeClientItemsKey];
    NSMutableArray *adapterArray = [NSMutableArray array];
    for (NSDictionary *item in items) {
      [adapterArray addObject:[[SearchItem alloc] initWithDictionary:item]];
    }
    successBlock(adapterArray);
  });
}
//..
}
@end

Swift.

class YoutubeClient {
//..
func search(query:String,success : [SearchItem] -> Void, failure : NSError -> Void) {
    dispatch_async(self.searchQueue, { () -> Void in
      
      let escapedQuery: String! = query.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())
      let urlString = "https://www.googleapis.com/youtube/v3/search?part=snippet&q=\(escapedQuery)&type=video&key=\(Constants.apiKey)"
      switch self.fetchURL(NSURL(string: urlString)!) {
      case let .failure(error):
        failure(error)
        
      case let .success(jsonData):
        switch self.parseJSON(jsonData as NSData) {
        case let .failure(error):
          failure(error)
          
        case let .success(jsonDictionary):
          if let items = (jsonDictionary as? JSONDictionary)?[Constants.items] as? [JSONDictionary] {
            var adapterArray = [SearchItem]()
            for item in items {
              adapterArray.append(SearchItem(dictionary: item))
            }
            success(adapterArray)
          } else {
            failure(NSError(domain: Constants.Error.errorDomain, code: Constants.Error.noItemsFound, userInfo: nil))
          }
        }
      }
    })
  }
//..
}

Básicamente hacen lo mismo, descargan los datos, si hay errores los notifican, y si no, convierten a JSON, de nuevo checa si hay errores, si no extraen los items y los convierte a instancias de nuestro Adapter, finalmente llama al closure o block de étzito!!, junto con el array de adapters.

En la última parte del tutorial veremos como conectar el cliente con el modelo de nuestra búsqueda y finalmente mostrar todos los resultados en la tabla.

Webservices y JSON con Youtube API. Parte 2: Adapter Pattern

En esta segunda parte te mostraré como usar el patrón adaptador para facilitar el uso de objetos JSON (Diccionarios y arrays). El patrón adaptador es un patrón de diseño que te permite usar instancias de una clase como si fueran otras, es decir encapsula el significado de la clase y actúa como interfaz hacia afuera.

Bájate el código de swift de github.
Bájate el código de Objective-c de github.

Si aún no has configurado tu proyecto, checa la primera parte, donde configuramos el MVC y explicamos la arquitectura.

Agreguemos una clase SearchItem.

Ahora si lo interesante de esta segunda parte, el adaptador. El objetivo de este adaptador es exponer como propiedades los objetos contenidos en el diccionario, para que en lugar de tener diccionarios y llaves regados por todo el código, estos estarán contenidos en el adaptador, que solo los mostrará hacia afuera como las propiedades.

La clase SearchItem representará un solo resultado de la búsqueda. Entonces crea una nueva clase, en Objective-C lo agregaremos como una subclase de NSObject y en Swift lo agregaremos como clase anidada de YoutubeClient:

El objetivo de la clase SearchItem es encapsular los datos que obtendremos del webservice, traduciéndolos de JSON a un objeto nativo, una muestra de un resultado en JSON sería algo como esto:

{
    "kind": "youtube#searchResult",
    "etag": "\"PSjn-HSKiX6orvNhGZvglLI2lvk/otIrgrwhRy2Ap4z3JppVBTPquf4\"",
    "id": {
        "kind": "youtube#video",
        "videoId": "sOnqjkJTMaA"
    },
    "snippet": {
        "publishedAt": "2009-10-03T06:51:49.000Z",
        "channelId": "UCulYu1HEIa7f70L2lYZWHOw",
        "title": "Michael Jackson - Thriller",
        "description": "Music video by Michael Jackson performing Thriller. (C) 1982 MJJ Productions Inc. #VEVOCertified on October 29, 2010. http://www.vevo.com/certified ...",
        "thumbnails": {
            "default": {
                "url": "https://i.ytimg.com/vi/sOnqjkJTMaA/default.jpg"
            },
            "medium": {
                "url": "https://i.ytimg.com/vi/sOnqjkJTMaA/mqdefault.jpg"
            },
            "high": {
                "url": "https://i.ytimg.com/vi/sOnqjkJTMaA/hqdefault.jpg"
            }
        },
        "channelTitle": "michaeljacksonVEVO",
        "liveBroadcastContent": "none"
    }
}

Ya sé que podríamos usar el diccionario que nos devuelve la clases NSJSONSerialization para representar los resultados… ya lo sé!, pero no seas huevón, no es buena idea tener llaves del diccionario regadas por todos lados. Si creamos un adaptador el código será más claro y autodocumentado.

En nuestra clase SearchItem nos vamos a concentrar en el contenido de “snippet”  y “id”, que son dos diccionarios; para atacar el problema, vamos a dividir el problema, en Objective-C haremos lo siguiente.

  1. Definir propiedades en la clase SearchItem que nos devuelvan de manera directa y explícita los datos.
  2. Crear un inicializador que nos reciba el diccionario de donde va a extraer los datos.
  3. En lugar de tener las llaves todas regadas, las conservaremos escondidas en constantes en el archivo m.
  4. Preferiremos un modelo huevón, ejem.. Lazy; para los más lentos: o sea que obtendremos el valor cada que sea necesario, no en el momento de la inicialización.

Objc.
SearchItem.h

#import <Foundation/Foundation.h>

@interface SearchItem : NSObject
// Estas propiedades serán visibles hacia afuera, pero serán calculados en el momento que sean solicitadas.
@property (nonatomic,readonly) NSString *videoId;
@property (nonatomic,readonly) NSString *publishedAt;
@property (nonatomic,readonly) NSString *title;
@property (nonatomic,readonly) NSString *videoDescription;
@property (nonatomic,readonly) NSString *defaultThumbnail;
@property (nonatomic,readonly) NSString *mediumThumbnail;
@property (nonatomic,readonly) NSString *highThumbnail;
@property (nonatomic,readonly) NSString *channelTitle;

- (instancetype)initWithDictionary:(NSDictionary*)dictionary;
@end

SearchItem.m

#import "SearchItem.h"
// en lugar de tener cadenas mágicas por todos lados, concentramos las llaves aquí
NSString * const kVideoId = @"videoId";
NSString * const kPublishedAt = @"publishedAt";
NSString * const kTitle = @"title";
NSString * const kVideoDescription = @"description";
NSString * const kThumbnails = @"thumbnails";
NSString * const kChannelTitle = @"channelTitle";
NSString * const kId = @"id";
NSString * const kSnippet = @"snippet";

NSString * const kDefaultThumbnail = @"default";
NSString * const kMediumThumbnail = @"medium";
NSString * const kHighThumbnail = @"high";
NSString * const kURL = @"url";

@interface SearchItem ()
@property (strong,nonatomic) NSDictionary *dictionary;
@end

@implementation SearchItem
- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
  if (self = [super init]) {
    self.dictionary = dictionary;
  }
  return self;
}

- (NSDictionary*)snippet {
  return self.dictionary[kSnippet];
}
- (NSDictionary*)_id {
  return self.dictionary[kId];
}
- (NSDictionary*)thumbnails {
  return [self snippet][kThumbnails];
}

- (NSString*)videoId {
  return [self _id][kVideoId];
}

- (NSString*)publishedAt {
  return [self snippet][kPublishedAt];
}

- (NSString*)title {
  return [self snippet][kTitle];
}

- (NSString*)videoDescription {
  return [self snippet][kVideoDescription];
}

- (NSString*)channelTitle {
  return [self snippet][kChannelTitle];
}

- (NSString*)defaultThumbnail {
  return [self thumbnails][kDefaultThumbnail];
}

- (NSString*)mediumThumbnail {
  return [self thumbnails][kMediumThumbnail];
}

- (NSString*)highThumbnail {
  return [self thumbnails][kHighThumbnail];
}
@end

Swift
Para Swift vamos a seguir una estrategia similar, pero aprovechando algunas características de Swift:

  1. Anidaremos la clase SearchItem en YoutubeClient para establecer la jerarquía de clases y limitar su scope.
  2. Como las constantes de las llaves solo serán usadas aquí, también las anidaremos en un struct privado.
  3. Los thumbnail los encapsularemos en un miembro de SearchItem y lo anidaremos dentro de la misma clase de nuevo con el objetivo de limitar su scope a donde es necesario, esto es algo que extrañaba en Objective-C, la habilidad de crear namespaces.
  4. Al igual que en Objective-C solo expondremos al exterior las propiedades, que también serán Lazy, como tú comprenderás.
class YoutubeClient {
  // Usa el anidado de clases para crear una jerarquía de clases, o namespace
  class SearchItem {
    // una de las ventajas de anidar clases y structs es que puedes clasificarlos y reducir su scope
    private struct Constants {
      static let videoId = "videoId"
      static let publishedAt = "publishedAt"
      static let title = "title"
      static let videoDescription = "description"
      static let thumbnails = "thumbnails"
      static let channelTitle = "channelTitle"
      static let id = "id"
      static let snippet = "snippet"
    }
    class Thumbnails {
      private struct Constants {
        static let defaultThumbnail = "default"
        static let mediumThumbnail = "medium"
        static let highThumbnail = "high"
        static let url = "url"
      }
      private func thumbnailWithName(name:String) -> String? {
        if let thumbnail = self.dictionary[name] as? [String: AnyObject] {
          return thumbnail[Constants.url] as String?
        }
        return nil
      }
      private let dictionary: [String:AnyObject]!
      var defaultThumbnail: String? {
        get {
          return thumbnailWithName(Constants.defaultThumbnail)
        }
      }
      var mediumThumbnail: String? {
        get {
          return thumbnailWithName(Constants.mediumThumbnail)
        }
      }
      var highThumbnail: String? {
        get {
          return thumbnailWithName(Constants.highThumbnail)
        }
      }
      init(dictionary: [String:AnyObject]) {
        self.dictionary = dictionary
      }
    }
    
    private func snippetPropertyWithName(name: String) -> AnyObject? {
      if let snippet = self.dictionary[Constants.snippet] as? [String: AnyObject] {
        return snippet[name]
      }
      return nil
    }
    
    var videoId: String? {
      get {
        if let id = self.dictionary[Constants.id] as? [String: AnyObject] {
          return id[Constants.videoId] as String?
        }
        return nil
      }
    }
    var publishedAt: String? {
      get {
        return snippetPropertyWithName(Constants.publishedAt) as String?
      }
    }
    var title: String? {
      get {
        return snippetPropertyWithName(Constants.title) as String?
      }
    }
    var videoDescription: String? {
      get {
        return snippetPropertyWithName(Constants.videoDescription) as String?
      }
    }
    var channelTitle: String? {
      get {
        return snippetPropertyWithName(Constants.channelTitle) as String?
      }
    }
    
    let thumbnails: Thumbnails?
    
    private let dictionary: [String:AnyObject]!
    init (dictionary: [String:AnyObject]) {
      self.dictionary = dictionary
      if let thumbnailDictionary = self.snippetPropertyWithName(Constants.thumbnails) as [String:AnyObject]? {
        self.thumbnails = Thumbnails(dictionary: thumbnailDictionary)
      }
    }
  }
}

Checa el tutorial de variables y constantes en swift si tienes alguna duda de los opcionales en swift, o si de plano eres nuevo checa el tutorial para Aprender swift en 7 minutos, garantizado!!.

En la siguiente parte haremos las llamadas al webservice para obtener los datos en formato JSON.

Webservices y JSON con Youtube API. Parte 1: Arquitectura y MVC

En este tutorial te mostraré cómo usar Webservices que te devuelven JSON, como lo son todas las API nuevas de Google, en este caso usaré la API de Youtube para realizar búsquedas y mostrar los resultados en un Table View. El código lo veremos al mismo tiempo en Objective-C y en Swift (chécate el tutorial para Aprender a programar en Swift en 7 minutos!).

Bájate el código de swift de github.
Bájate el código de Objective-c de github.

En la actualidad, muchas apps bajan contenido de internet, aunque hay muchos formatos que pueden usar, como XML, archivos de texto planos, archivos CSV, etc; sin duda uno de los más usados es el JSON (Se dice “Yeison”, no me salgas con la mamada de “JOTASÓN”, asco!!), probablemente por su simpleza.

Un archivo de JSON, es un archivo de texto que suelen contener en su estructura diccionarios o arrays o valores, que a su vez pueden contener otros diccionarios, arrays o valores; muchas APIs en la actualidad usan JSON; para este ejemplo usaremos la API V3 de Youtube.

En esta primera parte te mostraré como configurar el proyecto en ambos lenguajes, Swift y Objective-C. También crearemos la estructura básica del MVC.

En la segunda parte usaremos el patrón adaptador para convertir los diccionarios y arrays obtenidos desde el webservice para encapsular la responsabilidad de la conversión.
Ir a la segunda parte -> Adapter Pattern.

En la tercera parte te mostraré como hacer llamadas y consumir los datos de manera asíncrona del webservice, y cómo convertir el texto JSON a NSDictionary y NSArray.

En la parte final, conectaremos todos los componentes del MVC para mostrar los resultados en la UITableView.

Ok Caon, Comencemos.

Primero sigue los pasos que se indican en la página de la API V3 de Youtube; yo te esperaré tomando un café de java… oh no espera no tomo café, quise decir Cocoa, anda y ve…

¿Listo? Para ahora lo importante es que tengas tu API-KEY.

Arquitectura.

La arquitectura para esta App es muy sencilla

Captura de pantalla 2014-10-15 a la(s) 21.17.00

Para la comunicación entre Controlador y Modelo lo voy a resolver con el patrón delegado, para conservar uniformidad entre Swift y Objc, ya que Swift no implementa el patrón KVO debido a que Swift es type-safe.

Para el Client de Youtube emplearé métodos con callbacks en blocks o closures como se te acomode mejor (Objc, Swift).

Empecemos.

Crear el proyecto.

Usaré Xcode 6.1 que es el release estable al momento de escribir esto, pero los pasos serán similares en la versión que tengas de Xcode.

Empecemos por crear un nuevo Proyecto (File > New > Project), selecciona SingleView.

Ponle de nombre YoutubeSearch, en realidad le puedes poner como se te hinchen los huevos, pero por uniformidad con el código te recomiendo que le pongas así, ok???

Captura de pantalla 2014-10-15 a la(s) 21.25.01

En lenguaje escoge el lenguaje que quieras, en el tutorial veremos los dos, o sea que el que mejor te entre. En Devices selecciona solo iPhone para simplificar el ejemplo y desmarca la casilla de Core Data, en este tutorial todo lo haremos de memoria ;)

Configuración del MVC.

Como probablemente ya sabes MVC es el modelo favorecido para las Apps de iOS, pronto escribiré un tutorial sobre él, mientras lo que tienes que saber es:

  • El Modelo realiza cálculos sobre los datos, almacena, etc, es decir es la parte “inteligente” del patrón.
  • El controlador da ordenes y recibe retroalimentación.
  • La vista es estúpida, solo lo que sabe es verse bonita y avisar al controlador que hay cambios en la interfaz.

Lo primero que hay que hacer es cambiar nuestro controlador para que sea una subclase de UITableViewController.

Objective-C: ViewController.h

#import <UIKit/UIKit.h>

@interface ViewController : UITableViewController

@end
Swift: ViewController.swift

import UIKit

class ViewController: UITableViewController {

// ... no pondré todo el código

A continuación modificaremos el Storyboard, esta parte es igual para Ambos lenguajes. Abre tu Main.storyboard:

1. Elimina el controlador creado por defecto (View Controller)

Captura de pantalla 2014-10-15 a la(s) 21.39.49

2. Busca el Table View controller desde la barra de la derecha y arrástralo hacia el storyboard.

Captura de pantalla 2014-10-15 a la(s) 21.42.28

3. Selecciona el nuevo Table View Controller del Document Outline:

Captura de pantalla 2014-10-15 a la(s) 21.49.30

 

4. Selecciona el Identity Inspector en la barra de Utilities y en el Campo de Class escribe ViewController (Es nuestro controller)

Captura de pantalla 2014-10-15 a la(s) 21.44.35

 

También asegurare de Marcar la casilla Is Intial View Controller en el Attributes Inspector.

Captura de pantalla 2014-10-23 a la(s) 22.10.22

 

5. Selecciona la UITableView y verifica que el data source y el delegate esten conectados al View Controller en el Inspector de Connections, si no, conéctalos.

Captura de pantalla 2014-10-15 a la(s) 22.33.28

6. Busca la Search Bar en la Object Library y arrástrala sobre la table view en el storyboard para agregarla como subview.

Captura de pantalla 2014-10-15 a la(s) 22.31.23

7. Selecciona la UISearchBar del Document Outline:

Captura de pantalla 2014-10-23 a la(s) 22.03.22

8. Checa que el delegado de la UISearchBar esté asignado al Table View Controller, si no conectadlo tío!

Captura de pantalla 2014-10-23 a la(s) 22.03.30

9. Ahora ya tenemos nuestro Controller y Storyboard….

Agreguemos el Modelo.

El modelo se encargará de almacenar y solicitar la información al cliente, también notificará al controlador sobre los cambios en los datos.

Agrega un nuevo archivo (File > New > File) y Selecciona Swift File o Cocoa Touch Class (Objective-C), en Objective-C hazlo subclass de NSObject, llámalo ViewModel, obtendrás los siguientes archivos según el lenguaje:

Objc:

ViewModel.h

#import <Foundation/Foundation.h>

@interface ViewModel : NSObject

@end

ViewModel.m

#import "ViewModel.h"

@implementation ViewModel

@end

Swift.

ViewModel.swift

import Foundation
class ViewModel {

}

La clase YoutubeClient

La clase YoutubeClient se encargará de comunicarse de manera asíncrona con el webservice, jalará los datos y los devolverá como instancias del adaptador de resultados de la búsqueda.
Agrega un nuevo archivo (File > New > File) y Selecciona Swift File o Cocoa Touch Class (Objective-C), en Objective-C hazlo subclass de NSObject, llámalo YoutubeClient:

Objc:
YoutubeClient.h

#import <Foundation/Foundation.h>

@interface YoutubeClient : NSObject

@end

YoutubeClient.m

#import "YoutubeClient.h"

@implementation YoutubeClient

@end

Swift

import Foundation
class YoutubeClient {
  
}

OK. Con esto tenemos la estructura básica de la aplicación, en el siguiente tutorial te muestro cómo usar el Patrón Adaptador o en inglis Adapter pattern.

Aprende a programar para iPhone con swift: variables, constantes y tipos de datos

Si ya tienes algo de experiencia de programación puede que quieras leer el tutorial para aprender a programar para iPhone con swift en 7 minutos, en este articulo veremos a detalle las variables y constantes de swift, el nuevo lenguaje de programación de Apple.

Para que aprendas mas rápido y te hagas la vida más fácil, te recomiendo que abras un playground en Xcode 6+ (File > New > Playground), cada que veas un bloque e código como el siguiente podrás escribir (o para los huevones, copia y pegar) el código:

println("Hola mundo")

Variables y constantes

Las variables y constantes representan almacenes de datos, espero que no seas tan lento para darte cuenta que las variables se espera que cambien, y las constantes tendrán el mismo valor en el tiempo que dura dura.

Las variables se declaran usando la palabra var, y las constantes con la palabra let:

var age = 28
let adultAge = 18

Siguiendo las mejores practicas del lenguaje, las variables y constantes deben tener nombres descriptivos que con el simple nombre te digan para que chingaos la creaste, puedes nombrar tus variables y constantes como quieras, pero existen algunas regla y algunas guías:

Los nombres no deben comenzar comenzar con un número, pero pueden contener números en medio o al final, por convención, las variables y constantes comienzan con minúscula:

var NombreValidoPeroNoRecomendable : Tipo
var 123NombreInvalido = Tipo

Los nombres deberían usar camel case, es decir: usarasMayusculaEnElComienzoDeCadaPalabra; en los nombres de las variables en swift puedes usar Unicode, o sea que ya puedes escribir nombres de variables en correcto castellano, tío:

var  nombreExplícitoEnEspañol : Tipo // valid!!
let 你好 = “Hola” // valid!!

Los nombres no pueden contener espacios en blanco, o símbolos matemáticos o separadores.

Declaración

No como declaración de amor no mames, sino es la manera en que creas una nueva variable o constante, en general la sintaxis es la siguiente:

var nombreDeVariable : Tipo
let nombreDeConstante : Tipo

También puedes asignar un valor des el momento de la creación e la variable con la siguiente sintaxis:

var nombreDeVariable = valorInicial
let nombreDeConstante = datoConstante

El contenido de las variables las puedes cambiar cuando se te hinchen las pelotas, pero las constantes no se puede, simplemente el compilador no te dejara.

let name = "Emanuel"
var age = 10 // valor inicial 10
age = 11

Ejercicio. Intenta cambiar la constante nombre

Se pueden declarar muchas variables en una misma línea si las separas con una coma:

var edadInicial = 18, edadFinal = 22

Chingon tip: Si el valor del dato que estas creando no va a cambiar, decláralo como constante, con let, para que sea claro en tu código la intensión.

Anotaciones de tipo.

A menos que seas medio wey, ya notaste que en las declaraciones de variables y constantes puedes indicar el tipo de dato que la variable o constante va a contener, al igual tu novia que te maltrata, chantajea y manipula, el tipo e la variable o va a cambiar:

var day : String // solo podrá contener datos tipo String
day = "lunes" // ok

Ejercicio: nomas pa que me creas intenta asignar un numero entero a la variable day.

Inferencia de datos.

Una e las características que hace que swift este bastante chido es la inferencia de datos, aunque no es algo nuevo, resulta muy útil. La inferencia de datos significa que no tienes que ponerle a patín el tipo de dato que va a contener la variable o la constante, sino que Swift puede deducirlo por el contexto, por ejemplo:

var string = "Soy una variable tipo String"
// Equivale a :
// var string :String = "Soy una variable tipo String"

let integer = 31337
// Equivale a :
// let integer :Int = 31337

var array = ["elemento1","elemento2","elemento3"]
//var array :[String] = ["elemento1","elemento2","elemento3"]

var dictionary = [ "Key1" : "Valor1", "Key2" : "Valor2", "Key3" : "Valor3"]
//var dictionary :[String:String] = [ "Key1" : "Valor1", "Key2" : "Valor2", "Key3" : "Valor3"]

Por eso se dice que Swift es un lenguaje “type-safe“, o sea que Swift sabe de que tipo son las variables, y que tipo de valor devuelven las funciones, a diferencia de Objective-C donde los chequeos de tipo se hacían realmente en la ejecución, aunque el compilador hacía un chequeo superficial.

Conversiones de tipos.

Para acabar pronto, Swift no hace conversiones de tipo implícitas, nunca, ni siquiera con números, no insistas, no lograrás nada, como con tu ex que no has logrado olvidar.

Observa los siguientes ejemplos:

var implicitInteger = 1300
var implicitDouble = 37.0

var sum = Int(implicitDouble) + implicitInteger

var descriptionLabel = "Hola, soy "
var description = descriptionLabel + String(sum)

Try this: Intenta quitar las palabras Int, y String de las declaraciones en la variable sum y description. ¿Que pasa morro?

Recuerda, Swift es “Type Safe“, si no eres su tipo, no te va a querer, como la vecina de la que has estado enamorado desde los 12 años.

Podría parecer que en la línea:

var description = descriptionLabel + String(sum)

Simplemente se esta haciendo un cast, pero no es así, en realidad NO ES UN CAST. Lo que en realidad esta sucediendo es que estas creando una nueva instancia de String, ya que el operador “+” se encuentra sobrecargado para (String, String) -> String, o sea, tomar dos String y devolver otro, como nota cultural, en Swift puedes crear tus propios operadores.

El hecho de que Swift pueda checar el tipo de dato que contienen las variables y las constantes, significa menos errores en el tiempo de ejecución, aunque también como todo en la vida, hay un tradeoff, a cambio de menor flexibilidad.

Type Alias.

Swift al igual que otros lenguajes te permite crear alias para los diferentes tipos, puedes pensar en los alias como un typedef, o alias, de otros lenguajes, ¿Que pa qué sirve?, aaaa pinche muchacho resongón, pues sirven para darle un nombre más apropiado, o para ahorrarte unos teclazos:

typealias TimeInterval = Int
var timeInterval : TimeInterval = 60 // 1 Minuto

OK, en este caso escribiste más lineas que si hubieras escrito solo Int, o más aún de haber usado la inferencia de datos, pero ya no eres un puberto preparatoriano, si quieres hacer Ingeniería de software debes dejar la hueva de lado, anda ve y cuélgala y regresas…

Ya? OK. El chiste de esto es hacer el código más legible, autodocumentado.

Tuples

Las tuples son un tipo de dato en Swift que te permite agrupar dos o más valores en uno solo, lo cual te sirve para ahorrarte variables adicionales o estructuras de datos que solo usas en un sitio y ya jamás los vuelves a usar, lo cual no esta chico, no es buena ingeniería bato.

Las tuples las puedes crear en cualquier lugar, per o son usadas frecuentemente como retorno de una función:

var gradeArray = [ 100, 90, 96, 98, 100]

func gradeInformationForArray(gradeArray :[Int]) -> (average: Double, gradeCount: Int) {
  var sum = 0
  for grade in gradeArray {
    sum += grade
  }
  return (Double(sum)/Double(gradeArray.count),gradeArray.count)
}

let gradeInformation = gradeInformationForArray(gradeArray)
gradeInformation.0
gradeInformation.average
gradeInformation.1
gradeInformation.gradeCount

Las tuples pueden contener el número de valores que quieras, en este caso contiene un par de valores, el primero de tipo Double, el segundo es un Int. Chécate que al final, se puede hacer referencia a los valores del tuple de dos formas, con el índice (en caso de no haber proporcionado un nombre a la tuple) o con el nombre que se le proporciona en la declaración de la función al tipo de retorno, lo último no es necesario, puedes tener una tuple anónima y usar los índices.

Ejercicio. Quita el nombre de los elementos de la tuple (“average:”, “gradeCount.”) en la declaración de la función,  para que solo sea accesible con el índice.

Optionals.

Si has programado para C, Objective-C, o C++, estarás familiarizado con el concepto de NULL, o nil. Es un patrón común en estos lenguajes checar si el apuntador apunta a nulo para ver si existe un valor o prevenir accesos incorrectos a memoria.

En Swift no puedes asignar null o nil a variables normales, anda si no me crees inténtalo:

var notNullableVariable = "valor inicial"
notNullableVariable = nil

var notAValidDeclaration = nil
var anotherInvalidDeclaration :String = nil

¿ Ya vez?, te dije wey.

Entonces, ¿cómo putas le haces para indicar que no hay un valor?.

Enter the Optionals.

Los optionals no son nada nuevo, lenguajes como C# los soportan, el concepto es el equivalente a una caja que puede o puede no tener un valor, la caja en sí no hace nada, solo es el recipiente para transportar algo (some) o nada (nil):

var imAStringOptional : String? = nil
var imAnotherStringOptional : String? = "Valor"

let invalidParsedInt = "notANumber".toInt() // la cadena no es un número, devolverá nil
let validParsedInt = "31337".toInt() // la cadena puede ser convertida a Int, devolverá { Some 31,337 }

Si me hiciste caso y abriste un archivo de playground podrás ver que en la segunda declaración, el playground te muestra que la primera variable contiene nil y la segunda contiene { Some “Valor” }. También la primera constante devolverá nil, ya que esa cadena no se puede representar como entero, y la segunda constante sí se pude, por lo que te devolverá un opcional con some.

Ejercicio. Intenta asignar nil a imAnotherStringOptional

OK, entonces cómo accedo a los datos dentro de la caja Some. Existen varias formas:

Forced Unwrapping.

Si estas seguro que el optional contiene some, es posible abrir la caja por la fuerza, lo que se conoce como Forced Unwrapping, siguiendo con el ejemplo de los optionals, si el optional es diferente a nil, quiere decir que contiene some, para forzar la extracción, usa el operador unario ! después de la variable opcional para desempaquetarla

if invalidParsedInt != nil {
  println("No you're never gonna get it")
  println(invalidParsedInt!)
}
if validParsedInt != nil {
  println("How are you handsome?")
  println(validParsedInt!)
}

Wachaut: Es muy importante que te asegures que contiene some, ya que si intentas usar el valor del optional cuando contiene nil, podrás incurrir en excepciones de memoria.

Time to break stuff: Intenta desempaquetar el optional invalidParsedInt fuera del scope del if, ¿Qué sucede?

Optional Binding.

Un patrón útil con los optionals es el binding, que consiste en un chequeo y extracción automática en caso de que contenga some.

Volvamos  al ejemplo de los integers parseados.

if let parsedInt = validParsedInt {
  println("Ahora puedes usar el valor de parsedInt: \(parsedInt)")
}

if let parsedInt = invalidParsedInt {
  println("Esta línea no verá la luz del sol: \(parsedInt)")
} else {
  println("No se pudo convertir el número")
}

Con eso realizas el chequeo de some y la extracción del valor, que se hacen en un solo paso.

Opcionales implícitamente extraídos.

Existen casos donde a pesar de que la API te devuelve un optional, uno puede estar seguro que ese optional contiene some, como en el caso de la inicialización de miembros de una clase, que comienzan con un valor inicial, pero en determinado punto pueden volverse nil. Para esos casos podemos usar un opcional explícitamente extraído, que es un opcional que se extrae automáticamente, sin embargo no debes descuidar que si llega a ser nil en el momento de usarlo, te lanzará al infierno de las excepciones en la ejecución.

class Person {
  var name = ""
  var telephoneNumber : String? = nil
  var nacionalidad : String! = "Mexicano"
  func description() {
    println("Nombre :" + self.name)
    if let telephoneNumber = self.telephoneNumber {
      println("Número telefónico: \(telephoneNumber)")
    } else {
      println("\(self.name) valora su privacidad")
    }
    if nacionalidad != nil {
      println("\(self.name) es \(self.nacionalidad)")
    } else {
      println("\(self.name) no tiene nacionalidad")
    }
  }
}

var chucho = Person()
chucho.name = "Chucho"
chucho.telephoneNumber = "3133731337"
chucho.nacionalidad
chucho.description()

var edwardSnowden = Person()
edwardSnowden.name = "Edward"
edwardSnowden.telephoneNumber = nil
edwardSnowden.nacionalidad = nil
edwardSnowden.description()

Aunque normalmente las personas tienen nacionalidad, el pobre Edward fue exiliado, entonces no tiene nacionalidad, lo lógico sería tratar al miembro nacionalidad como una variable no opcional, ya que en la mayoría de los casos sí se tiene, por eso se hace un opcional explícitamente extraído, pero aún así, se tiene que hacer el chequeo, debido a que si intentamos acceder al valor cuando no contiene some sino nil, el programa nos arrojará una excepción del tiempo de ejecución. Not good…

OK, eso es to.. eso es to.. eso es todo amigos.

Si tienen alguna duda o algún tema que les gustaría tratar, no duden en hacérmelo saber.