Advertisement
  1. Code
  2. Swift

Swift from Scratch: Controle de Acesso e observadores de propriedade

Scroll to top
Read Time: 13 min
This post is part of a series called Swift From Scratch.
Swift From Scratch: Delegation and Properties
Swift From Scratch: Initialization and Initializer Delegation

() 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 classe ViewController.

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!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.