Advertisement
  1. Code
  2. Swift

Swift from Scratch: Inicialização e inicializador por delegação

Scroll to top
Read Time: 12 min
This post is part of a series called Swift From Scratch.
Swift From Scratch: Access Control and Property Observers

() 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étodo init(coder:) chama o método init(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étodo init é 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!

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.