SA1016 — Списки (Lists)
Содержание страницы
Просто чтобы убедиться, что вы полностью понимаете все, что сделали до сих пор, в следующий раз вы расширите приложение новыми функциями, которые более или менее повторяют то, что вы только что сделали.
Но я также добавлю несколько поворотов, чтобы сделать его интересным…
Приложение называется Checklists не просто так: оно позволяет вам вести более одного списка дел. Однако до сих пор приложение поддерживало только один список. Теперь вы добавите возможность обрабатывать несколько контрольных списков.
Чтобы завершить функциональность этой главы, вам понадобятся два новых экрана, а это означает два новых контроллера просмотра:
AllListsViewController
— показывает все списки пользователей.ListDetailViewController
— позволяет добавлять новый список и редактировать имя и значок существующего списка.
В этой главе рассматриваются следующие вопросы:
- Контроллеры представления всех списков (The All Lists view controllers): Добавьте новый контроллер представления,чтобы показать все списки дел.
- Пользовательский интерфейс всех списков (The All Lists UI): Завершите пользовательский интерфейс для экрана всех списков.
- Просмотр контрольных списков (View the checklists): Отображение элементов задач для выбранного списка на экране Все списки.
- Управление контрольными списками (Manage checklists): добавьте контроллер представления для добавления/редактирования контрольных списков.
Контроллер представления всех списков (The All Lists view controller)
Вы сначала добавите AllListsViewController
. Это становится новым главным экраном (the new main screen) приложения.
Когда вы закончите, вот как это будет выглядеть:

Новый главный экран приложения
Этот экран очень похож на то, что вы создали раньше. Это контроллер табличного представления (a table view controller), который показывает список объектов Checklist
(не ChecklistItem
объектов).
Отныне я буду называть этот экран экраном All Lists (все списки), а экран, на котором отображаются пункты из одного контрольного списка — экраном “Контрольный список” (Checklist).
Добавьте новый контроллер представления (Add the new view controller)
- Щелкните правой кнопкой мыши группу Checklists в навигаторе проектов и выберите New File (Новый файл). Выберите Cocoa Touch Class template (шаблон класса Cocoa Touch) (под iOS, Source).
На следующем шаге выберите следующие параметры:
- Class (Класс): AllListsViewController
- Subclass of (Подкласс): UITableViewController
- Также создайте XIB-файл: убедитесь, что это не стоит галочка
- Язык: Swift
Примечание: Убедитесь, что в поле “Подкласс” установлено значение UITableViewController, а не “UIViewController”. Также будьте осторожны, чтобы Xcode не переименовал то, что вы ввели в класс, в “AllListsTableViewController” с дополнительным словом “Table” при изменении значения “Подкласс of”. Это может быть так подло…
- Нажмите кнопку Далее, а затем Create (Создать), чтобы закончить.
Как вы помните из предыдущей главы, шаблон Xcode для контроллера табличного представления содержит много шаблонного кода, который вам не нужен. Давайте сначала уберем это.
Вы также поместите некоторые поддельные данные в табличное представление, чтобы запустить его. Как вы уже знаете, мне всегда нравится делать как можно меньше шагов, а затем запускать приложение, чтобы посмотреть, работает ли оно. Как только все заработает, вы сможете двигаться вперед и вводить реальные данные.
Очистите шаблонный код
- В AllListsViewController.swift удалите весь закомментированный код
viewDidLoad
. - Удалите
numberOfSections(in:)
метод. Без этого в табличном представлении всегда будет один раздел. - Измените
tableView(_:numberOfRowsInSection:)
метод на:
override func tableView(
_ tableView: UITableView,
numberOfRowsInSection section: Int
) -> Int {
return 3
}
- Реализуйте
tableView(_:cellForRowAt:)
метод помещения некоторого текста в ячейки, просто чтобы было на что посмотреть.
Обратите внимание, что шаблонный код уже содержит закомментированную версию этого метода. Вы можете раскомментировать его, удалив /*
и */
окружив метод, и внести в него свои изменения.
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: cellIdentifier, for: indexPath)
cell.textLabel!.text = "List \(indexPath.row)"
return cell
}
В табличном представлении ChecklistViewController
используются прототипы ячеек, созданные в Interface Builder. Просто для удовольствия AllListsViewController
вы примете другой подход, когда вместо этого будете создавать ячейки в коде.
Подход к коду (the code approach) — это сама простота — вы просто удаляете ячейку из табличного представления, а затем настраиваете ее как обычную.
- В этот момент вы получите сообщение об
cellIdentifier
ошибке «неизвестно». Давайте добавим константу в класс для идентификатора ячейки — через мгновение вы поймете, почему. Добавьте следующее в верхнюю часть реализации класса, куда вы обычно добавляете переменные экземпляра:
let cellIdentifier = "ChecklistCell"
Вы используете dequeueReusableCell(withIdentifier:)
здесь то же самое, что и с прототипными ячейками. Однако здесь у нас нет прототипа ячейки. Итак, нам нужен способ сообщить приложению, какой тип ячейки табличного представления (или, скорее, класс ячейки) должен быть создан для вызова dequeueReusableCell(withIdentifier:)
с нашим пользовательским идентификатором ячейки, указанным в константе cellIdentifier
.
Для этого нам нужно добавить немного кода в viewDidLoad
, чтобы зарегистрировать идентификатор ячейки в базовом табличном представлении.
- Добавьте следующий код в конец
viewDidLoad
:
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellIdentifier)
Вышеизложенное регистрирует наш идентификатор ячейки в табличном представлении, чтобы табличное представление знало, какой класс ячейки следует использовать для создания нового экземпляра ячейки табличного представления, когда запрос dequeue поступает с этим идентификатором ячейки. Кроме того, в этом случае мы зарегистрировали стандартный класс ячеек табличного представления как класс, который будет использоваться для создания новых ячеек, но если вы хотите, вы также можете зарегистрировать здесь пользовательские классы ячеек табличного представления.
Теперь вы должны понять, почему мы настроили идентификатор ячейки на уровне класса — потому что нам нужен доступ к идентификатору как минимум из двух разных методов.
- Удалите все остальные закомментированный хлам из AllListsViewController.swift. Xcode помещает его туда, чтобы быть полезным, но он также создает беспорядок.
Изменения раскадровки (Storyboard changes)
Последний шаг — добавить новый контроллер представления в раскадровку.
- Откройте раскадровку и перетащите новый Table View Controller (контроллер табличного представления) на холст. Поместите его где-нибудь рядом с начальным навигационным контроллером.
Control-drag (перетащите) из навигационного контроллера в этот новый контроллер табличного представления.:

Control-перетащите из контроллера навигации в новый контроллер табличного представления.
Во всплывающем меню выберите Relationship Segue — root view controller:

Relationships — это тоже segues
Это приведет к разрыву существующего соединения между навигационным контроллером и ChecklistViewController
приложением, так что “Checklists” больше не будут главным экраном приложения.
- Выберите новый контроллер табличного представления и установите его Class в Identity inspector в AllListsViewController.
- Выберите элемент навигации нового контроллера представления (Navigation Item) в контуре документа (Document Outline), а затем измените его название на Checklists с помощью Attributes Inspector.
Это приведет к тому, что Xcode переименует контроллер представления в контуре документа из All Lists View Controller в Checklists — иногда этого не произойдет, пока вы не перезагрузите Xcode. Это немного сбивает с толку, потому что уже есть контроллер просмотра контрольных списков.
Исправить названия сцен достаточно просто. Обычно имя сцены основано либо на имени базового контроллера представления, либо на названии элемента навигации. Но вы можете установить все, что хотите, в качестве имени сцены, просто изменив отображаемый заголовок в контуре документа :]
- Коснитесь нового контроллера вида в контуре документа — желтого круга, а не прямоугольника, представляющего сцену, — а затем коснитесь его еще раз, чтобы перевести заголовок в режим редактирования. Затем просто переименуйте его во All Lists.

Переименовать сцену
Повторите описанный выше шаг, чтобы переименовать оставшуюся Checklists scene в Checklist (обратите внимание на пропущенную букву “s” в конце).
Возможно, на этом этапе вам захочется реорганизовать раскадровку, чтобы все снова выглядело аккуратно. Сцена «All Lists» находится между другими сценами.
Как я уже упоминал, вы не собираетесь использовать прототипные ячейки для этого табличного представления. Было бы прекрасно, если бы вы это сделали, и в качестве упражнения вы могли бы переписать код, чтобы использовать ячейки прототипа позже, но я хочу показать вам другой способ создания ячеек табличного представления.
- Удалите пустую ячейку прототипа из сцены «All Lists», просто выбрав ячейку табличного представления и нажав delete на клавиатуре.
- Control-drag (перетащите) значок желтого круга в верхней части сцены всех списков на сцену контрольного списка и создайте сегмент шоу Show segue.

Управление-перетаскивание из сцены Всех списков в сцену контрольного списка
Это добавляет “push” переход от экрана всех списков к экрану контрольного списка. Он также возвращает панель навигации на Checklist scene (ту, что справа).
- Дважды щелкните панель навигации на сцене Checklist scene,чтобы изменить ее название на (Name of the Checklist). Это просто текст — заполнитель (placeholder text).
Исправьте названия
- Если вы включили/отключили (enabled/disabled) большие заголовки с помощью раскадровки, то отключите большие заголовки для Checklist scene, установив для атрибута большого заголовка Navigation Item’s значение Never.
- Если вы настроили большие заголовки с помощью кода, то второй экран, Checklist, будет иметь большой заголовок, а первый — нет! Это связано с тем, что вы изначально настроили большие заголовки для ChecklistViewController.swift.
Упражнение: Можете ли вы исправить заголовки самостоятельно, чтобы большие заголовки были включены AllListsViewController.swift, а на экране Checklist не отображался большой заголовок?
Это изменение достаточно просто реализовать.
- Переместите следующие строки кода из
viewDidLoad
ChecklistViewController.swift вviewDidLoad
файла AllListsViewController.swift:
// Enable large titles
navigationController?.navigationBar.prefersLargeTitles = true
- Добавьте этот код
viewDidLoad
в ChecklistViewController.swift:
// Disable large titles for this view controller
navigationItem.largeTitleDisplayMode = .never
В каждом случае комментарии объясняют, что делает код :]
Выполните переход через код (Perform a segue via code)
Обратите внимание, что новый сегмент не привязан ни к одной кнопке или ячейке представления таблицы.
На экране «All Lists» нет ничего, на что можно было бы нажать или с чем можно было бы взаимодействовать иным образом, чтобы вызвать этот переход. Это означает, что вы должны выполнить переход программно.
- Нажмите на new segue, чтобы выбрать его, перейдите в Attributes inspector и дайте ему идентификатор ShowChecklist.
Kind переход должен быть Show (e.g. Push), потому что при выполнении этого перехода вы помещаете контроллер представления контрольного списка в стек навигации.
- В
AllListsViewController.swift
добавьтеtableView(_:didSelectRowAt:)
метод:
override func tableView(
_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath
) {
performSegue(withIdentifier: "ShowChecklist", sender: nil)
}
Напомним, что этот метод делегата табличного представления вызывается при касании строки.
Раньше нажатие на строку автоматически выполняло переход, потому что вы подключили переход к ячейке прототипа. Однако табличное представление для этого экрана не использует ячейки-прототипы. Поэтому вам придется выполнить переход вручную.
Это достаточно просто: просто позвоните performSegue(withIdentifier:sender:)
с именем сегмента, и все начнет двигаться.
- Запустите приложение. Теперь это должно выглядеть так:
. Нажатие на строку открывает экран контрольного списка (справа). «)
Первая версия экрана «All Lists» (слева). Нажатие на строку открывает экран контрольного списка (справа).
Нажмите на строку, и знакомое ChecklistViewController
изображение появится на экране.
Вы можете нажать кнопку “Назад” в левом верхнем углу, чтобы вернуться к основному списку. Теперь вы по-настоящему используете мощь навигационного контроллера!
Пользовательский интерфейс всех списков (The All Lists UI)
Вы собираетесь дублировать большую часть функций контроллера представления контрольного списка для этого нового экрана всех списков. Вверху будет кнопка +, которая позволит пользователям добавлять новые контрольные списки, они могут сделать свайп для удаления, и они могут нажать кнопку раскрытия, чтобы отредактировать название контрольного списка.
Конечно, вы также сохраните массив Checklist
объектов в файле Checklists.plist.
Как вы уже видели, как это работает, на этот раз мы пройдем по шагам немного быстрее.
Модель данных
Вы начинаете с создания объекта модели данных, который представляет собой контрольный список.
- Добавьте новый файл в проект на основе Cocoa Touch Class. Назовите его Checklist и сделайте его подклассом NSObject. Также убедитесь, что язык установлен на Swift.
Это добавляет файл Checklist.swift в проект. Точно так же ChecklistItem
, как ты строишь Checklist
на вершине NSObject
. Как вы выяснили ранее, это требование требуется, когда вам нужно сравнить объекты – для того, чтобы найти элемент списка в массиве списков.
- Дайте Checklist.swift
name
свойство:
import UIKit
class Checklist: NSObject {
var name = ""
}
Затем вы дадите массив AllListsViewController
, который будет хранить эти новые Checklist
объекты.
- Добавьте новую переменную экземпляра в AllListsViewController.swift.:
var lists = [Checklist]()
Это массив, который будет содержать Checklist
объекты.
Примечание: Вы также можете написать вышесказанное следующим образом:
var lists = Array<Checklist>()
Версия с квадратными скобками является еще одним экземпляром синтаксического сахара для полной нотации, которая является
Array<
_типом объектов, помещаемых в массив_>
.Вы увидите обе формы, используемые в программах Swift, и они делают одно и то же. Поскольку массивы используются часто, разработчики Swift включили удобную стенографию с квадратными скобками.
В качестве первого шага вы заполните этот новый массив тестовыми данными, которые будете делать viewDidLoad()
, как и раньше. Помните, что UIKit автоматически вызывает этот метод при первой загрузке контроллера представления.
Фиктивные данные (Dummy data)
В AllListsViewController.swift вы можете добавить следующее viewDidLoad()
— на самом деле не добавляйте его прямо сейчас, просто прочитайте вместе с описанием:
// 1
var list = Checklist()
list.name = "Birthdays"
lists.append(list)
// 2
list = Checklist()
list.name = "Groceries"
lists.append(list)
list = Checklist()
list.name = "Cool Apps"
lists.append(list)
list = Checklist()
list.name = "To Do"
lists.append(list)
Вы видели что-то очень похожее некоторое время назад, когда добавляли поддельные тестовые данные ChecklistViewController
. Вот что он делает шаг за шагом:
- Создайте новый
Checklist
объект, дайте ему имя и добавьте его в массив. - Вы создаете еще три
Checklist
объекта. Поскольку вы объявили локальную переменнуюlist
какvar
вместоlet
, вы можете повторно использовать ее.
Обратите внимание, как это выполняет одни и те же два шага для каждого нового Checklist
объекта, который вы создаете?
list = Checklist()
list.name = "Name of the checklist"
Вполне вероятно, что у каждого Checklist
, кого вы когда-либо создадите, тоже будет имя. Вы можете сделать это требованием, написав свой собственный init
метод, который принимает имя в качестве параметра. Тогда вы можете просто написать:
list = Checklist(name: "Name of the checklist")
- Перейдите в Checklist.swift и добавьте новый
init
метод.:
init(name: String) {
self.name = name
super.init()
}
Этот инициализатор принимает один параметр name
и помещает его в вызываемое свойство name
.
Обратите внимание, что хотя параметр и свойство названы name
— это две разные сущности. Таким образом, вы используете self.name
ссылку на свойство (или переменную экземпляра, если вы предпочитаете этот термин).
Если бы вы использовали этот код вместо этого:
init(name: String) {
name = name
super.init()
}
Тогда Свифт не поймет, что первое name
относится к собственности.
Чтобы устранить неоднозначность, вы используете self
. Напомним, что self
— относится к объекту, в котором вы находитесь, поэтому self.name
означает переменную name
текущего Checklist
объекта.
- Вернитесь к AllListsViewController.swift и добавьте следующий код в конец
viewDidLoad()
, на этот раз по-настоящему:
override func viewDidLoad() {
. . .
// Add placeholder data
var list = Checklist(name: "Birthdays")
lists.append(list)
list = Checklist(name: "Groceries")
lists.append(list)
list = Checklist(name: "Cool Apps")
lists.append(list)
list = Checklist(name: "To Do")
lists.append(list)
}
Это немного короче того, что я показывал вам раньше, и это гарантирует, что новые `Checklist` объекты теперь всегда будут иметь свое `name` свойство.
Обратите внимание, что вы не писали:
swift
var list = Checklist.init(name: «Birthdays»)
Несмотря на то , что метод назван `init`, это не обычный метод. Инициализаторы используются только для создания новых объектов, и вы пишете это как:
swift
var object = ObjectName(parameter1: value1, parameter2: value2, . . .)
В зависимости от заданных параметров Swift найдет соответствующий `init` метод и вызовет его.
Но важно понимать, что вы _можете_ написать инициализатор, используя `init` метод, как в первом примере выше — это не неверно, и ошибок компилятора не будет. Просто короче написать инициализатор, как во втором примере.
Ясно? Отлично! Давайте продолжим построение экрана всех списков.
### Отображение данных в табличном виде
- Измените `tableView(_:numberOfRowsInSection:)` метод, чтобы вернуть количество объектов в новом массиве:
swift
override func tableView(
_ tableView: UITableView,
numberOfRowsInSection section: Int
) -> Int {
return lists.count
}
Наконец, измените порядок заполнения ячеек для строк `tableView(_:cellForRowAt:)`:
swift
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: cellIdentifier,
for: indexPath)
// Update cell information
let checklist = lists[indexPath.row]
cell.textLabel!.text = checklist.name
cell.accessoryType = .detailDisclosureButton
return cell
}
- Запустите приложение. Это должно выглядеть так:

В табличном представлении отображаются объекты контрольного списка
Теперь у вас есть табличное представление с ячейками, представляющими `Checklist` объекты. Остальная часть экрана пока мало что делает, но это только начало.
#### Множество способов сделать ячейки табличного представления
Создание новой ячейки представления таблицы `AllListsViewController` немного сложнее, чем то, как это было сделано `ChecklistViewController`.
Там вы просто сделали следующее, чтобы получить новую ячейку представления таблицы:
swift
let cell = tableView.dequeueReusableCell(withIdentifier: «ChecklistItem», for: indexPath)
Но здесь у вас есть три отдельных бита кода, чтобы сделать то же самое:
swift
// At the top of the class implementation
let cellIdentifier = «ChecklistCell»
// In viewDidLoad
tableView.register(
UITableViewCell.self,
forCellReuseIdentifier: cellIdentifier)
// In tableView(_:cellForRowAt:)
let cell = tableView.dequeueReusableCell(
withIdentifier: cellIdentifier,
for: indexPath)
Вызов to `dequeueReusableCell(withIdentifier:for:)` все еще существует, за исключением того, что раньше в раскадровке была прототипная ячейка с этим идентификатором, а теперь ее нет.
Существует четыре способа создания ячеек табличного представления:
1. Использование ячеек-прототипов. Это самый простой и быстрый способ. Ты сделал это внутри `ChecklistViewController`.
2. Использование статических ячеек. Вы сделали это для экрана Добавления/редактирования элемента (Add/Edit Item). Статические ячейки ограничены экранами, на которых вы заранее знаете, какие ячейки у вас будут. Большим преимуществом статических ячеек является то, что вам не нужно предоставлять какие-либо методы источника данных (`cellForRowAt` и т. Д.).
3. Использование _nib_ file. nib (также известное как XIB) похоже на мини-раскадровку, содержащую только один настроенный `UITableViewCell` объект. Это очень похоже на использование ячеек-прототипов, за исключением того, что вы можете сделать это вне раскадровки.
4. Вручную, как то, что вы сделали выше. Это довольно похоже на то, как вы должны были это делать в первые дни iOS, но вы можете стать немного ближе к металлу (the metal), так сказать.
Когда вы создаете ячейку вручную, вы можете указать определенный **стиль ячейки**, который дает вам ячейку с предварительно настроенным макетом, который уже имеет метки и вид изображения.
Для сцены “All Lists” вы используете стиль "Default". Позже вы переключите его на “Subtitle”, что даст ячейке вторую, меньшую метку под основной меткой. Тогда вы увидите, как пройти действительно старую школу. :]
Использование стандартных стилей ячеек означает, что вам не нужно создавать свой собственный макет ячейки. Для многих приложений этих стандартных макетов достаточно, так что это экономит вам некоторую работу.
Прототипные ячейки и статические ячейки также могут использовать эти стандартные стили ячеек. Стиль по умолчанию для прототипа или статической ячейки-“Пользовательский”, который требует использования собственных меток, но вы можете изменить его на один из встроенных стилей с помощью конструктора интерфейсов.
И, наконец, мягкое предупреждение: иногда я вижу код, который создает новую ячейку для каждой строки, а не пытается повторно использовать ячейки, удаляя их из очереди. Не делай этого! Всегда используйте один из `dequeueReusableCell` методов — табличное представление автоматически создаст новую ячейку, если не сможет найти существующую для повторного использования.
Создание новой ячейки для каждой строки приведет к замедлению работы приложения, так как создание объекта происходит медленнее, чем простое повторное использование существующего объекта. Создание всех этих новых объектов также занимает больше памяти, что является драгоценным товаром на мобильных устройствах. Для достижения наилучшей производительности повторно используйте эти ячейки!
Если вы не заметили, есть два `dequeueReusableCell` варианта — о втором вы узнаете позже.
## Просмотр контрольных списков (View the checklists)
Прямо сейчас модель данных состоит `lists` массива из `AllListsViewController`, содержащего несколько `Checklist`объектов. Существует также отдельный `items` массив в `ChecklistViewController` c `ChecklistItem` объектами.
Возможно, вы заметили, что при нажатии на название списка появляется экран контрольного списка, но в настоящее время на нем всегда отображаются одни и те же элементы, независимо от того, на какой список вы нажали.
Каждый контрольный список действительно должен иметь свой собственный список дел. Вы будете работать над этим позже, так как это требует значительных изменений в модели данных.
Для начала давайте установим заголовок экрана контрольного списка таким образом, чтобы он отражал выбранный контрольный список.
### Установите заголовок экрана
- Добавьте новую переменную экземпляра в **ChecklistViewController.swift**:
swift
var checklist: Checklist!
Сейчас я объясню, зачем нужен восклицательный знак.
Изменение `viewDidLoad` в **ChecklistViewController.swift** на:
swift
override func viewDidLoad() {
. . .
title = checklist.name
}
Это изменяет заголовок экрана, который отображается на панели навигации, на имя `Checklist` объекта.
Вы передадите необходимый `Checklist` объект `ChecklistViewController` при выполнении перехода.
- В **AllListsViewController.swift** обновите `tableView(_:didSelectRowAt:)` до следующего:
swift
override func tableView(
_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath
) {
let checklist = lists[indexPath.row]
performSegue(
withIdentifier: «ShowChecklist»,
sender: checklist)
}
Как и раньше, вы используете его `performSegue()`для запуска перехода. Этот метод имеет `sender` параметр, который вы ранее установили `nil`. Теперь вы будете использовать его для отправки `Checklist` объекта из строки, на которую нажал пользователь.
Вы можете вложить в него все, что захотите `sender`. Если переход выполняется раскадровкой, а не вручную, как вы делаете здесь, то `sender` будет ссылаться на элемент управления, который его запустил, например, на `UIBarButtonItem` объект для кнопки Добавить или `UITableViewCell` для строки в таблице.
Но поскольку вы начинаете этот конкретный сегмент вручную, вы можете поместить в него все, что вам наиболее удобно `sender`.
Помещение `Checklist` объекта в `sender` параметр еще не передает его`ChecklistViewController`. Это происходит в “prepare-for-segue”, который вам все равно нужно добавить для этого контроллера представления.
- Добавьте `prepare(for:sender:)` метод в **AllListsViewController.swift**:
swift
// MARK: — Navigation
override func prepare(
for segue: UIStoryboardSegue,
sender: Any?
) {
if segue.identifier == «ShowChecklist» {
let controller = segue.destination as! ChecklistViewController
controller.checklist = sender as? Checklist
}
}
Вы уже видели этот метод раньше. `prepare(for:sender:)` вызывается непосредственно перед переходом из контроллера представления. Здесь вы получаете возможность задать свойства нового контроллера представления до того, как он станет видимым.
Внутри `prepare(for:sender:)` вам нужно передать `ChecklistViewController` объект `Checklist` из строки , на которую нажал пользователь. Вот почему вы поместили этот объект в `sender` параметр ранее. Вместо этого вы могли бы временно сохранить `Checklist` объект в переменной экземпляра, но передать его в `sender` параметре гораздо проще и чище.
Все это происходит через короткое время после `ChecklistViewController` создания экземпляра, но непосредственно перед `ChecklistViewController` загрузкой представления. Это означает, что его `viewDidLoad()` метод вызывается после `prepare(for:sender:)`.
На этом этапе свойство контроллера представления `checklist`присваивается `Checklist` объекту из `sender`и `viewDidLoad()`может соответствующим образом задавать заголовок экрана.

*Шаги, связанные с выполнением перехода*
Эта последовательность событий является причиной того, что свойство `checklist` объявляется как `Checklist!` с восклицательным знаком. Это позволяет временно сохранять его значение `nil` до тех пор, пока `viewDidLoad()` не произойдет.
`nil` обычно это недопустимое значение для необязательных переменных в Swift, но с помощью `!` вы переопределяете его.
Это очень похоже на опционы? Восклицательный знак превращается `checklist` в особый вид опциональности. Это очень похоже на опции с вопросительным знаком, но вам не нужно писать `if let`, чтобы развернуть его. Такие _неявно развернутые_ опции следует использовать экономно и осторожно, так как они не имеют такой защиты от сбоев, как обычные опции.
- Запустите приложение и обратите внимание, что когда вы касаетесь строки контрольного списка, на следующем экране правильно отображается заголовок контрольного списка.

*Название выбранного контрольного списка теперь отображается на панели навигации*
Обратите внимание, что передача `Checklist` объекта в систему `ChecklistViewController` не делает его копию. Вы только передаете контроллеру представления _ссылку_ на этот объект — любые изменения, внесенные пользователем в этот `Checklist` объект, также видны `AllListsViewController`. Оба контроллера представления имеют доступ к одному и тому же `Checklist` объекту. Позже вы воспользуетесь этим в своих интересах, чтобы добавить новое `ChecklistItems` к выбранному `Checklist`.

#### Тип слепков (Type Casts)
В `prepare(for:sender:)` сделай это:
swift
override func prepare(
for segue: UIStoryboardSegue,
sender: Any?
) {
. . .
controller.checklist = sender as? Checklist
. . .
}
Что это за `as? Checklist` кусочек?
Если вы обращали внимание — конечно, обращали! — значит, вы уже несколько раз видели, как это “как-то” используется. Это называется ==_приведением типа_==.
Приведение типа говорит Swift интерпретировать значение как имеющее другой тип данных.
Это противоположно тому, что происходит с некоторыми актерами в кино. Для них приведение типов приводит к тому, что они всегда воспроизводят один и тот же символ; в Swift приведение типов фактически изменяет характер объекта.
Здесь `sender` имеет тип `Any?`, означающий , что это может быть любой объект: `UIBarButtonItem`, `UITableViewCell` или, в данном случае, `Checklist`. Благодаря вопросительному знаку это даже может быть `nil`.
Но `controller.checklist` свойство всегда ожидает `Checklist`объект – оно не будет знать, что делать с объектом.`UITableViewCell`… Следовательно, Swift требует, чтобы вы помещали `Checklist` в `checklist` свойство только объекты.
Написав `sender as? Checklist`, вы сообщаете Swift, что он может безопасно обрабатываться `sender` как `Checklist` объект, если его можно использовать как `Checklist` объект, или отправлять `nil`, если есть проблема.
Другим примером типизации является:
swift
let controller = segue.destination as! ChecklistViewController
Свойство segue `destination` относится к контроллеру представления на приемном конце segue. Но очевидно, что инженеры Apple не могли заранее предсказать, что мы его вызовем `ChecklistViewController`. В отличие от предыдущего `as?` приведения типа, эта сила разворачивает значение того типа, который вы указали, и не должно быть никакой возможности сбоя приведения типа.
Таким образом, вы должны привести его из его общего типа (`UIViewController`) в конкретный тип, используемый в этом приложении (`ChecklistViewController`), прежде чем вы сможете получить доступ к любому из свойств, специфичных для него `ChecklistViewController`.
Не волнуйся, если что-то из этого пройдет у тебя над головой прямо сейчас. Вы увидите еще много примеров приведения типов в действии.
Основная причина, по которой вам нужны все эти приведения типов, заключается в совместимости с фреймворками iOS, написанными на Objective-C. Swift менее снисходителен к типам, чем Objective-C, и требует от вас гораздо более четкого указания типов различных элементов данных, с которыми вы работаете.

## Управление контрольными списками
Давайте быстро добавим экран добавления / редактирования контрольного списка. Это будет еще один `UITableViewController`, со статическими ячейками, и вы представите его со стороны `AllListsViewController`.
Если предыдущее предложение имело для вас смысл, то вы уже освоились с этим!
### Добавьте контроллер представления
- Добавьте новый файл в проект **ListDetailViewController.swift**. Вы можете использовать шаблон файла **Swift File** для этого, так как вы будете добавлять полную реализацию контроллера представления вручную.
- Добавьте следующее в **ListDetailViewController.swift**:
swift
import UIKit
protocol ListDetailViewControllerDelegate: AnyObject {
func listDetailViewControllerDidCancel(
_ controller: ListDetailViewController)
func listDetailViewController(
_ controller: ListDetailViewController,
didFinishAdding checklist: Checklist
)
func listDetailViewController(
_ controller: ListDetailViewController,
didFinishEditing checklist: Checklist
)
}
class ListDetailViewController: UITableViewController, UITextFieldDelegate {
@IBOutlet var textField: UITextField!
@IBOutlet var doneBarButton: UIBarButtonItem!
weak var delegate: ListDetailViewControllerDelegate?
var checklistToEdit: Checklist?
}
Я просто взял содержимое **ItemDetailViewController.swift** и изменил имена. Кроме того, вместо свойства для a `ChecklistItem` вы теперь имеете дело с `Checklist`.
- Добавьте `viewDidLoad()` метод:
swift
override func viewDidLoad() {
super.viewDidLoad()
if let checklist = checklistToEdit {
title = «Edit Checklist»
textField.text = checklist.name
doneBarButton.isEnabled = true
}
}
Это изменяет заголовок экрана, если пользователь редактирует существующий контрольный список, и помещает его имя в текстовое поле.
- Также добавьте `viewWillAppear()` метод для всплывающей клавиатуры.:
swift
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
textField.becomeFirstResponder()
}
### Кнопки Отмена и Готово
- Добавьте методы действий для кнопок Отмена и Готово:
swift
// MARK: — Actions
@IBAction func cancel() {
delegate?.listDetailViewControllerDidCancel(self)
}
@IBAction func done() {
if let checklist = checklistToEdit {
checklist.name = textField.text!
delegate?.listDetailViewController(
self,
didFinishEditing: checklist)
} else {
let checklist = Checklist(name: textField.text!)
delegate?.listDetailViewController(
self,
didFinishAdding: checklist)
}
}
Это тоже должно выглядеть знакомо. По сути, это то же самое, что делает экран добавления/редактирования элемента. Чтобы создать новый `Checklist` объект `done()`, вы используете его `init(name:)` метод и передаете содержимое `textField.text` в качестве `name` параметра.
Вы не можете написать это так, как вы это сделали для `ChecklistItems`– это не сработает:
swift
let checklist = Checklist()
checklist.name = textField.text!
Поскольку у `Checklist` нет `init()` метода, который не принимает параметров, запись `Checklist()` приводит к ошибке компилятора. У него есть только `init(name:)` метод, и вы всегда должны использовать этот инициализатор для создания новых `Checklist` объектов.
### Другие функциональные возможности
- Также убедитесь, что пользователь не может выбрать ячейку таблицы с текстовым полем:
swift
// MARK: — Table View Delegates
override func tableView(
_ tableView: UITableView,
willSelectRowAt indexPath: IndexPath
) -> IndexPath? {
return nil
}
- И, наконец, добавьте методы делегирования текстового поля, которые включают или отключают кнопку Готово в зависимости от того, пустое текстовое поле или нет.
swift
// MARK: — Text Field Delegates
func textField(
_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String
) -> Bool {
let oldText = textField.text!
let stringRange = Range(range, in: oldText)!
let newText = oldText.replacingCharacters(
in: stringRange,
with: string)
doneBarButton.isEnabled = !newText.isEmpty
return true
}
func textFieldShouldClear(_ textField: UITextField) -> Bool {
doneBarButton.isEnabled = false
return true
}
Опять же, это то же самое, что и то, что вы сделали `ItemDetailViewController`– никаких сюрпризов быть не должно.
Давайте создадим пользовательский интерфейс для этого нового контроллера представления в Interface Builder.
### Раскадровка
Откройте раскадровку. Перетащите новый **контроллер табличного представления** из библиотеки объектов на холст и переместите его под другие контроллеры представлений.

*Добавление нового контроллера табличного представления на холст*
- Выберите новый контроллер табличного представления и перейдите в **Identity inspector**. Измените его класс на **ListDetailViewController**.
- **Control-drag** из желто го круга в верхней части сцены All Lists в новую сцену. Выберите **Show** в разделе Manual Segue всплывающего меню.
- Установите заголовок панели навигации для **Add Checklist**. Теперь новая сцена должна появиться в контуре документа как сцена добавления контрольного списка.
- Выберите Navigation Item и установите **Large Title** в Attributes inspector **значение Никогда** > **Never** — это избавит вас от необходимости добавлять код в контроллер представления, чтобы отключить большие заголовки.
- Добавьте элементы кнопок панели Отмена и Готово к navigation item и подключите их к методам действий в сцене Add Checklist. Также подключите кнопку Done к розетке doneBarButton и **снимите флажок** Enabled.
Помните, что вы можете управлять перетаскиванием с кнопки на контроллер представления, чтобы подключить его к методу действия. Чтобы подключить розетку, сделайте это наоборот: Control-drag ее с контроллера вида на кнопку.
Измените представление таблицы на **Static Cells**, сгруппированные по стилю **Grouped**. Вам нужна только одна ячейка, поэтому удалите две нижние.
Поместите новое **Text Field** в ячейку, отрегулируйте его размер и положение, а затем установите **ограничения автоматической компоновки слева, сверху**, **справа**и **снизу**. Затем задайте следующие параметры конфигурации с помощью инспектора **атрибутов**:
- Размер шрифта: **17**
- Текст заполнителя: **Имя списка**
- Стиль границы: **нет**
- (Необязательно) Кнопка Очистить: **появляется при редактировании**
- Подстроиться под себя: **отключено**
- Заглавные буквы: **Предложения**
- Ключ возврата: **Готово**
- Автоматическое включение клавиши возврата: **проверено**
- Control-перетащите из контроллера представления в текстовое поле и подключите его к выходу текстового поля.
- Затем Control-перетащите наоборот, из текстового поля обратно в контроллер представления, и выберите **делегат** в разделе **Розетки**. Теперь контроллер представления является делегатом для текстового поля.
- Подключите **событие Did End on Exit текстового поля** к действию done на контроллере представления.
Это завершит настройку нового контроллера представления в качестве экрана добавления / редактирования контрольного списка:

*Готовый дизайн ListDetailViewController*
### Подключите контроллеры просмотра
- Перейдите на сцену **All Lists** (ту, что называется "Checklist") и перетащите **Bar Button Item** в правый навигационный элемент. Измените его на кнопку Добавить.
- **Control-drag** эту кнопку с новой панели на сцену Добавить контрольный список ниже, чтобы добавить новый сегмент **Show**.
- Нажмите на новый сегмент и назовите его **AddChecklist**.
- Нажмите на другой сегмент (тот, который не подключен к кнопке Добавить) и назовите его **EditChecklist**.
Теперь ваша раскадровка должна выглядеть примерно так:

*Полная раскадровка: 1 навигационный контроллер, 4 контроллера табличного представления*
### Настройка делегатов
Почти приехали. Вам все равно нужно сделать делегата `AllListsViewController` для `ListDetailViewController`, и тогда все готово. Опять же, это очень похоже на то, что вы делали раньше.
- Объявите контроллер представления All Lists соответствующим протоколу делегирования, добавив `ListDetailViewControllerDelegate`его в строку класса.
Вы делаете это в **AllListsViewController.swift**:
swift
class AllListsViewController: UITableViewController, ListDetailViewControllerDelegate {
- Все еще в **AllListsViewController.swift**, расширяется `prepare(for:sender:)` до:
swift
override func prepare(
for segue: UIStoryboardSegue,
sender: Any?
) {
if segue.identifier == «ShowChecklist» {
. . .
} else if segue.identifier == «AddChecklist» {
let controller = segue.destination as! ListDetailViewController
controller.delegate = self
}
}
Первое `if` не меняется. Вы добавили секунду `if` для нового сегмента “AddChecklist”, который вы только что определили в раскадровке.
Как и раньше, вы ищете контроллер представления и устанавливаете его `delegate` свойство равным `self`.
- Затем реализуйте следующие методы делегата в **AllListsViewController.swift**:
swift
// MARK: — List Detail View Controller Delegates
func listDetailViewControllerDidCancel(
_ controller: ListDetailViewController
) {
navigationController?.popViewController(animated: true)
}
func listDetailViewController(
_ controller: ListDetailViewController,
didFinishAdding checklist: Checklist
) {
let newRowIndex = lists.count
lists.append(checklist)
let indexPath = IndexPath(row: newRowIndex, section: 0)
let indexPaths = [indexPath]
tableView.insertRows(at: indexPaths, with: .automatic)
navigationController?.popViewController(animated: true)
}
func listDetailViewController(
_ controller: ListDetailViewController,
didFinishEditing checklist: Checklist
) {
if let index = lists.firstIndex(of: checklist) {
let indexPath = IndexPath(row: index, section: 0)
if let cell = tableView.cellForRow(at: indexPath) {
cell.textLabel!.text = checklist.name
}
}
navigationController?.popViewController(animated: true)
}
Эти методы вызываются, когда пользователь нажимает Cancel или Done на экране new Add/Edit Checklist.
Ничто из этого не должно сбивать с толку — это именно то, что вы делали раньше, но теперь для объектов `ListDetailViewController` и `Checklist`.
- Также добавьте метод источника данных табличного представления, который позволяет пользователю удалять контрольные списки:
swift
override func tableView(
_ tableView: UITableView,
commit editingStyle: UITableViewCell.EditingStyle,
forRowAt indexPath: IndexPath
) {
lists.remove(at: indexPath.row)
let indexPaths = [indexPath]
tableView.deleteRows(at: indexPaths, with: .automatic)
}
- Запустите приложение. Теперь вы можете добавлять новые контрольные списки и удалять их снова:

*Добавление новых списков*
> **Примечание:** Если приложение выйдет из строя, вернитесь назад и убедитесь, что вы правильно установили все соединения в Interface Builder. Очень легко пропустить всего одну крошечную вещь, но даже самая крошечная ошибка может привести к тому, что приложение рухнет в огне…
Однако вы еще не можете редактировать имена существующих списков. Это требует последнего дополнения к коду.
Чтобы открыть экран редактирования контрольного списка, пользователь нажимает синюю кнопку аксессуара в `ChecklistViewController` том месте, где произошел переход. Здесь тоже не помешал бы переход. Если вы хотите пойти по этому пути, вы уже настроили сегмент с именем “EditChecklist” на раскадровке, который можно использовать для этой цели. Но я хочу показать тебе другой способ.
На этот раз вы вообще не будете использовать segue, а загрузите новый контроллер представления вручную из раскадровки. Просто потому, что вы можете — и потому, что хорошо знать несколько способов сделать одно и то же.
### Загрузить контроллер представления с помощью кода (Load a view controller via code)
- Добавьте следующий `tableView(_:accessoryButtonTappedForRowWith:)`метод в **AllListsViewController.swift**. Этот метод исходит из протокола делегирования табличного представления, и название, надеюсь, достаточно очевидно, чтобы вы догадались, что он делает.
swift
override func tableView(
_ tableView: UITableView,
accessoryButtonTappedForRowWith indexPath: IndexPath
) {
let controller = storyboard!.instantiateViewController(
withIdentifier: «ListDetailViewController») as! ListDetailViewController
controller.delegate = self
let checklist = lists[indexPath.row]
controller.checklistToEdit = checklist
navigationController?.pushViewController(
controller,
animated: true)
}
«`
В этом методе вы создаете объект контроллера представления для экрана добавления/редактирования контрольного списка и помещаете его в стек навигации. Это примерно эквивалентно тому, что segue будет делать за кулисами. Контроллер представления встроен в раскадровку, и вы должны попросить объект раскадровки загрузить его.
Где вы взяли этот объект раскадровки? Как это бывает, каждый контроллер представления имеет storyboard
свойство, которое ссылается на раскадровку, с которой был загружен контроллер представления. Вы можете использовать это свойство для выполнения любых действий с раскадровкой, например для создания экземпляров других контроллеров представлений.
Это storyboard
свойство является необязательным, поскольку контроллеры представлений не всегда загружаются из раскадровки. Но это так, и именно поэтому вы можете использовать !
_принудительное разворачивание_ опции. Это похоже на использование if let
, но поскольку вы можете с уверенностью предположитьstoryboard
, что его не будет nil
в этом приложении, вам не нужно разворачивать его внутри if
оператора.
Вызов to instantiateViewController(withIdentifier:)
принимает строку идентификатора, ListDetailViewController
. Именно так вы просите раскадровку создать новый контроллер представления. В вашем случае это будет так ListDetailViewController
. Обратите внимание, что идентификатор не обязательно должен совпадать с именем класса контроллера представления — это может быть любое уникальное строковое значение, — хотя мы решили использовать здесь имя класса контроллера представления.
Вы все равно должны установить этот идентификатор на навигационном контроллере, иначе раскадровка не сможет его найти. И если вы попытаетесь запустить приложение без установки идентификатора, оно выйдет из строя.
- Откройте раскадровку и выберите контроллер подробного представления списка List Detail View Controller. Перейдите в Identity inspector и установите Storyboard ID в ListDetailViewController:

Установка идентификатора раскадровки
- Это должно сработать. Запустите приложение и нажмите несколько кнопок раскрытия деталей.
Если приложение выйдет из строя, убедитесь, что раскадровка сохранена, прежде чем нажимать кнопку Выполнить.
Ты все еще со мной?
Если в этот момент ваши глаза остекленели и вам хочется сдаться: не надо. Изучать новые вещи трудно, а программировать-вдвойне. Отложите книгу в сторону, поспите на ней и приходите через несколько дней.
Скорее всего, тем временем у вас будет а-ха! момент, когда то, что не имело никакого смысла, вдруг становится ясным как день.