Статьи

Swift From Scratch: инициализация и делегирование инициализатора

На предыдущем уроке Swift From Scratch мы создали функциональное приложение для ведения дел. Однако модель данных может использовать некоторую любовь. В этом последнем уроке мы собираемся реорганизовать модель данных путем реализации пользовательского класса модели.

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

Начнем с реализации класса 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 .

Удобные инициализаторы являются необязательными. Не каждый класс имеет удобный инициализатор. Требуются назначенные инициализаторы, и класс должен иметь как минимум один назначенный инициализатор, чтобы создать полностью инициализированный экземпляр самого себя.

Однако реализация класса 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 нет такой вещи как соответствие протоколу или дополнительные методы протокола. Другими словами, если класс придерживается определенного протокола, компилятор проверяет и ожидает, что каждый метод протокола реализован.

После реализации класса 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 . Начнем с последнего.

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

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

Это означает, что нам также необходимо обновить действие 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)
    }
}

Класс 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
    }
}

Это также означает, что нам необходимо провести рефакторинг нескольких других методов, таких как метод 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()
    }
}

Обновление состояния элемента, когда пользователь 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() .

Поскольку мы обновили протокол 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)
}

Вместо того, чтобы хранить элементы в пользовательской базе данных по умолчанию, мы собираемся сохранить их в каталоге документов приложения. Прежде чем обновить 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 меняется совсем немного. Сначала мы сохраняем результат 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() является коротким и простым. Мы сохраняем результат 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»)
    }
}

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

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

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

А пока ознакомьтесь с некоторыми другими нашими курсами и учебными пособиями по разработке iOS на языке Swift!

  • стриж
    Создавайте приложения для iOS с помощью Swift
    Маркус Мюльбергер
  • IOS
    Идите дальше со Swift: анимация, работа в сети и пользовательские элементы управления
    Маркус Мюльбергер
  • стриж
    Запрограммируйте игру с боковой прокруткой с помощью Swift и SpriteKit
    Дерек Дженсен
  • iOS SDK
    Правильный способ поделиться состоянием между контроллерами Swift View
    Маттео Манфердини