() translation by (you can also view the original English article)
Nos tutoriais anteriores, adicionamos a capacidade de criar itens de tarefa. Apesar deste acréscimo fazer com que o aplicativo fique um pouco mais útil, também seria conveniente adicionarmos a capacidade de marcar itens como finalizados e excluir itens. Este será o foco neste tutorial.
Pré-requisitos
Se você quiser me acompanhar, então certifique-se que você tem o Xcode 6.3 ou superior instalado em sua máquina. No momento desta escrita, o Xcode 6.3 está em beta e disponível a partir iOS Dev Center da Apple para desenvolvedores iOS registrados.
A razão para exigir o Xcode 6.3 ou superior é ser capaz de tirar proveito de Swift 1.2, que a Apple introduziu em Fevereiro. O Swift 1.2 introduz um número grande de adições que vamos aproveitar no resto da série.
1. Exclusão de Itens
Para deletar itens, precisamos implementar dois métodos adicionais do protocolo UITableViewDataSource
. Primeiro precisamos informar a table view que as linhas podem ser editadas implementando o método tableView(_: canEditRowsIndexPath:)
. Como você pode ver no código a seguir, a implementação é simples. Temos que informar a table view que todas as linhas são editáveis, retornando true
.
1 |
func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { |
2 |
return true |
3 |
}
|
O segundo método que estamos interessados é o tableView(_:commitEditingStyle:forRowAtIndexPath:)
A implementação é um pouco mais complexa, mas bem fácil de entender.
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 |
// Update Table View
|
10 |
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Right) |
11 |
}
|
12 |
}
|
Iniciamos verificando o valor da editingStyle
, uma enumeração do tipo UITableViewCellEditingStyle
. Apenas deletamos um item se o valor do editingStyle
for igual ao UITableViewCellEditingStyle.Delete
.
O Swift é mais esperto do que parece. Como ele sabe que editingStyle
é do tipo UITableViewCellEditingStyle
, podemos omitir UITableViewCellEditingStyle
do nome da enumeração e escrever .Delete
, apenas o valor membro da enumeração que nos interessa. Se você é novo em enumerações em Swift, então eu recomendo que você leia dica-rápida sobre enumerações em Swift.
Em seguida, buscamos o item correspondente da propriedade items
e armazenamos temporariamente o seu valor em uma constante chamada item
. Atualizamos a fonte de dados da table view, items
, chamando removeAtIndex(index: Int)
da propriedade items
, passando o índice correto.
Finalmente, atualizamos a table view chamando deleteRowsAtIndexPaths(_:withRowAnimation:)
na tableView
, passando um array com indexPath
e .Right
para especificar o tipo de animação. Como vimos anteriormente, podemos omitir o nome da enumeração, UITableViewRowAnimation
, já que o Swift sabe que o tipo do segundo argumento é UITableViewRowAnimation
.
Agora, o usuário deve ser capaz de excluir itens da lista. Compile e execute o aplicativo para testar.
2. Verificando os Itens
Para marcar um item como finalizado, vamos adicionar um marcador na linha correspondente. Isto implica que precisamos manter o controle dos itens que o usuário vai marcar como finalizado. Para este propósito, vamos declarar uma nova propriedade que gerenciará isto para nós. Declare uma propriedade variável, chekedItems
, do tipo [String]
e inicialize ela com um array vazio.
1 |
var checkedItems: [String] = [] |
Na tableView(_:cellForRowAtIndexPath:)
, vamos verificar se chekedItems
contém o respectivo item usando a função contains
, uma função global definida na Biblioteca Padrão do Swift. Passamos chekedItems
como o primeiro argumento e item
como o segundo argumento. A função retorna true
se checkedItems
contém o item
.
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 |
10 |
|
11 |
if contains(self.checkedItems, item) { |
12 |
tableViewCell.accessoryType = .Checkmark |
13 |
} else { |
14 |
tableViewCell.accessoryType = .None |
15 |
}
|
16 |
|
17 |
return tableViewCell |
18 |
}
|
Se item
existir em chekedItems
, vamos definir a propriedade accessoryType
da célula com .Checkmark
, um valor membro da enumeração UITableViewCellAccessoryType
. Se item
não existir, vamos voltar para .None
o tipo de acessório da célula.
O próximo passo é adicionar a funcionalidade de marcar um item como finalizado implementando um método do protocolo UITableViewDelegate
, tableView(_:didSelectRowAtIndexPath:)
. Neste método delegado, primeiro vamos chamar deselectRowAtIndexPath(_:animated:)
na tableView
para desmarcar as linhas que o usuário tocar.
1 |
// MARK: Table View Delegate Methods
|
2 |
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { |
3 |
tableView.deselectRowAtIndexPath(indexPath, animated: true) |
4 |
|
5 |
// Fetch Item
|
6 |
let item = self.items[indexPath.row] |
7 |
|
8 |
// Fetch Table View Cell
|
9 |
let tableViewCell = tableView.cellForRowAtIndexPath(indexPath) |
10 |
|
11 |
// Find Index of Item
|
12 |
let index = find(self.checkedItems, item) |
13 |
|
14 |
if let index = index { |
15 |
self.checkedItems.removeAtIndex(index) |
16 |
tableViewCell?.accessoryType = UITableViewCellAccessoryType.None |
17 |
|
18 |
} else { |
19 |
self.checkedItems.append(item) |
20 |
tableViewCell?.accessoryType = UITableViewCellAccessoryType.Checkmark |
21 |
}
|
22 |
}
|
Podemos, então, buscar o item correspondente de items
e uma referência da célula que corresponde com a linha tocada. Usamos a função find
, definida na Biblioteca Padrão do Swift, para obter o índice do item
em checkedItems
. A função find
retorna um opcional Int
. Se checkedItems
contém item
, removeremos ele da chekedItems
e alteramos o tipo acessório da célula como .None
. Se chekedItems
não contém item
, vamos adicionar ele no checkedItems
e definir o tipo acessório com .Checkmark
.
Com essas adições, o usuário é capaz agora de marcar itens como finalizados. Compile e execute o aplicativo para verificar que está tudo funcionando como o esperado.
3. Salvando Estado
A aplicação atualmente não salva o estado entre as execuções. Para resolver isto, agora vamos salvar os arrays items e chekedItems na base de dados user defaults do aplicativo.
Passo 1: Carregando Estado
Começamos criando dois métodos de ajuda, loadItems
e loadChekedItems
. Note a palavra-chave private
prefixando cada método de ajuda. A palavra-chave private
diz ao Swift que estes métodos são acessíveis apenas dentro deste arquivo fonte.
1 |
// MARK: Private Helpers
|
2 |
private func loadItems() { |
3 |
let userDefaults = NSUserDefaults.standardUserDefaults() |
4 |
|
5 |
if let items = userDefaults.objectForKey("items") as? [String] { |
6 |
self.items = items |
7 |
}
|
8 |
}
|
9 |
|
10 |
private func loadCheckedItems() { |
11 |
let userDefaults = NSUserDefaults.standardUserDefaults() |
12 |
|
13 |
if let checkedItems = userDefaults.objectForKey("checkedItems") as? [String] { |
14 |
self.checkedItems = checkedItems |
15 |
}
|
16 |
}
|
A palavra-chave private
faz parte do controle de acesso do Swift. Como o nome indica, controle de acesso define qual código te acesso a qual código. Níveis de acesso aplicam-se a métodos, funções, tipos, etc. A Apple simplesmente refere-se a entidades. Existem três níveis de acesso: públicos, internos e privados.
- Público: Entidades marcadas como pública são acessíveis tanto por entidades no mesmo módulo como em outros módulos. Este nível de acesso é ideal para expor a interface de uma estrutura.
- Interno: Este é o nível de acesso padrão. Em outras palavras, se não especificar o nível de acesso, este nível de acesso é aplicado. Uma entidade com um nível de acesso interno é acessível apenas por entidades definida no mesmo módulo.
- Privado: Uma entidade declarada como privada é acessível apenas por entidades no mesmo arquivo fonte. Por exemplo, os métodos de ajuda privado definidos na classe
ViewController
são acessível apenas pela classeViewController
.
A implementação dos métodos de ajuda são simples se você estiver familiarizado com a classe NSUserDefaults
. Para facilitar o uso, armazenamos uma referência do objeto standard user defaults na constante chamada de userDefaults
. No caso do loadItems
, pedimos ao userDefaults
pelo objecto associada com a chave "items"
e fazemos o downcast dele para um array opcional de strings. Desempacotamos (unwrap) com segurança o opcional, o que significa que armazenamos o valor em uma constante items
se o opcional não for nulo
, e atribuímos o valor pra o propriedade items
.
Se a instrução IF
parecer confusa, então de uma olhada na versão simplificada do método loadItems
no exemplo abaixo. O resultado é idêntico, a unica diferença é a concisão.
1 |
private func loadItems() { |
2 |
let userDefaults = NSUserDefaults.standardUserDefaults() |
3 |
let storedItems = userDefaults.objectForKey("items") as? [String] |
4 |
|
5 |
if let items = storedItems { |
6 |
self.items = items |
7 |
}
|
8 |
}
|
A implementação da loadCheckedItems
é idêntica, exceto pela chave usada para carregar os objetos armazenados na base de dados user defaults. Vamos colocar a loadItems
e loadCheckedItems
em uso atualizando o método viewDidLoad
.
1 |
override func viewDidLoad() { |
2 |
super.viewDidLoad() |
3 |
|
4 |
// Set Title
|
5 |
self.title = "To Do" |
6 |
|
7 |
// Load State
|
8 |
self.loadItems() |
9 |
self.loadCheckedItems() |
10 |
|
11 |
// Register Class for Cell Reuse
|
12 |
self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "TableViewCell") |
13 |
}
|
Passo 2: Salvando Estado
Para salvar o estado, implementaremos mais dois métodos de ajuda, saveItems
e saveCheckedItems
. A lógica é similar ao do loadItems
e loadCheckedItems
. A diferença é que vamos armazenar os dados na base de dados user defaults. Certifique-se que a chave usada para chamar o setObject(_:forkey:)
corresponde com a usada em loadItems
e loadCheckedItems
.
1 |
private func saveItems() { |
2 |
let userDefaults = NSUserDefaults.standardUserDefaults() |
3 |
|
4 |
// Update User Defaults
|
5 |
userDefaults.setObject(self.items, forKey: "items") |
6 |
userDefaults.synchronize() |
7 |
}
|
8 |
|
9 |
private func saveCheckedItems() { |
10 |
let userDefaults = NSUserDefaults.standardUserDefaults() |
11 |
|
12 |
// Update User Defaults
|
13 |
userDefaults.setObject(self.checkedItems, forKey: "checkedItems") |
14 |
userDefaults.synchronize() |
15 |
}
|
A chamada de synchronize
não é estritamente necessária. O sistema operacional fará com que os dados que você armazenar no banco de dados user defaults serão gravados no disco em algum momento. Chamando o synchronize
, entretanto, você explicitamente pede ao sistema operacional para salvar qualquer alteração pendente no disco. Isto é útil durante o desenvolvimento, porque o sistema operacional não salva suas alterações em disco se você mata o aplicativo. Então pode parecer que alguma coisa não está funcionando corretamente.
Precisamos chamar a saveItems
e saveCheckedItems
em alguns lugares. Para começar, chamar saveItems
quando um novo item for adicionado a lista. Fazemos isso no método delegado do protocolo AddItemViewControllerDelegate
.
1 |
// MARK: Add Item View Controller Delegate Methods
|
2 |
func controller(controller: AddItemViewController, didAddItem: String) { |
3 |
// Update Data Source
|
4 |
self.items.append(didAddItem) |
5 |
|
6 |
// Save State
|
7 |
self.saveItems() |
8 |
|
9 |
// Reload Table View
|
10 |
self.tableView.reloadData() |
11 |
|
12 |
// Dismiss Add Item View Controller
|
13 |
self.dismissViewControllerAnimated(true, completion: nil) |
14 |
}
|
Quando o estado de um item mudar em tableView(_:didSelectRowAtIndesPath:)
, atualizamos checkedItems
. É uma boa ideia também chamar saveCheckedItems
neste ponto.
1 |
// MARK: Table View Delegate Methods
|
2 |
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { |
3 |
tableView.deselectRowAtIndexPath(indexPath, animated: true) |
4 |
|
5 |
// Fetch Item
|
6 |
let item = self.items[indexPath.row] |
7 |
|
8 |
// Fetch Table View Cell
|
9 |
let tableViewCell = tableView.cellForRowAtIndexPath(indexPath) |
10 |
|
11 |
// Find Index of Item
|
12 |
let index = find(self.checkedItems, item) |
13 |
|
14 |
if let index = index { |
15 |
self.checkedItems.removeAtIndex(index) |
16 |
tableViewCell?.accessoryType = UITableViewCellAccessoryType.None |
17 |
|
18 |
} else { |
19 |
self.checkedItems.append(item) |
20 |
tableViewCell?.accessoryType = UITableViewCellAccessoryType.Checkmark |
21 |
}
|
22 |
|
23 |
// Save State
|
24 |
self.saveCheckedItems() |
25 |
}
|
Quando um item for deletado, ambos, items
e checkedItems
são atualizados. Para salvar está mudança, nós chamamos ambos, saveItems
e saveCheckedItems
.
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 |
if contains(self.checkedItems, item) { |
10 |
self.checkedItems.removeAtIndex(indexPath.row) |
11 |
}
|
12 |
|
13 |
// Save State
|
14 |
self.saveItems() |
15 |
self.saveCheckedItems() |
16 |
|
17 |
// Update Table View
|
18 |
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Right) |
19 |
}
|
20 |
}
|
É isso. Compile e execute o aplicativo para testar se funciona. Utilize um pouco o aplicativo e force o fechamento dele. Quando você iniciar o aplicativo novamente, a ultimo estado visível deve ser carregado novamente.
4. Observadores de Propriedade
A experiência do usuário do aplicativo é um pouco carente no momento. Quando todos os itens forem deletados ou quando o aplicativo for iniciado pela primeira vez, o usuário verá uma table view vazia. Isso não é legal. Para resolver isso mostraremos uma mensagem quando não tiver itens. Isto me dará a oportunidade de mostrar para vocês outro recurso do Swift, observadores de propriedade.
Passo 1: Adicionando uma Label
Vamos começar adicionando uma label para a interface do usuário mostrar a mensagem. Declare um outlet chamado messageLabel
do tipo UILabel
na classe ViewController
, abra o Main.storyboard, e adicione uma label na view da view controller.
1 |
@IBOutlet var messageLabel: UILabel! |
Adicione as restrições (constraints) de layout necessárias na label e conecte ela com a outlet messageLabel
da view controller no Connections Inspector. Altere o texto da label para You don't have any to-dos. e centralize o texto da label em Attributes Inspector.



Passo 2: Implementando um Observador de Propriedade
A label de mensagem apenas será visível se o items
não tiver elementos. Quando isso acontece, nós também vamos ocultar a exibição de tabela. Poderíamos resolver este problema adicionando varias verificações na classe ViewController
, mas uma abordagem mais conveniente e elegante é usar um observador de propriedade.
Como o nome indica, observadores de propriedade observa uma propriedade. Um observador de propriedades é chamada sempre que uma propriedade é alterada, até quando o novo valor é igual ao valor antigo. Há dois tipos de observadores de propriedade.
-
willSet
: chamada antes do valor ter mudado. -
didSet
: chamado depois do valor ter mudado.
Para nosso propósito, vamos implementar o observador didSet
para a propriedade items
. De uma olhada na sintaxe do trecho de código abaixo.
1 |
var items: [String] = [] { |
2 |
didSet { |
3 |
let hasItems = items.count > 0 |
4 |
self.tableView.hidden = !hasItems |
5 |
self.messageLabel.hidden = hasItems |
6 |
}
|
7 |
}
|
A construção pode parecer um pouco estranho no começo, então deixe-me explicar o que está acontecendo. Quando o observador didSet
for chamado, depois da propriedade items
for alterada, verificamos se a propriedade items
contém algum elemento. Baseado no valor da constante hasItems
, atualizamos a interface do usuário. É simples assim.
O observador didSet
passa um parâmetro constante que contém o valor antigo da propriedade. Isso foi omitido no exemplo acima, porque não precisamos disto em nossa implementação. O exemplo a seguir mostra como poderia ser usado.
1 |
var items: [String] = [] { |
2 |
didSet(oldValue) { |
3 |
if oldValue != items { |
4 |
let hasItems = items.count > 0 |
5 |
self.tableView.hidden = !hasItems |
6 |
self.messageLabel.hidden = hasItems |
7 |
}
|
8 |
}
|
9 |
}
|
O parametro oldValue
no exemplo não tem um tipo explicito, por que o Swift sabe o tipo da propriedade items
. No exemplo, nós apenas atualizamos a interface do usuário se o valor antigo for diferente do valor novo.
Um observador willSet
trabalha de uma forma semelhante. A principal diferença é que o parâmetro passado no observador willSet
é a constante com o valor novo da propriedade. Ao usar observadores de propriedade, tenha em mente que eles não são chamados quando a instância for incializada.
Compile e execute o aplicativo para certificar-se de que tudo está funcionando corretamente. Apesar do aplicativo não ser perfeito e podendo ter mais algumas funcionalidades, você criou seu primeiro aplicativo iOS usando Swift.
Conclusão
Ao longo das ultimas três lições desta serie, você criou um aplicativo iOS funcional usando recursos de orientação de objetos do Swift. Se você tem alguma experiência de programação e desenvolvimento de aplicações, então você deve ter notado que o modelo atual de dados tem algumas deficiências, para dizer levemente. Armazenar itens como strings e criar um array separado para armazenar o estado do item não é uma boa idéia se você está construindo um bom aplicativo. Uma abordagem melhor seria criar uma classe separada ToDo
para a modelagem dos itens e armazená-los na área de segurança do aplicativo. É nosso objetivo para a próxima edição desta série.
Seja o primeiro a saber sobre novas traduções–siga @tutsplus_pt no Twitter!