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
.
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.
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
Na seção Relationships adicione um relacionamento com o Name: type
e o Type: To Many
.
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
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.
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.
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 NSManagedObjectModel
para 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
.
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
.
Ainda no storyboard adicione 7 UILabel
e 1 UIImageView
ao DetailViewController
conforme imagem a seguir. Adicione as constraints necessárias.
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.
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.