Swift с нуля: Запуск и объявление иницииализатора
() 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.