Advertisement
  1. Code
  2. Mobile Development
  3. iOS Development

Swift с нуля: Запуск и объявление иницииализатора

Scroll to top
Read Time: 10 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)

В предыдущих выпусках Swift с нуля, мы создали работающее приложение to-do.  И вы уже могли полюбить созданную модель данных. В этом уроке мы собираемся реорганизовать эту модель данных, реализуя собственный класс модели.

1. Модель данных

Модель данных, которую мы собираемся реализовать, включает два класса: класс  Task и класс ToDo, который наследуется из класса Task. Пока мы создаем и внедряем эти классы моделей, мы продолжаем наше изучение объектно-ориентированного программирования в Swift. В этом уроке мы рассмотрим инициализацию экземпляров класса и принципы наследования ролей во время инициализации.

Task Class

Начнем с реализации класса Task.  Создайте новый файл Swift, выбрав New > File ... в меню File в Xcode. Выберите Swift File в разделе iOS> Source. Назовите файл Task.swift и нажмите Create.

Основная реализация краткая и простая. Класс Task наследует NSObject, определенный в Foundation Framework, и свойство переменной name типа String. Класс определяет два инициализатора, init и init(name :). Есть несколько нюансов, которые могут вас сбить с толку, поэтому позвольте мне объяснить всё подробнее. Есть несколько нюансов, которые могут вас сбить с толку, поэтому позвольте мне объяснить всё подробнее.

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
}

Поскольку метод init также определен в классе NSObjectclass, нам нужно добавить префикс инициализатора с помощью ключевого слова override. В предыдущих сериях мы рассмотрели методы переопределения. В методе init мы вызываем метод init(name:), передавая в "New Task" значение параметра name.

Метод init(name:) - это еще один инициализатор, принимающий параметр name типа String. В этом инициализаторе значение параметра name присваивается свойству name. Это достаточно легко понять. Правильно?

Инициализаторы Designated и Convenience

Что происходит с ключевым словом convenience, под префиксом метода init? Классы могут иметь два типа инициализаторов, Designated инициализаторы и инициализаторы Convenience. Инициализаторы Convenience имеют префикс ключевого слова Convenience, что подразумевает, что init(name:) является Designated инициализатором.  Почему так происходит? В чем разница между Designated и Convenience инициализаторами?

Designated инициализаторы полностью инициализируют экземпляр класса, что означает, что каждое свойство экземпляра имеет начальное значение после инициализации. Например, глядя на класс Task, мы видим, что свойство name задано со значением параметра name инициализатора init(name:). Результат после инициализации - это полностью инициализированный экземпляр Task.

Однако инициализаторы Convenience полагаются на Designated инициализатор для создания экземпляра класса. Вот почему инициализатор init класса Task вызывает init(name:) в своей реализации. Этот процесс называется делегированием инициализатора. init делегирует к назначенному инициализатору для создания класса Task.

Инициализаторы Convenience необязательны. Не каждый класс имеет инициализатор Convenience. Designated инициализаторы обязательны, и класс должен иметь по крайней мере один Designated инициализатор для создания экземпляра.

Протокол NSCoding

Реализация класса Task еще не завершена. Далее в этой статье мы создадим массив ToDo на диске. Это возможно только в том случае, если экземпляры класса ToDo могут быть закодированы и декодированы.

Не волнуйтесь, это не ракетостроение. Нам всего лишь нужно сделать классы Task и ToDo совместимыми с NSCodingprotocol.  Поэтому класс Task наследует форму класса NSObject, поскольку протокол NSCoding может быть реализован только классами, наследующими - прямо или косвенно - из NSObject.  Подобно классу NSObject, протокол NSCoding определён в Foundation framework.

Принятие протокола - это то, что мы уже рассмотрели в этой серии, но есть несколько ньюансов, о которых я хочу указать. Начнем с того, что мы сообщаем компилятору, о том что класс Task соответствует протоколу NSCoding.

1
class Task: NSObject, NSCoding {
2
    var name: String
3
    
4
    ...
5
}

Затем нам нужно реализовать два метода, объявленных в протоколе NSCoding, init(coder:) и encodeWithCoder(_:). Реализация достаточно проста, если вы знакомы с протоколом 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
}

Инициализатор init(coder:) designated, он инициализирует экземпляр Task. Несмотря на то, что мы реализуем метод init(coder:) для соответствия протоколу NSCoding, вам никогда не придется ссылаться на этот метод напрямую. То же самое верно и для encodeWithCoder(_:), который кодирует экземпляр класса Task.

Ключевое слово required - префикс метода init(coder:), указывает, что каждый подкласс класса Task должен реализовать этот метод. Ключевое слово required применяется только к инициализаторам, поэтому нам не нужно добавлять его в метод encodeWithCoder(_ :).

Прежде чем пойти дальше, нам нужно поговорить об атрибуте @objc. Поскольку протокол NSCoding является протоколом Objective-C, соответствие может быть проверено только путем добавления атрибута @objc.  В Swift нет такой вещи, как соответствие протокола или дополнительные методы протокола. Другими словами, если класс соответствует определенному протоколу, компилятор проверяет и ожидает, что будет реализован каждый метод протокола.

Класс ToDo

После реализованного класса Task пришло время реализовать класс ToDo. Создайте новый файл Swift и назовите его ToDo.swift. Давайте посмотрим на реализацию класса 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
}

Класс ToDo наследуется из класса Task и объявляет свойство переменной Done типа Bool.  В дополнение к двум требуемым методам протокола NSCoding, которые он наследует от класса Task, он также объявляет designated инициализатор init(name:done:).

Как и в Objective-C, ключевое слово super ссылается на суперкласс, каким явялется класс Task в этом примере. Есть одна важная деталь, заслуживающая внимания. Прежде чем вы вызовете метод init(name:) в суперклассе, каждое свойство, объявленное в классе ToDo , должно быть инициализировано. Другими словами, прежде чем класс ToDo делегирует инициализацию своего суперкласса, каждое свойство, объявленное классом ToDo, должно иметь начальное значение. Вы можете проверить это, переключив порядок инструкций и проверив ошибки во всплывающем окошке.

То же самое относится к методу init(coder:). Сначала мы инициализируем свойство done перед вызовом init(coder:) в суперклассе. Также обратите внимание, что мы изменяем и разворачиваем результат decodeObjectForKey(_ :) на Bool, используя as!.

Инициализаторы и наследование

Имея дело с наследованием и инициализацией, существует несколько правил, которые следует учитывать. Правило для designated инициализаторов простое.

  • Designated инициализатор должен делать вызов из своего суперкласса. В классе ToDo, например, метод init(coder:) вызывает init(coder:) из суперкласса. Это также называется делегированием.

Правила для инициализаторов convenience немного сложнее. Есть два правила, которые следует иметь в виду.

  • Инициализатор convenience всегда должен вызвать другой инициализатор класса, в котором он определен. В классе Task, например, метод init является инициализатором convenience и делегирует инициализацию init(name:) в примере. Это называется кросс делегирование.
  • Несмотря на то, что convenience  инициализатор не должен делегировать к назначенному инициализатору, convenience инициализатор должен вызвать определенный инициализатор в определённый момент. Это необходимо для полной инициализации экземпляра, который задействован.

При использовании обоих классов моделей настало время реорганизовать классы ViewController и AddItemViewController. Давайте начнем с последнего.

2. Реорганизация AddItemViewController

Шаг 1: Обновление протокола AddItemViewControllerDelegate

Единственные изменения, которые мы должны внести в класс AdAdItemViewController, связаны с протоколом AdAdItemViewControllerDelegate.  В объявлении протокола измените тип didAddItem String на ToDo, класс модели, который мы реализовали ранее.

1
protocol AddItemViewControllerDelegate {
2
    func controller(controller: AddItemViewController, didAddItem: ToDo)
3
}

Шаг 2: Обновление create действия

Это означает, что нам также необходимо обновить действие create, в котором мы вызываем делегированный метод. В обновленной реализации мы создаем экземпляр ToDo, передавая его делегированному методу.

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. Реорганизация ViewController

Шаг 1: Обновление свойств items

Класс ViewController потребует немного больше работы. Сначала нам нужно изменить тип свойства items на [ToDo], в массиве экземпляров 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
}

Шаг 2. Методы исходных данных в таблице

Это также означает, что нам нужно реорганизовать несколько других методов, таких как метод cellForRowAtIndexPath(_:), показанный ниже. Поскольку массив items теперь содержит экземпляры ToDo, проверка того, что элемент отмечен как выполненный происходит намного проще. Мы используем тернарный оператор Swift для обновления типа ячейки таблицы.

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
}

Когда пользователь удаляет элемент, нам нужно только обновить свойство items, удалив соответствующий экземпляр ToDo. Это отражено в реализации метода tabletableView (_:commitEditingStyle:forRowAtIndexPath:), показанного ниже.

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
}

Шаг 3. Методы делегирования в таблице

Обновление состояния элемента, когда пользователь удаляет строку, обрабатывается в методе tableView(_:didSelectRowAtIndexPath:). Реализация этого метода UITableViewDelegate простая благодаря классу 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
}

Соответствующий экземпляр ToDo обновляется, и это изменение отражается в представлении таблицы. Чтобы сохранить состояние, мы вызываем saveItems вместо saveCheckedItems.

Шаг 4: Добавление методов делегирования Item View Controller

Поскольку мы обновили протокол AddItemViewControllerDelegate, нам также необходимо обновить реализацию ViewController. Изменение совсем простое. Нам нужно только обновить подпись метода.

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
}

Шаг 5: Сохранение элементов

pathForItems

Вместо того, чтобы хранить элементы в пользовательской базе данных по умолчанию, мы будем использовать папку в приложении. Прежде чем мы обновим методы loadItems и saveItems, мы реализуем вспомогательный метод pathForItems. Метод является закрытым и возвращает путь, расположение элементов в каталоге документов.

1
private func pathForItems() -> String {
2
    let documentsDirectory = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first as! String
3
    return documentsDirectory.stringByAppendingPathComponent("items")
4
}

Сначала мы получаем путь к каталогу документов в приложении, вызвав NSSearchPathForDirectoriesInDomains (_:_:_). Поскольку этот метод возвращает массив строк, мы выделяем первый элемент, принудительно разворачиваем его и изменяем его на String.  Возвращаемое значение из pathForItems состоит из пути к каталогу документов с добавленной к нему строкой "items".

loadItems

Метод loadItems изменяем совсем немного. Сначала мы сохраняем результат pathForItems в константу  под названием path. Затем мы разворачиваем объект, заархивированный по этому пути, и переносим его в необязательный массив экземпляров ToDo.  Мы используем необязательную привязку, чтобы развернуть параметр и назначить его в константу items. В условии if мы указываем значение, хранящееся в items, в свойстве 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

Метод saveItems простой.  Мы сохраняем результат pathpathItems в константу path  и вызываем archiveRootObject(_:toFile:) на NSKeyedArchiver, передавая свойство items и path. Мы выводим результат операции на консоль.

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
}

Шаг 6: Очистка

Давайте завершим наш урок весело, удаляя код. Начните с удаления свойства checkedItems вверху, так как мы больше не нуждаемся в нем. В результате этого мы также можем удалить методы loadCheckedItems и saveCheckedItems и каждую ссылку на эти методы в классе ViewController.

Создайте и запустите приложение, чтобы увидеть, все ли работает. Модель данных делает код приложения намного проще и надежнее. Благодаря классу ToDo управление элементами в нашем списке теперь намного проще и содержит меньше ошибок.

Вывод

В этом уроке мы реорганизовали модель данных нашего приложения. Вы узнали больше об объектно-ориентированном программировании и наследовании. Инициализация экземпляра является важной концепцией в Swift, поэтому убедитесь, что вы понимаете, всё то, что мы рассмотрели в этом уроке. Вы можете больше узнать об инициализации и делегировании инициализаторов на языке в статье Swift Programming.

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.