() translation by (you can also view the original English article)
Nos capítulos anteriores de Swift from Scratch, criamos um aplicativo gerenciador de tarefas funcional. Entretanto, o modelo de dados poderia se um pouco mais bem estuturado. Neste tutorial, vamos refactorar nosso modelo de dados implementado uma classe modal personalisada.
1. Modelo de Dados
O modelo de dados que vamos implementar inclui duas classes, uma classe Task
e uma classe ToDo
que herda da classe Task
. Enquanto criamos e implementamos as classes modelo, vamos continuar nossa exploração em programação orientada a objetos em Swift. Neste tutorial, vamos verificar a inicialização de instâncias de classes e qual o papel a herança desempenha durante a inicialização.
Classe Task
Vamos começar com a implementação da classe Task
. Criar um novo arquivo Swift selecionando New > File... no menu File do Xcode. Escolher Swift File na seção iOS > Source. Nomear o arquivo de Task.swift e clicar Create.



A implementação básica é curta e simples. A classe Task
herda de NSObject
, definido no framework Foundation, e tem a propriedade variável name
do tipo String
. A classe define dois inicializadores, init
e init(name:)
. Existem alguns detalhes que podem confundi-los, então deixem-me explicar o que está acontecendo.
1 |
import Foundation |
2 |
|
3 |
class Task: NSObject { |
4 |
var name: String |
5 |
|
6 |
convenience override init() { |
7 |
self.init(name: "New Task") |
8 |
}
|
9 |
|
10 |
init(name: String) { |
11 |
self.name = name |
12 |
}
|
13 |
}
|
Como o método init
também é definido na classe NSObject
, precisamos usar o prefixo override
no inicializador. Trabalhamos com substituição (overriding) de métodos mais cedo nesta serie. No método init
, chamamos o método init(name:)
, passando "New Task"
como valor para o parâmetro name
.
O método init(name:)
é outro inicializador, que aceita um único parâmetro name
do tipo String
. Neste inicializador, o valor do parâmetro name
é atribuído para propriedade name
. Isto é fácil de entender. Certo?
Inicializadores Designados e de Conveniência
O que é aquela palavra-chave convenience
prefixando o método init
? Classes podem ter dois tipos de inicializadores, inicializadores designados e inicializadores de conveniência. Inicializadores de conveniencia são prefixados com a palavra-chave conveniente
, o que indica que init(name:)
é um inicializador designado. O que é isso? Qual é a diferença entre inicializador designado e inicializador de conveniência?
Inicializadores designados inicializam totalmente uma instância de uma classe, significa que todas as propriedades da instância tem um valor após a inicialização. Olhando para a classe Task
, como exemplo, vimos que a propriedade name
é definida com o valor do parâmetro name
do inicializador init(name:)
. O resultado após a inicialização é uma instância Task
totalmente inicializada.
Inicializadores de conveniência, entretanto, conta com um inicializador designado para criar uma instanciada totalmente inicializada de uma classe. É por isso que o inicializador init
da classe Task
chama o inicializador init(name:)
em sua implementação. Isto é referido como delegação de inicializador. O inicializador init
delaga a inicialização para o inicializador designado para criar uma instância da classe Task
totalmente inicializada.
Inicializadores de conveniência são opcionais. Nem todas as classes tem um inicializador de conveniência. Inicializadores designados são obrigatórios e uma classe precisa ter ao menos um inicializador designado para criar uma instância completa de si.
Protocolo NSCoding
A implementação da classe Task
ainda não está completa. Mais a frente neste artigo, vamos escrever um array da instâncias de ToDo
para armazena-lo. Isto é possível apenas se a instância da classe ToDo
puder ser codificada e decodificada.
Mas não se preocupe, isto não é coisa da NSA. Precisamos apenas criar as classes Task
e ToDo
conforme o protocolo NSCoding
. É por isso que a classe Task
herda da classe NSObject
, pois o protocolo NSCoding
só pode ser implementado por classes herdando — diretamente ou indiretamente — do NSObject
. Como a classe NSObject
, o protocolo NSCoding
é definido no framework Foundation.
Adotar um protocolo é algo que nós já discutimos nesta série, mas existem alguns macetes que eu quero ressaltar. Vamos começar informando ao compilador que a classe Task
está em conformidade com o protocolo NSCoding
.
1 |
class Task: NSObject, NSCoding { |
2 |
var name: String |
3 |
|
4 |
...
|
5 |
}
|
Em seguida, vamos implementar os dois métodos declarados no protocolo NSCoding
, init(code:)
e encodeWithCoder(_:)
. A implementação é simples se você está familiarizado com o protocolo NSCoding
.
1 |
import Foundation |
2 |
|
3 |
class Task: NSObject, NSCoding { |
4 |
var name: String |
5 |
|
6 |
@objc required init(coder aDecoder: NSCoder) { |
7 |
name = aDecoder.decodeObjectForKey("name") as! String |
8 |
}
|
9 |
|
10 |
@objc func encodeWithCoder(aCoder: NSCoder) { |
11 |
aCoder.encodeObject(name, forKey: "name") |
12 |
}
|
13 |
|
14 |
convenience override init() { |
15 |
self.init(name: "New Task") |
16 |
}
|
17 |
|
18 |
init(name: String) { |
19 |
self.name = name |
20 |
}
|
21 |
}
|
O inicializador init(coder:)
é um inicializador designado que inicializa uma instância Task
. Embora implementamos o método init(coder:)
em conformidade com o protocolo NSCoding
, você não precisa chamar o método diretamente. O mesmo serve para encodeWithCoder(_:)
, que codifica uma instância da classe Task
.
A palavra-chave required
prefixando o método init(coder:)
indica que toda subclasse da classe Task
precisa implementar este método. A palavra-chave required
apenas se aplica aos inicializadores, por isso não precisamos adiciona-la ao método encodeWithCoder(_:)
.
Antes de seguir em frente, precisamos falar sobre o atributo @objc
. Como o protocolo NSCoding
é um protocolo Objective-C, a conformidade do protocolo só pode ser verificada adicionando o atributo @objc
. Em Swift, não existe essas coisas de conformidade de protocolo ou métodos opcional de protocolo. Em outras palavras, se a classe adiciona um determinado protocolo, o compilador verifica e espera que todos os métodos do protocolo sejam implementados.
Classe ToDo
Com a classe Task
implementada, é hora de implementar a classe ToDo
. Criar um novo arquivo Swift com o nome ToDo.swift. Vamos ver como é a implementação da classe ToDo
.
1 |
import Foundation |
2 |
|
3 |
class ToDo: Task { |
4 |
var done: Bool |
5 |
|
6 |
@objc required init(coder aDecoder: NSCoder) { |
7 |
self.done = aDecoder.decodeObjectForKey("done") as! Bool |
8 |
super.init(coder: aDecoder) |
9 |
}
|
10 |
|
11 |
@objc override func encodeWithCoder(aCoder: NSCoder) { |
12 |
aCoder.encodeObject(done, forKey: "done") |
13 |
super.encodeWithCoder(aCoder) |
14 |
}
|
15 |
|
16 |
init(name: String, done: Bool) { |
17 |
self.done = done |
18 |
super.init(name: name) |
19 |
}
|
20 |
}
|
A classe ToDo
herda da classe Task
e declara uma propriedade variável done
do tipo Bool
. Além dos dois métodos exigidos do protocolo de NSCoding
, que herda da classe de Task
, ele também declara um inicializador designado, init(name:done:)
.
Como em Objective-C, a palavra chave super
refere-se a superclasse, a classe Task
no exemplo. Este é um detalhe importante que merece atenção. Antes de você chamar o método init(name:)
na superclasse, cada propriedade declarada pela classe ToDo
precisa ser inicializado. Em outras palavras, antes da classe ToDo
delegar a inicialização para sua superclasse, cada propriedade declarada da classe ToDo
, precisa ter um valor inicial válido. Você pode verificar isso trocando a order da instrução e verificar o erro que aparece.



O mesmo se aplica ao método init(coder:)
. Primeiro vamos inicializar a propriedade done
antes de chamar init(coder:)
na superclasse. Note também o downcast e o desempacotamento forçado do resultado de decodeObjectForKey(_:)
para um Bool
usando as!
.
Inicializadores e Herança
Quando lidamos com herança e inicialização, há algumas regras que devemos ter em mente. A regra para inicializadores designados é simples.
- Um inicializador designado precise chamar um inicializador designado de sua superclasse. Na classe
ToDo
, por exemplo, o métodoinit(coder:)
chama o métodoinit(coder:)
de sua superclasse. Isto também é referido como delegating up.
As regras para inicializadores por conveniência são um pouco mais complexas. Há duas regras para se ter em mente.
- Um inicializador por conveniência sempre precisa chamar outro inicializador que é definido na própria classe. Na classe
Task
, por exemplo, o métodoinit
é um inicializador por conveniência e delega a inicialização para outro inicializador,init(name:)
por exemplo. Isso é conhecido como delegating across. - Mesmo que um inicializador de conveniência não precise delegar a inicialização a um inicializador designado, um inicializador de conveniência precisa chamar um inicializador designado em algum momento. Isto é necessário para inicializar completamente a instância que está sendo inicializada.
Com ambos os modelos de classes implementados, é hora de retaforar as classes ViewController
e AddItemViewController
. Vamos começar com o último.
2. Refatoração da AddItemViewController
Passo 1: Atualizando o Protocolo AddItemViewControllerDelegate
A unica alteração que precisamos fazer na classe AddItemViewController
é relacionado ao protocolo AddItemViewControllerDelegate
. Na declaração do protocolo, mudaremos o tipo do didAddItem
de String
para ToDo
, a classe modelo que implementamos anteriormente.
1 |
protocol AddItemViewControllerDelegate { |
2 |
func controller(controller: AddItemViewController, didAddItem: ToDo) |
3 |
}
|
Passo 1: Atualizar a Ação create
Isto significa que nós também precisamos atualizar a ação create
em que chamamos o método delegate. Na atualização da implementarção, criamos uma instância ToDo
, passando ela ao método delegate.
1 |
@IBAction func create(sender: AnyObject) { |
2 |
let name = self.textField.text |
3 |
|
4 |
let item = ToDo(name: name, done: false) |
5 |
|
6 |
if let delegate = self.delegate { |
7 |
delegate.controller(self, didAddItem: item) |
8 |
}
|
9 |
}
|
3. Refatoração da ViewController
Passo 1: Atualizando Propriedade Items
A classe ViewController
requer um pouco mais de trabalho. Primeiro precisamos alterar o tipo da propriedade items
para [ToDo]
, um array de instâncias ToDo
.
1 |
var items: [ToDo] = [] { |
2 |
didSet { |
3 |
let hasItems = items.count > 0 |
4 |
self.tableView.hidden = !hasItems |
5 |
self.messageLabel.hidden = hasItems |
6 |
}
|
7 |
}
|
Passo 2: Métodos do Table View Data Source
Isto também significa que é preciso refatorar alguns outros métodos, tais como o método cellForRowAtIndexPath(_:)
mostrado abaixo. Como a array items
agora contém instâncias de ToDo
, verificar se um item foi marcado como finalizado é muito mais simples. Nós usamos o operador condicional ternário do Swift para atualizar o tipo de acessório da table view cell.
1 |
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { |
2 |
// Fetch Item
|
3 |
let item = self.items[indexPath.row] |
4 |
|
5 |
// Dequeue Table View Cell
|
6 |
let tableViewCell = tableView.dequeueReusableCellWithIdentifier("TableViewCell", forIndexPath: indexPath) as! UITableViewCell |
7 |
|
8 |
// Configure Table View Cell
|
9 |
tableViewCell.textLabel?.text = item.name |
10 |
tableViewCell.accessoryType = item.done ? .Checkmark : .None |
11 |
|
12 |
return tableViewCell |
13 |
}
|
Quando o usuário deletar um item, precisamos apenas atualizar a propriedade items
removendo um correspondente da instância ToDo
. Isso reflete na implementação do método tableView(_:commitEditingStyle:forRowAtIndexPath:)
mostrado a seguir.
1 |
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { |
2 |
if editingStyle == .Delete { |
3 |
// Fetch Item
|
4 |
let item = self.items[indexPath.row] |
5 |
|
6 |
// Update Items
|
7 |
self.items.removeAtIndex(indexPath.row) |
8 |
|
9 |
// Save State
|
10 |
self.saveItems() |
11 |
|
12 |
// Update Table View
|
13 |
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Right) |
14 |
}
|
15 |
}
|
Passo 3: Métodos do Table View Delegate
A atualização do estado de um item quando o usuário seleciona uma linha é manipulada no método tableView(_:didSelectRowAtIndexPath:)
. A implementação deste método UITableViewDelegate
ficou muito simples, graças a classe ToDo
.
1 |
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { |
2 |
tableView.deselectRowAtIndexPath(indexPath, animated: true) |
3 |
|
4 |
// Fetch Item
|
5 |
let item = self.items[indexPath.row] |
6 |
|
7 |
// Fetch Table View Cell
|
8 |
let tableViewCell = tableView.cellForRowAtIndexPath(indexPath) |
9 |
|
10 |
// Update Item
|
11 |
item.done = !item.done |
12 |
|
13 |
// Update Table View Cell
|
14 |
tableViewCell?.accessoryType = item.done ? .Checkmark : .None |
15 |
|
16 |
// Save State
|
17 |
self.saveItems() |
18 |
}
|
A instância correspondente do ToDo
é atualizada e essa alteração é refletida pelo table view. Para salvar o estado, chamaremos saveItems
ao invés do saveCheckeItems
.
Passo 4: Métodos da Add Item View Controller Delegate
Já que atualizamos o protocolo AddItemViewControllerDelegate
, também precisamos atualizar a implementação da ViewController
deste método. A alteração, entretanto, é simples. Precisamos apenas atualizar a assinatura do método.
1 |
func controller(controller: AddItemViewController, didAddItem: ToDo) { |
2 |
// Update Data Source
|
3 |
self.items.append(didAddItem) |
4 |
|
5 |
// Save State
|
6 |
self.saveItems() |
7 |
|
8 |
// Reload Table View
|
9 |
self.tableView.reloadData() |
10 |
|
11 |
// Dismiss Add Item View Controller
|
12 |
self.dismissViewControllerAnimated(true, completion: nil) |
13 |
}
|
Passo 5: Salvando Itens
pathForItems
Em vez de armazenar os itens no banco de dados padrão do usuário, nós vamos armazená-los no diretório de documentos do aplicativo. Antes de atualizar os métodos loadItems
e saveItems
, vamos implementar um método auxiliar chamado pathForItems
. O método é privado e retorna um caminho, a localização do item no diretório de documentos.
1 |
private func pathForItems() -> String { |
2 |
let documentsDirectory = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first as! String |
3 |
return documentsDirectory.stringByAppendingPathComponent("items") |
4 |
}
|
Nós primeiro buscaremos o caminho para o diretório de documentos na área de segurança do aplicativo chamando NSSearchPathForDirectoriesInDomains(_:_:_:). Como este método retorna um array de strings, vamos pegar o primeiro item, forçar o desempacotamento, e fazer o downcast para um String
. O valor retornado do pathForItems
é composto do caminho para o diretório de documentos com a string "items"
anexada a ele.
loadItems
O método loadItems muda um pouco. Primeiro armazenamos o resultado do pathForItems
em uma constante chamada path
. Então, desarquivamos o objeto arquivado no caminho e fazemos o downcast para um array opcional da instância ToDo
. Usamos optional binding para desempacotar o opcional e atribui-lo à contante chamada items
. Na clausula if
, atribuímos o valor armazenado em items
para a propriedade items
.
1 |
private func loadItems() { |
2 |
let path = self.pathForItems() |
3 |
|
4 |
if let items = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as? [ToDo] { |
5 |
self.items = items |
6 |
}
|
7 |
}
|
saveItems
O método saveItems é pequeno e simples. Armazenamos o resultado da pathForItems
em uma constante chamava path
e chamamos archiveRootObject(_:toFile:)
em NSKeyedArchiver
, passando a propriedade items
e path
. Imprimimos o resultado da operação no console.
1 |
private func saveItems() { |
2 |
let path = self.pathForItems() |
3 |
|
4 |
if NSKeyedArchiver.archiveRootObject(self.items, toFile: path) { |
5 |
println("Successfully Saved") |
6 |
} else { |
7 |
println("Saving Failed") |
8 |
}
|
9 |
}
|
Passo 6: Limpando
Vamos finalizar com a parte divertida, deletando código. Começaremos removendo a propriedade checkedItems
no topo, já que não precisamos mais dela. Como resultado, podemos remover também os métodos loadCheckedItems
e saveCheckedItems
, e qualquer referência a esses métodos na classe ViewController
.
Compile e execute o aplicativo para ver se ainda está tudo funcionando. O modelo de dados torna o código do aplicativo muito mais simples e confiável. Graças a classe ToDo
, gerenciar os itens em nossa lista é muito mais fácil agora e menos propenso a erros.
Conclusão
Neste tutorial, refatoramos o modelo de dados do nosso aplicativo. Aprendemos um pouco mais sobre programação orientada a objetos e herança. Inicialização de instância é um conceito importante em Swift, por isso certifique-se de entender o que foi explicado neste tutorial. Você pode ler sobre inicialização e delegação de inicializador em The Swift Programming Language.
Seja o primeiro a saber sobre novas traduções–siga @tutsplus_pt no Twitter!