Criando uma Pokedex com Core Data

Criando uma Pokedex com Core Data

A persistência de dados no aparelho é uma necessidade comum em vários aplicativos. Muitas vezes precisamos salvar informações que precisam ser acessadas sem conexão com a internet. Podem ser informações inseridas pelo usuário ou informações referentes ao aplicativo. Para informações mais simples como preferências do usuário podemos utilizar a classe NSUserDefaults como visto neste artigo. Mas para informações mais complexas o Core Data é mais adequado.

O que é o Core Data

Um erro muito comum é achar que o Core Data é um banco de dados. O Core Data não é um banco de dados. Ele é uma framework que pode fazer persistência de objetos. Estes objetos podem ser salvos no disco em arquivos binários ou no SQLite. O core data é o model do MVC.

A biblioteca do Core Data possui diversas classes mas entre elas podemos destacar três como principais. São elas:

  • NSManagedObjectModel
  • NSPersistentStoreCoordinator
  • NSManagedObjectContext

Managed Object Model

A classe NSManagedObjectModel é a responsável pelo model da aplicação. Durante a inicialização ele carrega os arquivos com os modelos definidos pelo desenvolvedor no Xcode.
Apesar do Core Data não ser um banco de dados podemos fazer um paralelo do model com o esquema do banco de dados onde são definidos as entidades, os seus campos e os seus relacionamentos.

Persistent Store Coordinator

A classe NSPersistentStoreCoordinator como o nome diz, é a responsável por persistir o modelo no disco. Ela também é a responsável por garantir a compatibilidade entre o arquivo de persistência e o modelo de dados. Outra de sua atribuição é carregar e gerenciar o cache das informações com o(s) managed object context(s).

Managed Object Contexts

A classe NSManagedObjectContext gerencia uma coleção de modelos representados pelo objeto da classe NSManagedObject. Uma aplicativo pode ter vários NSManagedObjectContext, cada um relacionado a um persistent store coordinator.

É importante salientar que enquanto o manage object model e o persistent store coordinator são thread safe o NSManagedObjectContext não o é.
Para trabalhar com várias threads é necessário que seja utilizado um NSManagedObjectContext por thread.

Criando o projeto Pokedex

Crie um novo projeto no Xcode com o template Single View Application. No Product Name digite Pokedex. Selecione Swift como Language e iPhone como Devices. Marque a opção Use Core Data.

Create new project Pokedex

Como selecionamos a opção “Use Core Data” ao criarmos o projeto, o Xcode criará o código necessário para utilizarmos o Core Data automaticamente. Vamos ao código adicionado ao AppDelegate.swift.

A primeira alteração em relação ao código padrão é a adição do framework Core Data ao fonte.

import CoreData

Depois podemos notar 4 propriedades e uma função:

  • applicationDocumentsDirectory do tipo NSURL
  • managedObjectModel do tipo NSManagedObjectModel
  • managedObjectContext do tipo NSManagedObjectContext
  • persistentStoreCoordinator do tipo NSPersistentStoreCoordinator
  • saveContext()

A propriedade applicationDocumentsDirectory é uma propriedade auxiliar para retornar o caminho do diretório Document do aplicativo.

lazy var applicationDocumentsDirectory: NSURL = {
  let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)

  return urls[urls.count-1]
}()

As outras três propriedades são as relacionadas às classes do Core Data. Primeiro temos o managed object model. Ele é uma propriedade lazy, ou seja, será criada apenas quando for acessada.

O que ela faz é carregar o nosso modelo de dados definido no arquivo Pokedex.momd. Este arquivo é gerado pelo Xcode com base na compilação do arquivo Pokedex.xcdatamodeld que foi adicionado ao projeto. Veremos o mesmo posteriormente quando criarmos o nosso modelo de dados.

O managed object model suporta versionamento e migração de dados entre versões do modelo. Também consegue carregar diversos arquivos .momd.

lazy var managedObjectModel: NSManagedObjectModel = {
  let modelURL = NSBundle.mainBundle().URLForResource("Pokedex", withExtension: "momd")!

  return NSManagedObjectModel(contentsOfURL: modelURL)!
 }()

A propriedade persistentStoreCoordinator retorna um objeto da classe NSPersistentStoreCoordinator. A classe recebe como parâmetro um objeto NSManagedObjectModel.

O método addPersistentStoreWithType(_:configuration:URL:options:) da classe NSPersistentStoreCoordinator adiciona a persistência de dados. No nosso caso iremos utilizar o SQLite para persistência dos dados e para isto precisamos criar a variável url com o caminho onde ficará salvo o arquivo .sqlite.

Passamos como primeiro parâmetro o tipo NSSQLiteStoreType. O Core Data também suporta os tipos binário (NSBinaryStoreType) e em memória (NSInMemoryStoreType).

Uma vez que o método addPersistentStoreWithType(_:configuration:URL:options:) pode gerar exceção devemos utilizá-lo em um bloco do-catch.

 lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
  let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
  let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("Pokedex.sqlite")

  do {
    try coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil)
  } catch {
    var dict = [String: AnyObject]()
    dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data"
    dict[NSLocalizedFailureReasonErrorKey] = "There was an error creating or loading the application's saved data."

    dict[NSUnderlyingErrorKey] = error as NSError
    let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)

    NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
    abort()
  }

  return coordinator
}()

A propriedade managedObjectContext retorna um objeto da classe NSManagedObjectContext. A classe recebe como parâmetro .MainQueueConcurrencyType o que significa que o objeto será executado na thread Main. Consequentemente todas as operações realizadas com este objeto deverão estar na thread Main.

lazy var managedObjectContext: NSManagedObjectContext = {
   let coordinator = self.persistentStoreCoordinator

   var managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
   managedObjectContext.persistentStoreCoordinator = coordinator

   return managedObjectContext
 }()

Por último temos um método auxiliar para persistir os dados no disco. Este método verifica se existe alteração nos dados com a propriedade hasChanges da classe NSManagedObjectContext. Caso haja alteração chamamos o método save() dentro de um bloco do-catch.

As alterações serão persistidas no arquivo pelo NSPersistentStoreCoordinator.

// MARK: - Core Data Saving support
func saveContext () {
  if managedObjectContext.hasChanges {
    do {
      try managedObjectContext.save()
    } catch {
      let nserror = error as NSError
      NSLog("Unresolved error \(nserror), \(nserror.userInfo)")

      abort()
    }
  }
}

Definindo o modelo de dados

Vamos definir o nosso modelo de dados no Xcode. Para isto precisamos editar o arquivo Pokedex.xcdatamodeld.

CLique no botão Add Entity na parte de baixo do editor e altere o Name para Pokemon.

Caso a propriedade na apareça mude para a aba Data Model inspector com o comando Alt + command + 3.

Create Pokemon Entity

Na parte central da tela clique no botão + na seção Attributes e cadastre os quatro atributos abaixo:

  • Name: id, Attribute Type: Integer 64, Optional: desmarcado
  • Name: name, Attribute Type: String, Optional: desmarcado
  • Name: height, Attribute Type: Integer 64, Optional: desmarcado
  • Name: weight, Attribute Type: Integer 64, Optional: desmarcado

Pokemon entity attributes

Na seção Relationships adicione um relacionamento com o Name: type e o Type: To Many.

Pokemon entity relationships

Vamos repetir o processo com o Entity PokemonType. Adcione os atributos abaixo:

  • Name: id, Attribute Type: Integer 64, Optional: desmarcado
  • Name: name, Attribute Type: String, Optional: desmarcado

Adicione um relacionamento com as informações abaixo:

  • Name: pokemon
  • Destination: Pokemon
  • Inverse: type
  • Type: To Many

PokemonType entity relationships

Após configurarmos o relacionamento no entity PokemonType o relacionamento no entity Pokemon irá atualizar automaticamente as propriedades Destination e Inverse.

No canto inferior direito no botão Editor Style podemos ver o nosso model graficamente.

Pokedex Model

Note os atributos que cadastramos e o relacionamento N pra N entre as duas entidades.

Criando as classes NSManagedObject

Para facilitar o uso do Core Data e evitar erros de digitação nos nomes das propriedades, iremos criar classes que herdem da classe NSManagedObject para cada entity definida. O Xcode já possui um assistente que realiza esta tarefa.

Com o Pokedex.xcdatamodeld selecionado acesse o menu Editor -> Create NSManagedObject Subclass…. Selecione o DataModel Pokemon e clique em Next. Selecione os dois Entity e clique em Next.

Sera aberta uma tela para salvar os dados. Certifique-se o Language esteja como Swift. Marque o Options Use Scalar properties for primitive data types. Group como Pokedex (a pasta amarela e não o projeto). E que o Targets esteja com o projeto Pokemon selecionado.

Save NSManagedObject Subclass

Serão criados quatro arquivos:

  • Pokemon.swift

    Alterei a classe Pokemon acrescentando dois métodos que irei utilizar para incluir os tipos dos pokemons.

    import CoreData
    import Foundation
    
    class Pokemon: NSManagedObject {
      // Insert code here to add functionality to your managed object subclass
    }
    
    extension Pokemon {
      func addPokemonTypeObject(value: PokemonType) {
        let items = self.mutableSetValueForKey("type");
        items.addObject(value)
      }
    
      func removePokemonTypeObject(value: PokemonType) {
        let items = self.mutableSetValueForKey("type");
        items.removeObject(value)
      }
    }
  • PokemonType.Swift

    import CoreData
    import Foundation
    
    class PokemonType: NSManagedObject {
      // Insert code here to add functionality to your managed object subclass
    }
  • Pokemon+CoreDataProperties.swift

    import CoreData
    import Foundation
    
    extension Pokemon {
      @NSManaged var id: Int64
      @NSManaged var name: String
      @NSManaged var height: Int64
      @NSManaged var weight: Int64
      @NSManaged var type: NSSet?
    }
  • PokemonType+CoreDataProperties.swift

    import CoreData
    import Foundation
    
    extension PokemonType {
      @NSManaged var id: Int64
      @NSManaged var name: String
      @NSManaged var pokemon: NSSet?
    }

Note que pra cada entity foram criados dois arquivos. Caso você altere o modelo após criar as classes, o Xcode irá sobrescrever apenas os arquivos com +CoreDataProperties no nome. Todas as alterações que forem feitas na classe devem ser feitas nos fontes com os nomes dos entity para que o Xcode não sobreescreva os seu código.

Retire os opcionais das propriedades String.

Obtendo informações da Pokedex via PokeAPI

Iremos popular a nossa Pokedex com as informações obtidas no site pokeapi.co. Ele possui muitas informações sobre os pokemons mas iremos utilizar apenas o id, name, height, weight e type. Depois você pode complementar o aplicativo para obter outras informações como golpes, evoluções, atributos básicos, etc.

Para obter os dados dos pokemons irei criar uma novo arquivo PokemonData.swift. Acesse o menu File -> New -> File e na aba iOS | Source escolha Swift File.

Primeiramente irei criar as structs com as informações necessárias para a Pokedex.

Implementei o protocolo CustomStringConvertible para visualizarmos o log dos dados.

import Foundation

struct PokemonData: CustomStringConvertible {
  // MARK: - Properties
  var id: Int?
  var name: String?
  var height: Int?
  var weight: Int?
  var type = [TypeData]()

  var description: String {
    return "\(name!.capitalizedString)\nId:\(id!)\nHeight:\(height!)\nWeight:\(weight!)\nType: \(getTypes())\n"
  }

  // MARK: Public Methods

  // MARK: Private Methods
  private func getTypes() -> String {
    var result = ""

    result = type.flatMap { $0.name.capitalizedString }.joinWithSeparator(", ")

    return result
  }
}

struct TypeData: CustomStringConvertible {
  var id: Int
  var name: String

  var description: String {
    return "\(id)-\(name.capitalizedString)\n"
  }
}

Agora iremos criar os métodos e propriedades necessárias para carregar os dados do pokeapi.co.

Em primeiro lugar irei criar um novo arquivo Utils.swift para adicionar algumas funções que iremos utilizar.

No menu File -> New -> File na aba iOS | Source selecione Swift File.

Adicione o código abaixo ao fonte.

import UIKit
import Foundation

extension UIViewController
{
  var contentViewController: UIViewController
  {
    if let navcon = self as? UINavigationController
    {
      return navcon.visibleViewController!
    }
    else
    {
      return self
    }
  }
}

func delay(bySeconds seconds: Double, dispatchLevel: DispatchLevel = .Main, closure: () -> Void) {
  let dispatchTime = dispatch_time(DISPATCH_TIME_NOW, Int64(seconds * Double(NSEC_PER_SEC)))
  dispatch_after(dispatchTime, dispatchLevel.dispatchQueue, closure)
}

enum DispatchLevel {
  case Main, UserInteractive, UserInitiated, Utility, Background
  var dispatchQueue: OS_dispatch_queue {
    switch self {
    case .Main:             return dispatch_get_main_queue()
    case .UserInteractive:  return dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0)
    case .UserInitiated:    return dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)
    case .Utility:          return dispatch_get_global_queue(QOS_CLASS_UTILITY, 0)
    case .Background:       return dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0) }
  }
}

A primeira propriedade que adicionamos ao UIViewController serve para acessarmos o viewController quando ele está embutido em um UINavigationController.

A função delay(bySeconds:dispatchLevel:closure:) serve para executarmos um código depois de um certo tempo. Utilizaremos para não ultrapassarmos a quantidade de requisições por segundo permitidas pela API do pokeapi.co.

No fonte PokemonData.swift adicione o método initialLoad(). Este método será estático e iremos chamá-lo do método application(_:didFinishLaunchingWithOptions:) da classe AppDelegate.

Note que utilizei a função delay(bySeconds:dispatchLevel:closure:) para chamar até 30 pokemons em um intervalo de 30 segundos.

Apesar do pokeapi.co ir até o pokemon 721 e ainda possuir mais 90 pokemons, totalizando 811, irei utilizar apenas 151 pokemons na minha Pokedex. Você pode adicionar os pokemons restantes posteriormente.

static func initialLoad() {
  for id in 1...30 {
    var pokemon = PokemonData()
    pokemon.getPokemon(id)
  }

  delay(bySeconds: 30, dispatchLevel: .Background) {
    for id in 31...60 {
      var pokemon = PokemonData()
      pokemon.getPokemon(id)
    }
  }

  delay(bySeconds: 60, dispatchLevel: .Background) {
    for id in 61...90 {
      var pokemon = PokemonData()
      pokemon.getPokemon(id)
    }
  }

  delay(bySeconds: 90, dispatchLevel: .Background) {
    for id in 91...120 {
      var pokemon = PokemonData()
      pokemon.getPokemon(id)
    }
  }

  delay(bySeconds: 120, dispatchLevel: .Background) {
    for id in 121...151 {
      var pokemon = PokemonData()
      pokemon.getPokemon(id)
    }
  }
}

Altere o AppDelegate.swift.

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
  PokemonData.initialLoad()

  return true
}

O método getPokemon(_:) acessa a pokeapi.co utilizando a classe NSURLSession. Pegamos a sessão com o métodosharedSession() e depois criamos uma tarefa com o método dataTaskWithURL(_:completionHandler:). O parâmetro completitionHandler é uma closure no formato (NSData?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionDataTask. Como os parâmetros são opcionais, verificamos se a resposta existe e é com o código 200. Em seguida verificamos se os dados existem e podem ser serializados como JSON. Caso estas validações sejam atendidas chamamos o método parseDictionary(_:) para obtermos os dados do pokemon.

A tarefa do tipo NSURLSessionDataTask deve ser executada com o método resume().

  mutating func getPokemon(id: Int) {
  let session = NSURLSession.sharedSession()
  let url = NSURL(string: "https://pokeapi.co/api/v2/pokemon/\(id)/")!

  dataTask?.cancel()

  dataTask = session.dataTaskWithURL(url)
  {
    data, response, error in
    if let error = error where error.code == -999
    {
      return
    }

    if let httpResponse = response as? NSHTTPURLResponse where httpResponse.statusCode == 200,
      let data = data,
      let dictionary = self.parseJSON(data)
    {
      self.parseDictionary(dictionary)
    }
  }

  dataTask?.resume()
}

O método parseDictionary(_:) recebe um NSData e retorna um opcional [String: AnyObject]. Ele utiliza o método JSONObjectWithData(_:options:) da classe NSJSONSerialization para serializar o retorno da API. Como o método gera exceção utilizamos o bloco do-catch.

private func parseJSON(data: NSData) -> [String: AnyObject]?
{
  do
  {
    return try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject]
  } catch {
    return nil
  }
}

A API retornará a seguinte estrutura.

{
  ...
  "name": "bulbasaur",
  "weight": 69,
  ...
  "height": 7,
  ...
  "id": 1,
  ...
  "types": [
    {
      "slot": 2,
      "type": {
        "url": "https://pokeapi.co/api/v2/type/4/",
        "name": "poison"
      }
    },
    {
      "slot": 1,
      "type": {
        "url": "https://pokeapi.co/api/v2/type/12/",
        "name": "grass"
      }
    }
  ]
}

No método parseDictionary(_:) obtemos os dados dos campos id, name height, weight e os atribuímos ao objeto da classe PokemonData. O campo types possuí um array e passamos este array para o método getTypes(_:).

No final logamos o pokemon com o comando print.

private mutating func parseDictionary(dictionary: [String: AnyObject]) {
  guard let id = dictionary["id"] as? Int else { return }
  guard let name = dictionary["name"] as? String else { return }
  guard let height = dictionary["height"] as? Int else { return }
  guard let weight = dictionary["weight"] as? Int else { return }
  guard let types = dictionary["types"] as? [[String: AnyObject]] else { return }

  self.id = id
  self.name = name
  self.height = height
  self.weight = weight

  getTypes(types)

  print(self)
}

No método getTypes(_:) obtemos as informações do campo types transformando-o no objeto TypeData. Depois o acrescentamos ao array type do objeto da classe PokemonData.

private mutating func getTypes(types: [[String: AnyObject]]) {
  for slot in types {
    let type = slot["type"] as! [String: AnyObject]
    let name = type["name"] as! String
    let url = type["url"] as! String
    var id = (url as NSString).substringFromIndex(url.characters.count - 3)
    id = id.stringByReplacingOccurrencesOfString("/", withString: "")

    let typeData = TypeData(id: Int(id)!, name: name)

    self.type.append(typeData)
  }
}

Execute o projeto e será lançado um log como abaixo:

Starmie
Id:121
Height:11
Weight:800
Type: Psychic, Water

Scyther
Id:123
Height:15
Weight:560
Type: Flying, Bug

Gyarados
Id:130
Height:65
Weight:2350
Type: Flying, Water

Ditto
Id:132
Height:3
Weight:40
Type: Normal

...

Carregando os dados do Core Data

Renomeie o arquivo ViewController.swift para MainViewController.swift e a classe ViewController para MainViewController.

A primeira tarefa será passar o NSManagedObjectModelpara o MainViewController. Altere o método application(_:didFinishLaunchingWithOptions:) da classe AppDelegate.

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
  PokemonData.initialLoad()

  let mainViewController = window?.rootViewController?.contentViewController as! MainViewController
  mainViewController.managedObjectContext = managedObjectContext

  return true
}

Adicione a propriedade managedObjectContext à classe MainViewController. Para usar o Core Data precisaremos adicionar o import da biblioteca ao fonte.

Crie um outlet para o UITableView, ligue com o componente do storyboard e defina o MainViewController como o seu dataSource e delegate. No método viewDidLoad() defina o title como Pokedex.

Por último chamaremos o método loadPokemon() que iremos criar em seguida para carregar os pokemons do Core Data.

Iremos utilizar a classe NSFetchedResultsController para popular a tableView. Esta classe facilita em muito a integração do Core Data com o UITableView. Crie uma nova propriedade fetchedResultsController. O fonte ficará:

import UIKit
import CoreData

class MainViewController: UIViewController {
  @IBOutlet weak var tableView: UITableView!

  private struct Constants {
    static let entityName = "Pokemon"
    static let cellName = "PokemonCell"
    static let showDetailSegue = "showDetailSegue"
  }

  var fetchedResultsController: NSFetchedResultsController!

  weak var managedObjectContext: NSManagedObjectContext!

  override func viewDidLoad() {
    super.viewDidLoad()

    title = "Pokedex"

    loadPokemon()
  }
}

Para recuperarmos os dados do Core Data utilizamos a classe NSFetchRequest. A classe recebe o nome do entity/ que definimos no nosso modelo. Depois utilizamos o método executeFetchRequest(_:) da classe NSManagedObjectContext para retornar um array da classe NSManagedObject. Como o método gera exceção devemos utilizar um bloco do-catch.

let fetchRequest = NSFetchRequest(entityName: Constants.entityName)

do {
  let pokemons = try managedObjectContext.executeFetchRequest(fetchRequest) as! [Pokemon]
} catch {
  print(error)
}

Os dados consultados no NSFetchRequest podem ser ordenados e filtrados. Para isto utilizamos as classes NSSortDescriptor e NSPredicate respectivamente.

let fetchRequest = NSFetchRequest(entityName: Constants.entityName)

let sortDescriptor = NSSortDescriptor(key: "id", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]

let predicate = NSPredicate(format: "type.name == %@", "poison")
fetchRequest.predicate = predicate

do {
  let pokemons = try managedObjectContext.executeFetchRequest(fetchRequest) as! [Pokemon]
} catch {
  print(error)
}

Como listaremos os nossos pokemons em um UITableView vamos utilizar a classe NSFetchedResultsController para realizar a integração do Core Data com o tableView. O NSFetchedResultsController utiliza o NSFetchRequest para executar a consulta e possui métodos que utilizaremos para implementar o datasource do tableView. Também temos o protocolo NSFetchedResultsControllerDelegate onde gerenciamos as alterações realizadas nos dados do tableView.

É obrigatório informar ao menos um NSSortDescriptor no NSFetchRequest quando utilizarmos o NSFetchedResultsController.

private func loadPokemon() {
  // Initialize Fetch Request
  let fetchRequest = NSFetchRequest(entityName: Constants.entityName)

  // Add Sort Descriptors
  let sortDescriptor = NSSortDescriptor(key: "id", ascending: true)
  fetchRequest.sortDescriptors = [sortDescriptor]

  // Initialize Fetched Results Controller
  fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)

  // Configure Fetched Results Controller
  fetchedResultsController?.delegate = self

  // Execute the fetch request
  do {
    try fetchedResultsController?.performFetch()
  } catch {
    print(error)
  }

  // Reload tableView
  tableView?.reloadData()
}

Popular o tableView com o NSFetchedResultsController é bem simples. Utilizamos a propriedade sections do NSFetchedResultsController para obtermos as seções do tableView. Com a propriedade count da coleção temos a quantidade de seções. Informando o índice da seção obtemos uma seção específica. Com a seção utilizamos a propriedade numberOfObjects para obtermos a quantidade de objetos da seção. Como índice utilizamos o objeto NSIndexPath fornecido pelos método do UITableView.

Para obter um objeto específico temos o método objectAtIndexPath(_:) que recebe o NSIndexPath e retorna um AnyObject.

extension MainViewController: UITableViewDataSource {
  func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return fetchedResultsController?.sections?.count ?? 0
  }

  func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if let sections = fetchedResultsController?.sections {
      let sectionInfo = sections[section]

      return sectionInfo.numberOfObjects
    }

    return 0
  }

  func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier(Constants.cellName, forIndexPath: indexPath)

    configureCell(cell, indexPath: indexPath)

    return cell
  }

  func configureCell(cell: UITableViewCell, indexPath: NSIndexPath) {
    let pokemon = fetchedResultsController!.objectAtIndexPath(indexPath) as! Pokemon

    cell.textLabel?.text = pokemon.name.capitalizedString
    cell.detailTextLabel?.attributedText = pokemon.getTypes()

    if let image = UIImage(named: pokemon.name) {
      cell.imageView?.image = image
    } else {
      cell.imageView?.image = UIImage(named: "pokeball")!
    }
  }
}

Vamos implementar 4 métodos do protocolo NSFetchedResultsControllerDelegate. O método controllerWillChangeContent(_:) e controllerDidChangeContent(_:) servem para configurar o tableView para o modo de edição. Assim podemos excluir e inserir novas linhas sem precisar recarregar o mesmo.

O método controller(_:didChangeSection:atIndex:forChangeType:) serve para tratarmos as alterações realizadas nas seções do tableView. No nosso caso temos uma única seção com todos os pokemons. O método recebe o controller, a seção, o índice da seção e o tipo de alteração. Utilizamos estes parâmetros para inserir e excluir as seções no tableView.

O método controller(_:didChangeObject:atIndexPath:forChangeType:newIndexPath:) funciona de maneira similar. Utilizamos os parâmetros para inserir, excluir, editar e mover os pokemons da nossa tableView para refletir as alterações do Core Data.

extension MainViewController: NSFetchedResultsControllerDelegate {
  func controllerWillChangeContent(controller: NSFetchedResultsController) {
    tableView.beginUpdates()
  }

  func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
    switch type {
      case .Insert:
        tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
      case .Delete:
        tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
      case .Move:
        break
      case .Update:
        break
    }
  }

  func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
    switch type {
      case .Insert:
        tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
      case .Delete:
        tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
      case .Update:
        configureCell(self.tableView.cellForRowAtIndexPath(indexPath!)!, indexPath: indexPath!)
      case .Move:
        tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
        tableView.insertRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
    }
  }

  func controllerDidChangeContent(controller: NSFetchedResultsController) {
    tableView.endUpdates()
  }
}

Inserindo os Pokemons no Core Data

Para inserir um novo registro no Core Data utilizamos o método insertNewObjectForEntityForName(_:inManagedObjectContext:) da classe NSEntityDescription. Ele possui dois parâmetros. O primeiro parâmetro é o nome do entity definido no model e o segundo parâmetro é o NSManagedObjectContext.

Para facilitar o uso e reduzir a chance de erro de digitação vamos adicionar novos métodos às nossas classes NSManagedObject que retornarão novos objetos. Incluiremos também métodos para pesquisar se determinado objeto já existe no Core Data.

import UIKit
import CoreData
import Foundation

class Pokemon: NSManagedObject {
  func getTypes() -> NSAttributedString {
    let result = NSMutableAttributedString()

    if let types = type {
      for t in types {
        let color = (t as! PokemonType).typeColor

        let typeAttributed = NSAttributedString(string: t.name.capitalizedString, attributes: [ NSBackgroundColorAttributeName: color, NSForegroundColorAttributeName: UIColor.whiteColor() ])
        result.appendAttributedString(typeAttributed)

        result.appendAttributedString(NSAttributedString(string: " "))
      }
    }

    return result
  }
}

extension Pokemon {
  class func getPokemon(managedObjectContext: NSManagedObjectContext) -> Pokemon
  {
    return NSEntityDescription.insertNewObjectForEntityForName("Pokemon", inManagedObjectContext: managedObjectContext) as! Pokemon
  }

  class func exist(managedObjectContext: NSManagedObjectContext, identifier: Int) -> Pokemon? {
    // Initialize Fetch Request
    let entity = NSEntityDescription.entityForName("Pokemon", inManagedObjectContext: managedObjectContext)
    let fetchRequest = NSFetchRequest()
    fetchRequest.entity = entity

    // Add filter
    let predicate = NSPredicate(format: "id = %@", String(identifier))
    fetchRequest.predicate = predicate

    do {
      let pokemons = try managedObjectContext.executeFetchRequest(fetchRequest) as? [Pokemon]

      return pokemons?.first
    } catch {
      let _ = error as NSError
    }

    return nil
  }
}

Inseri o método getTypes() para retornar um NSAttributedString com o nome do tipo do pokemon. Isto irá destacar os diferentes tipos.

Para pesquisar se um pokemon já existe criei o método exist(_:identifier:) que recebe um NSManagedObjectContext e um Int com o id do pokemon. O método faz uma consulta com o NSFetchRequest filtrando o id do pokemon e retorna nil caso ele não exista.

import UIKit
import CoreData
import Foundation

class PokemonType: NSManagedObject {
  var typeColor: UIColor {
    switch id {
    case 1: return UIColor(red: 168.0/255.0, green: 168.0/255.0, blue: 120.0/255.0, alpha: 1)
    case 2: return UIColor(red: 192.0/255.0, green: 48.0/255.0, blue: 40.0/255.0, alpha: 1)
    case 3: return UIColor(red: 168.0/255.0, green: 144.0/255.0, blue: 240.0/255.0, alpha: 1)
    case 4: return UIColor(red: 160.0/255.0, green: 64.0/255.0, blue: 160.0/255.0, alpha: 1)
    case 5: return UIColor(red: 224.0/255.0, green: 192.0/255.0, blue: 104.0/255.0, alpha: 1)
    case 6: return UIColor(red: 184.0/255.0, green: 160.0/255.0, blue: 56.0/255.0, alpha: 1)
    case 7: return UIColor(red: 168.0/255.0, green: 184.0/255.0, blue: 32.0/255.0, alpha: 1)
    case 8: return UIColor(red: 112.0/255.0, green: 88.0/255.0, blue: 152.0/255.0, alpha: 1)
    case 9: return UIColor(red: 184.0/255.0, green: 184.0/255.0, blue: 208.0/255.0, alpha: 1)
    case 10: return UIColor(red: 240.0/255.0, green: 128.0/255.0, blue: 48.0/255.0, alpha: 1)
    case 11: return UIColor(red: 104.0/255.0, green: 144.0/255.0, blue: 240.0/255.0, alpha: 1)
    case 12: return UIColor(red: 120.0/255.0, green: 200.0/255.0, blue: 80.0/255.0, alpha: 1)
    case 13: return UIColor(red: 248.0/255.0, green: 208.0/255.0, blue: 48.0/255.0, alpha: 1)
    case 14: return UIColor(red: 248.0/255.0, green: 88.0/255.0, blue: 136.0/255.0, alpha: 1)
    case 15: return UIColor(red: 152.0/255.0, green: 216.0/255.0, blue: 216.0/255.0, alpha: 1)
    case 16: return UIColor(red: 112.0/255.0, green: 56.0/255.0, blue: 248.0/255.0, alpha: 1)
    case 17: return UIColor(red: 112.0/255.0, green: 88.0/255.0, blue: 72.0/255.0, alpha: 1)
    case 18: return UIColor(red: 238.0/255.0, green: 153.0/255.0, blue: 172.0/255.0, alpha: 1)
    case 10001: return UIColor(red: 104.0/255.0, green: 160.0/255.0, blue: 144.0/255.0, alpha: 1)
    default: return UIColor(red: 255.0/255.0, green: 255.0/255.0, blue: 255.0/255.0, alpha: 1)
    }
  }
}

extension PokemonType {
  class func getType(managedObjectContext: NSManagedObjectContext) -> PokemonType
  {
    return NSEntityDescription.insertNewObjectForEntityForName("PokemonType", inManagedObjectContext: managedObjectContext) as! PokemonType
  }

  class func exist(managedObjectContext: NSManagedObjectContext, identifier: Int) -> PokemonType? {
    // Initialize Fetch Request
    let entity = NSEntityDescription.entityForName("PokemonType", inManagedObjectContext: managedObjectContext)
    let fetchRequest = NSFetchRequest()
    fetchRequest.entity = entity

    // Add filter
    let predicate = NSPredicate(format: "id = %@", String(identifier))
    fetchRequest.predicate = predicate

    do {
      let types = try managedObjectContext.executeFetchRequest(fetchRequest) as? [PokemonType]

      return types?.first
    } catch {
      let _ = error as NSError
    }

    return nil
  }
}

Na classe PokemonType criei uma propriedade para retornar uma cor diferente pra cada tipo existente.

O método exist(_:identifier:) funciona de forma semelhante a classe Pokemon.

No arquivo PokemonData.swift iremos alterar a struct PokemonData para inserir um novo registro no Core Data depois de obter os dados pokeapi.co.

Vamos começar importando a biblioteca Core Data.

import CoreData

Depois vamos adicionar uma propriedade managedObjectContext.

var managedObjectContext: NSManagedObjectContext!

Vamos criar um initializer que recebe o NSManagedObject e o atribui a propriedade criada anteriormente.

Como vimos anteriormente a classe NSManagedObjectContext não é thread safe então iremos criar um novo objeto para rodar em uma thread em background e atribuir o managedObjectContext criado na thread Main à propriedade parentContext do nosso managedObjectContext.

init(parentManagedObjectContext: NSManagedObjectContext) {
  managedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
  managedObjectContext.parentContext = parentManagedObjectContext
}

O próximo método a ser alterado é o initialLoad(). Iremos receber um NSManagedObjectContext como parâmetro para passar para o Initialize da classe. Também iremos executar todo o método em background para não influenciar no tempo de abertura do aplicativo. A Apple rejeita aplicativos que bloqueiem a Main thread por mais de 10 segundos porque o usuário achará que o iOS travou.

static func initialLoad(managedObjectContext: NSManagedObjectContext) {
  dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
    for id in 1...30 {
      var pokemon = PokemonData(parentManagedObjectContext: managedObjectContext)
      pokemon.getPokemon(id)
    }

    delay(bySeconds: 30, dispatchLevel: .Background) {
      for id in 31...60 {
        var pokemon = PokemonData(parentManagedObjectContext: managedObjectContext)
        pokemon.getPokemon(id)
      }
    }

    delay(bySeconds: 60, dispatchLevel: .Background) {
      for id in 61...90 {
        var pokemon = PokemonData(parentManagedObjectContext: managedObjectContext)
        pokemon.getPokemon(id)
      }
    }

    delay(bySeconds: 90, dispatchLevel: .Background) {
      for id in 91...120 {
        var pokemon = PokemonData(parentManagedObjectContext: managedObjectContext)
        pokemon.getPokemon(id)
      }
    }

    delay(bySeconds: 120, dispatchLevel: .Background) {
      for id in 121...151 {
        var pokemon = PokemonData(parentManagedObjectContext: managedObjectContext)
        pokemon.getPokemon(id)
      }
    }
  }
}

No método getPokemon(_:) iremos consultar se o pokemon já existe no Core Data antes de consultá-lo via API. Com isto não fazemos requisições desnecessárias.

Após realizar o parse do retorno da API chamamos o método makePokemon().

mutating func getPokemon(id: Int) {
  if let _ = Pokemon.exist(managedObjectContext, identifier: id) {
    return
  }

  let session = NSURLSession.sharedSession()
  let url = NSURL(string: "https://pokeapi.co/api/v2/pokemon/\(id)/")!

  dataTask?.cancel()

  dataTask = session.dataTaskWithURL(url)
  {
    data, response, error in
    if let error = error where error.code == -999
    {
      return
    }

    if let httpResponse = response as? NSHTTPURLResponse where httpResponse.statusCode == 200,
      let data = data,
      let dictionary = self.parseJSON(data)
    {
      self.parseDictionary(dictionary)

      self.makePokemon()
    }
  }

  dataTask?.resume()
}

O método makePokemon() irá criar um novo objeto da classe Pokemon e preencher os dados de acordo com as propriedades do PokemonData. Em seguida iremos consultar se os tipos já existem no Core Data e caso não existam iremos criá-los. No final salvamos os dados com o método save() do NSManagedObjectContext dentro do bloco do-catch.

private func makePokemon() {
  let pokemonObject = Pokemon.getPokemon(managedObjectContext)

  pokemonObject.id = Int64(self.id!)
  pokemonObject.name = self.name!
  pokemonObject.height = Int64(self.height!)
  pokemonObject.weight = Int64(self.weight!)

  for t in type {
    if let x = PokemonType.exist(managedObjectContext, identifier: t.id) {
      pokemonObject.addPokemonTypeObject(x)
    } else {
      let x = PokemonType.getType(managedObjectContext)
      x.id = Int64(t.id)
      x.name = t.name

      pokemonObject.addPokemonTypeObject(x)
    }
  }

  if managedObjectContext.hasChanges {
    do {
      try managedObjectContext.save()
    } catch {
      print(error)
    }
  }
}

Altere a chamada do método initialLoad(_:) no AppDelegate.

PokemonData.initialLoad(managedObjectContext)

Incluindo os assets da Pokedex

A nossa Pokedex precisará das imagens dos pokemons. Baixei as imagens da internet e converti para o formato pdf. As imagens dos pokemons foram obtidas no site seeklogo.com. A versão pdf dos arquivos pode ser obtida aqui. Arraste os arquivos pdf para o Assets.xcassets do projeto.

Altere a propriedade Scale Factors das imagens para SingleVector.

Assets Scale Factors

Renomeie a imagem nidoran-2 para nidoran-f e a imagem nidoran para nidoran-m.

Definindo o layout da Pokedex

Abra o Main.storyboard selecione o viewController e mude a classe para MainViewController. Com o mesmo selecionado vá no menu Editor -> Embed In -> Navigation Controller.

Adicione uma UITableView e depois uma UITableViewCell a tableView. Altere o Style da tableViewCell para Subtitle, defina o Identifier para PokemonCell e adicione as constraints necessárias. Ligue o tableView ao outlet da classe MainViewController.

Crie um novo arquivo menu File -> New -> File. Na aba iOS | Source selecione Cocoa Touch Class. Nomeie a classe como DetailViewController e como Subclass of UIViewController.

import UIKit

class DetailViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()

    // Do any additional setup after loading the view.
  }
}

De volta ao Main.storyboard adicione um UIViewController e altere a sua classe para DetailViewController. Pressione control, clique e arraste o cursor da tableViewCell até o novo viewController. Selecione Accessory Action | Show. Defina o Identifier da segue como showDetailSegue.

Create segue

Segue detail

Ainda no storyboard adicione 7 UILabel e 1 UIImageView ao DetailViewController conforme imagem a seguir. Adicione as constraints necessárias.

Detail Layout

No fonte DetailViewController.swift adicione os outlets abaixo na classe DetailViewController e ligue os mesmos ao storyboard.

@IBOutlet weak var typeLabel: UILabel!
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var idLabel: UILabel!
@IBOutlet weak var heightLabel: UILabel!
@IBOutlet weak var weightLabel: UILabel!

Exibindo os detalhes do Pokemon

No fonte MainViewController.swift devemos implementar o método tableView(_:didSelectRowAtIndexPath:) do protocolo UITableViewDelegate.

extension MainViewController: UITableViewDelegate {
  func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    let pokemon = fetchedResultsController!.objectAtIndexPath(indexPath) as! Pokemon

    performSegueWithIdentifier(Constants.showDetailSegue, sender: pokemon)

    tableView.deselectRowAtIndexPath(indexPath, animated: true)
  }
}

Altere o método prepareForSegue(_:sender:) para enviar o pokemon selecionado para o DetailViewController.

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
  if segue.identifier == Constants.showDetailSegue {
    let detailViewController = segue.destinationViewController.contentViewController as! DetailViewController
    detailViewController.pokemon = sender as! Pokemon
  }
}

No fonte DetailViewController.swift adicionamos a propriedade pokemon que passamos do MainViewController e a propriedade numberFormatter para formatarmos os dados de peso e altura do pokemon.

Criamos o método loadPokemon() onde definimos o título do navigationBar como o nome do pokemon. Depois preenchemos as propriedades dos nosso outlets.

Por último chamamos o método loadPokemon() no método viewDidLoad().

import UIKit

class DetailViewController: UIViewController {
  // MARK: - Outlets
  @IBOutlet weak var typeLabel: UILabel!
  @IBOutlet weak var imageView: UIImageView!
  @IBOutlet weak var idLabel: UILabel!
  @IBOutlet weak var heightLabel: UILabel!
  @IBOutlet weak var weightLabel: UILabel!

  // MARK: - Properties
  weak var pokemon: Pokemon!

  lazy var numberFormatter: NSNumberFormatter = {
    let numberFormatter = NSNumberFormatter()
    numberFormatter.numberStyle = .DecimalStyle
    numberFormatter.maximumFractionDigits = 1
    numberFormatter.minimumFractionDigits = 1

    return numberFormatter
  }()

  // MARK: - Public Methods
  override func viewDidLoad() {
    super.viewDidLoad()

    loadPokemon()
  }

  // MARK: - Private Methods
  private func loadPokemon() {
    title = pokemon.name.capitalizedString

    typeLabel?.attributedText = pokemon.getTypes()
    idLabel?.text = String(pokemon.id)
    heightLabel?.text = "\(numberFormatter.stringFromNumber(NSNumber(double: Double(pokemon.height) / 10.0))!) m"
    weightLabel?.text = "\(numberFormatter.stringFromNumber(NSNumber(double: Double(pokemon.weight) / 10.0))!) Kg"

    if let image = UIImage(named: pokemon.name) {
      imageView?.image = image
    } else {
      imageView?.image = UIImage(named: "pokeball")!
    }
  }
}

Podemos rodar o aplicativo e ver os pokemons aparecerem na Pokedex a medida que eles forem sendo baixados do pokeapi.co.

Pokedex 01 Pokedex 02 Pokedex 03 Pokedex 04 Pokedex 05 Pokedex 06 Pokedex 07

Conclusão

O Core Data é uma ótima forma de trabalharmos com dados no iOS. Ele possui um eficiente gerenciamento de memória com cache e carrega somente os dados necessários no momento. A classe NSFetchedResultsController torna a integração com o UITableView bem prática e com boa performance.

Sugiro uma leitura na documentação oficial do Core Data. Também recomendo uma série do Bart Jacobs no site tutsplus.

Espero que tenha gostado do artigo. Se tiver dúvidas, críticas ou sugestões deixe o seu comentário abaixo. Se gostou compartilhe.

Publicidade

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s