На предыдущем уроке Swift From Scratch мы создали функциональное приложение для ведения дел. Однако модель данных может использовать некоторую любовь. В этом последнем уроке мы собираемся реорганизовать модель данных путем реализации пользовательского класса модели.
1. Модель данных
Модель данных, которую мы собираемся реализовать, включает в себя два класса, класс Task
класс ToDo
который наследуется от класса Task
. Пока мы создаем и реализуем эти модельные классы, мы продолжим наше исследование объектно-ориентированного программирования в Swift. В этом уроке мы рассмотрим инициализацию экземпляров классов и то, какую роль играет наследование во время инициализации.
Класс Task
Начнем с реализации класса Task
. Создайте новый файл Swift, выбрав New> File … в меню File Xcode. Выберите « Файл Swift» в разделе « iOS»> «Источник ». Назовите файл Task.swift и нажмите « Создать» .
Основная реализация короткая и простая. Класс Task
наследуется от NSObject
, определенного в платформе Foundation , и имеет переменное name
свойства типа String
. Класс определяет два инициализатора, init()
и init(name:)
. Есть несколько деталей, которые могут сбить вас с толку, поэтому позвольте мне объяснить, что происходит.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
import Foundation
class Task: NSObject {
var name: String
convenience override init() {
self.init(name: «New Task»)
}
init(name: String) {
self.name = name
}
}
|
Поскольку метод init()
также определен в классе NSObject
, нам нужно NSObject
префикс инициализатора к ключевому слову override
. Мы рассмотрели методы переопределения ранее в этой серии . В методе init()
мы вызываем метод init(name:)
, передавая "New Task"
в качестве значения параметра name
.
Метод init(name:)
— это другой инициализатор, принимающий одно name
параметра типа String
. В этом инициализаторе значение параметра name
присваивается свойству name
. Это достаточно легко понять. Правильно?
Назначенные и удобные инициализаторы
Что за convenience
ключевое слово с префиксом метода init()
? Классы могут иметь два типа инициализаторов, назначенные инициализаторы и удобные инициализаторы. Удобные инициализаторы имеют префикс convenience
ключевого слова, что означает, что init(name:)
является назначенным инициализатором. Это почему? В чем разница между назначенными и удобными инициализаторами?
Назначенные инициализаторы полностью инициализируют экземпляр класса, что означает, что каждое свойство экземпляра имеет начальное значение после инициализации. Посмотрев, например, на класс Task
, мы видим, что для свойства name
установлено значение параметра init(name:)
инициализатора init(name:)
. Результатом после инициализации является полностью инициализированный экземпляр Task
.
Однако удобные инициализаторы полагаются на назначенный инициализатор для создания полностью инициализированного экземпляра класса. Вот почему init()
класса Task
вызывает init(name:)
в своей реализации. Это называется делегированием инициализатора . init()
делегирует инициализацию назначенному инициализатору, чтобы создать полностью инициализированный экземпляр класса Task
.
Удобные инициализаторы являются необязательными. Не каждый класс имеет удобный инициализатор. Требуются назначенные инициализаторы, и класс должен иметь как минимум один назначенный инициализатор, чтобы создать полностью инициализированный экземпляр самого себя.
Протокол NSCoding
Однако реализация класса Task
не завершена. Позже в этом уроке мы напишем массив экземпляров ToDo
на диск. Это возможно только в том случае, если экземпляры класса ToDo
можно кодировать и декодировать.
Не волнуйтесь, это не ракетостроение. Нам нужно только привести классы Task
и ToDo
соответствие с протоколом NSCoding
. Вот почему класс Task
наследуется от класса NSCoding
поскольку протокол NSCoding
может быть реализован только классами, наследующими — прямо или косвенно — от NSObject
. Как и класс NSCoding
протокол NSCoding
определяется в платформе Foundation .
Принятие протокола — это то, что мы уже рассмотрели в этой серии, но есть несколько ошибок, на которые я хочу обратить внимание. Начнем с сообщения компилятору, что класс Task
соответствует протоколу NSCoding
.
1
2
3
4
5
6
7
8
9
|
import Foundation
class Task: NSObject, NSCoding {
var name: String
…
}
|
Далее нам нужно реализовать два метода, объявленных в протоколе NSCoding
: init?(coder:)
и encode(with:)
. Реализация проста, если вы знакомы с протоколом NSCoding
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import Foundation
class Task: NSObject, NSCoding {
var name: String
@objc required init?(coder aDecoder: NSCoder) {
name = aDecoder.decodeObject(forKey: «name») as!
}
@objc func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: «name»)
}
convenience override init() {
self.init(name: «New Task»)
}
init(name: String) {
self.name = name
}
}
|
init?(coder:)
— это назначенный инициализатор, который инициализирует экземпляр Task
. Даже несмотря на то, что мы реализуем метод init?(coder:)
для соответствия протоколу NSCoding
, вам никогда не потребуется вызывать этот метод напрямую. То же самое верно для encode(with:)
, который кодирует экземпляр класса Task
.
Обязательное ключевое слово с префиксом метода init?(coder:)
указывает, что каждый подкласс класса Task
должен реализовать этот метод. Обязательное ключевое слово применяется только к инициализаторам, поэтому нам не нужно добавлять его в метод encode(with:)
.
Прежде чем мы продолжим, нам нужно поговорить об @objc
. Поскольку протокол NSCoding
является протоколом Objective-C, соответствие протокола можно проверить только путем добавления атрибута @objc
. В Swift нет такой вещи как соответствие протоколу или дополнительные методы протокола. Другими словами, если класс придерживается определенного протокола, компилятор проверяет и ожидает, что каждый метод протокола реализован.
Класс ToDo
После реализации класса Task
пришло время реализовать класс ToDo
. Создайте новый файл Swift и назовите его ToDo.swift . Давайте посмотрим на реализацию класса ToDo
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import Foundation
class ToDo: Task {
var done: Bool
@objc required init?(coder aDecoder: NSCoder) {
self.done = aDecoder.decodeBool(forKey: «done»)
super.init(coder: aDecoder)
}
@objc override func encode(with aCoder: NSCoder) {
aCoder.encode(done, forKey: «done»)
super.encode(with: aCoder)
}
init(name: String, done: Bool) {
self.done = done
super.init(name: name)
}
}
|
Класс ToDo
наследуется от класса Task
и объявляет свойство переменной, done
типа Bool
. В дополнение к двум обязательным методам протокола NSCoding
, которые он наследует от класса Task
, он также объявляет назначенный инициализатор init(name:done:)
.
Как и в Objective-C, ключевое слово super
ссылается на суперкласс, класс Task
в этом примере. Есть одна важная деталь, которая заслуживает внимания. Перед вызовом метода init(name:)
в суперклассе каждое свойство, объявленное классом ToDo
должно быть инициализировано. Другими словами, прежде чем класс ToDo
делегирует инициализацию своему суперклассу, каждое свойство, определенное классом ToDo
должно иметь действительное начальное значение. Вы можете убедиться в этом, изменив порядок операторов и проверив всплывающую ошибку.
То же самое относится и к методу init?(coder:)
. Сначала мы инициализируем свойство done
перед вызовом init?(coder:)
для суперкласса.
Инициализаторы и Наследование
При работе с наследованием и инициализацией необходимо учитывать несколько правил. Правило для назначенных инициализаторов простое.
- Назначенный инициализатор должен вызывать указанный инициализатор из своего суперкласса. Например, в классе
ToDo
методinit?(coder:)
вызывает методinit?(coder:)
своего суперкласса. Это также называется делегированием .
Правила для удобных инициализаторов немного сложнее. Есть два правила, которые нужно иметь в виду.
- Удобный инициализатор всегда должен вызывать другой инициализатор класса, в котором он определен. Например, в классе
Task
методinit()
является вспомогательным инициализатором и делегирует инициализацию другому инициализатору,init(name:)
в примере. Это известно как делегирование через . - Даже если вспомогательному инициализатору не нужно делегировать инициализацию назначенному инициализатору, удобному инициализатору необходимо в определенный момент вызвать назначенный инициализатор. Это необходимо для полной инициализации инициализируемого экземпляра.
После реализации обоих классов модели настало время провести рефакторинг классов ViewController
и AddItemViewController
. Начнем с последнего.
2. Рефакторинг AddItemViewController
Шаг 1. Обновите протокол AddItemViewControllerDelegate
Единственные изменения, которые нам нужно внести в класс AddItemViewController
, связаны с протоколом AddItemViewControllerDelegate
. В объявлении протокола измените тип didAddItem
с String
на ToDo
, класс модели, который мы реализовали ранее.
1
2
3
4
5
|
protocol AddItemViewControllerDelegate {
func controller(_ controller: AddItemViewController, didAddItem: ToDo)
}
|
Шаг 2. Обновите действие create(_:)
Это означает, что нам также необходимо обновить действие create(_:)
в котором мы вызываем метод делегата. В обновленной реализации мы создаем экземпляр ToDo
, передавая его в метод делегата.
1
2
3
4
5
6
7
8
9
|
@IBAction func create(_ sender: Any) {
if let name = textField.text {
// Create Item
let item = ToDo(name: name, done: false)
// Notify Delegate
delegate?.controller(self, didAddItem: item)
}
}
|
3. Рефакторинг ViewController
Шаг 1: Обновите свойство items
Класс ViewController
требует немного больше работы. Сначала нам нужно изменить тип свойства items
на [ToDo]
, массив экземпляров ToDo
.
1
2
3
4
5
6
7
|
var items: [ToDo] = [] {
didSet(oldValue) {
let hasItems = items.count > 0
tableView.isHidden = !hasItems
messageLabel.isHidden = hasItems
}
}
|
Шаг 2. Методы источника данных табличного представления
Это также означает, что нам необходимо провести рефакторинг нескольких других методов, таких как метод tableView(_:cellForRowAt:)
показанный ниже. Поскольку массив items
теперь содержит экземпляры ToDo
, гораздо проще проверить, помечен ли элемент как выполненный. Мы используем троичный условный оператор Swift для обновления типа аксессуара ячейки табличного представления.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Fetch Item
let item = items[indexPath.row]
// Dequeue Cell
let cell = tableView.dequeueReusableCell(withIdentifier: «TableViewCell», for: indexPath)
// Configure Cell
cell.textLabel?.text = item.name
cell.accessoryType = item.done ?
return cell
}
|
Когда пользователь удаляет элемент, нам нужно только обновить свойство items
, удалив соответствующий экземпляр ToDo
. Это отражено в реализации метода tableView(_:commit:forRowAt:)
показанного ниже.
01
02
03
04
05
06
07
08
09
10
11
12
|
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// Update Items
items.remove(at: indexPath.row)
// Update Table View
tableView.deleteRows(at: [indexPath], with: .right)
// Save State
saveItems()
}
}
|
Шаг 3: Методы делегирования табличного представления
Обновление состояния элемента, когда пользователь tableView(_:didSelectRowAt:)
строки, обрабатывается в tableView(_:didSelectRowAt:)
. Реализация этого метода UITableViewDelegate
намного проще благодаря классу ToDo
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
// Fetch Item
let item = items[indexPath.row]
// Update Item
item.done = !item.done
// Fetch Cell
let cell = tableView.cellForRow(at: indexPath)
// Update Cell
cell?.accessoryType = item.done ?
// Save State
saveItems()
}
|
Соответствующий экземпляр ToDo
обновляется, и это изменение отражается в табличном представлении. Чтобы сохранить состояние, мы вызываем saveItems()
вместо saveCheckedItems()
.
Шаг 4: Добавить элемент Представление Методы делегата контроллера
Поскольку мы обновили протокол AddItemViewControllerDelegate
, нам также необходимо обновить реализацию этого протокола в ViewController
. Изменение, однако, просто. Нам нужно только обновить сигнатуру метода.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
func controller(_ controller: AddItemViewController, didAddItem: ToDo) {
// Update Data Source
items.append(didAddItem)
// Save State
saveItems()
// Reload Table View
tableView.reloadData()
// Dismiss Add Item View Controller
dismiss(animated: true)
}
|
Шаг 5: Сохранить элементы
Метод pathForItems()
Вместо того, чтобы хранить элементы в пользовательской базе данных по умолчанию, мы собираемся сохранить их в каталоге документов приложения. Прежде чем обновить loadItems()
и saveItems()
, мы собираемся реализовать вспомогательный метод с именем pathForItems()
. Метод является закрытым и возвращает путь, расположение элементов в каталоге документов.
1
2
3
4
5
6
7
8
|
private func pathForItems() -> String {
guard let documentsDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first,
let url = URL(string: documentsDirectory) else {
fatalError(«Documents Directory Not Found»)
}
return url.appendingPathComponent(«items»).path
}
|
Сначала мы выбираем путь к каталогу документов в песочнице приложения, вызывая NSSearchPathForDirectoriesInDomains(_:_:_:)
. Поскольку этот метод возвращает массив строк, мы берем первый элемент.
Обратите внимание, что мы используем оператор guard
чтобы убедиться, что значение, возвращаемое NSSearchPathForDirectoriesInDomains(_:_:_:)
является допустимым. Мы выдаем фатальную ошибку, если эта операция не удалась. Это немедленно прекращает применение. Почему мы это делаем? Если операционная система не может передать нам путь к каталогу документов, у нас есть большие проблемы, о которых нужно беспокоиться.
Значение, которое мы возвращаем из pathForItems()
, состоит из пути к каталогу документов с добавленной к нему строкой "items"
.
Метод loadItems()
Метод loadItems меняется совсем немного. Сначала мы сохраняем результат pathForItems()
в константе path
. Затем мы разархивируем объект, заархивированный по этому пути, и передаем его в необязательный массив экземпляров ToDo
. Мы используем необязательное связывание, чтобы развернуть необязательное и присвоить его константе. В предложении if
мы присваиваем значение, хранимое в items
items
свойству items
.
1
2
3
4
5
6
7
|
private func loadItems() {
let path = pathForItems()
if let items = NSKeyedUnarchiver.unarchiveObject(withFile: path) as?
self.items = items
}
}
|
Метод saveItems()
Метод saveItems()
является коротким и простым. Мы сохраняем результат pathForItems()
в константе path
и вызываем archiveRootObject(_:toFile:)
в NSKeyedArchiver
, передавая свойство items
и path
. Мы выводим результат операции на консоль.
1
2
3
4
5
6
7
8
9
|
private func saveItems() {
let path = pathForItems()
if NSKeyedArchiver.archiveRootObject(self.items, toFile: path) {
print(«Successfully Saved»)
} else {
print(«Saving Failed»)
}
}
|
Шаг 6: Очистить
Давайте закончим забавной частью, удалив код. Начните с удаления свойства checkedItems
сверху, так как оно нам больше не нужно. В результате мы также можем удалить loadCheckedItems()
и saveCheckedItems()
и все ссылки на эти методы в классе ViewController
.
Создайте и запустите приложение, чтобы увидеть, все ли еще работает. Модель данных делает код приложения намного проще и надежнее. Благодаря классу ToDo
, управление элементами в нашем списке теперь намного проще и менее подвержено ошибкам.
Вывод
В этом уроке мы провели рефакторинг модели данных нашего приложения. Вы узнали больше об объектно-ориентированном программировании и наследовании. Инициализация экземпляра является важной концепцией в Swift, поэтому убедитесь, что вы понимаете, что мы рассмотрели в этом уроке. Вы можете прочитать больше об инициализации и делегировании инициализатора на языке программирования Swift .
А пока ознакомьтесь с некоторыми другими нашими курсами и учебными пособиями по разработке iOS на языке Swift!