Сегодня вы узнаете, как работать с WatchKit API для настройки таблиц
Таблицы, таблицы, таблицы
При работе с такими структурами данных, как массивы или словари, необходимо иметь возможность динамически обрабатывать большие наборы данных. Зачастую, таблицы являются лучшим способом для отображения таких коллекций.
Настройки, новостные ленты, списки задач- все они базируются на таблицах, механизм настройки которых довольно-таки простой и индивидуальный от задачи к задаче.
При написании приложения для AppleWatch у вас несомненно появится такой же сценарий по отображению данных. Давайте посмотрим, в чем особенности таблиц в WatchKit.
iOS — UITableView & WatchKit — WKInterfaceTable
Альтернативой класса UITableView в WatchKit является класс WKInterfaceTable. Оба механизма похожи между собой по назначению- отображение коллекций данных. Но на этом сходства заканчиваются. Например, WKInterfaceTable не позволяет использовать несколько секций- весь интерфейс состоит только лишь из одной секции.
WKInterfaceTable прекрасно работает со storyboard’ами. После того, как таблица из storyboard’а будет связана с IBOutlet’ом, нужно просто указать количество ячеек по идентификатору из storyboard’а:
table.setNumberOfRows(10, withRowType: "Identifier")
Никаких delegate’ов и dataSource’ов. Все предельно унифицировано и упрощено.
Пример
Давайте опробуем на практике механизм работы с таблицами в WatchKit. Создадим приложение, которое будет отображать список участников команды Cocoa-beans и их статьи.
Откроем файл Interface.Storyboard и перетащим на контроллер объект Table из Object Library.
Каждая ячейка в таблице представляет собой объект класса WKInterfaceGroup. Убедиться в этом вы сможете, если кликните на placeholder «Table Row» и посмотрите в Identity Inspector.
О том, как работать с группами, можно узнать в этой статье.
Перетащим два label’а в ячейку таблицы. По дефолту элементы в ячейке выравниваются по горизонтали, но нам необходимо вертикальное выравнивание. Также настроим размер нашей ячейки так, чтобы он высчитывался, исходя из размеров содержимого. Наведём еще немного красоты: у верхнего label’а выставим в 0 количество строк, а у нижнего label’а изменим шрифт на Footnote и цвет- на Light Gray Color.
Подобно тому, как каждая ячейка в UITableView должна быть наследником класса UITableViewCell, WKInterfaceTable требует, чтобы мы создавали row controller для представления каждой ячейки. В row controller находятся IBOutlet’ы и IBAction’ы, которые связаны с ячейкой в storyboard’е. А WKInterfaceController, в котором находится таблица, отвечает за конфигурацию ячеек.
Добавим новый класс в AW_TableViewDemo WatchKit Extension. Назовём его MemberRowController и унаследуемся от NSObject.
Все объекты интерфейса в WatchKit являются прокси-объектами, а не представлениями. Поэтому нет никакого класса RowController.
В storyboard’е выберем Table Row Controller в document outline. В Identity Inspector’е установим для него наш класс MemberRowController. А в Attributes Inspector’е зададим идентификатор MemberRowType.
Для каждого label’а создадим в классе MemberRowController IBOutlet: nameLbl — для верхнего, skillsLbl — для нижнего. Также, привяжем нашу таблицу к InterfaceController’у.
Как упоминалось ранее, в отличие от UITableView, у WKInterfaceTable нет ни delegate’а, ни dataSource’а, которые необходимо было бы реализовать. Вместо этого есть два главных метода, которые нужно использовать для добавления и отображения данных:
- setNumberOfRows(_ numberOfRows: Int, withRowType rowType: String) — определяет количество строк в таблице, а также идентификатор для row controller’а. Используйте этот метод, если все строки в таблице имеют одинаковый идентификатор;
- rowController(at index: Int) -> Any? — возвращает row controller по индексу.
Откроем наш InterfaceController и вызовем эти два метода:
class InterfaceController: WKInterfaceController {
@IBOutlet var table: WKInterfaceTable!
// 1
let team = Team()
override func awake(withContext context: Any?) {
super.awake(withContext: context)
// 2
table.setNumberOfRows(team.members.count, withRowType: "MemberRowType")
// 3
for (index, member) in team.members.enumerated() {
let controller = table.rowController(at: index) as! MemberRowController
controller.nameLbl.setText(member.name)
controller.skillsLbl.setText(member.skills)
}
}
}
// 1 — объявляем свойство team, в котором содержатся все участники команды;
// 2 — говорим таблице, что количество строк для типа нашего идентификатора из storyboard’а будет равно количеству участников команды;
// 3 — пробегаемся по всем участникам: создаём контроллер по индексу и записываем в label’ы имя и навыки участника.
Запустим и посмотрим, что получилось:
Давайте теперь добавим переходы: по тапу на ячейку с участником команды будет открываться новый экран со списком его статей.
Откроем storyboard и перетащим на сцену новый interface controller. В document outline выберем row controller с идентификатором MemberRowController и перетащим правой кнопки мыши курсор на новый контроллер. Появилось модальное окно. Выбираем «push».
Мы создали segue между нашими двумя контроллерами.
Строки WKInterfaceTable умеют инициировать навигационные события, когда их кто-то выбирает(собственно, как и UITableViewCell).
Добавим в InterfaceController следующий метод:
override func contextForSegue(withIdentifier segueIdentifier: String,
in table: WKInterfaceTable,
rowIndex: Int) -> Any? {
return team.members[rowIndex]
}
Переопределение этого метода позволяет определить, какие данные передавать новому контроллеру (через awake(withContext:)), когда срабатывает segue от выбранной секции. Этот метод очень удобен, потому что сразу предоставляет нам всю необходимую информацию: идентификатор segue и индекс выбранной секции. Сейчас у нас есть секции и segue только одного типа, поэтому можно не беспокоиться о безопасном срабатывании метода.
Вернемся в storyboard и настроим наш новый контроллер. Перетащим в него таблицу и зададим для неё 2 прототипа в Attributes Inspector. Первая секция будет служить заголовком- имя участника. Вторая- название статьи. Перетащим label в первую секцию, сделаем количество строк равное 0, и шрифт изменим на Headline. Во вторую секцию тоже добавим label и количество строк также сделаем равным 0.
Выберем группу первой секции и установим прозрачный фон для того, чтобы заголовок выделялся из списка статьей. Для обеих групп установим Size Height в Size To Fit Content. Контроллер примет следующий вид:
Теперь нам необходимо создать контроллеры для каждой из секции. Назовём их MemberHeaderController и MemberArticleController соответсвенно.
В storyboard’е изменим для каждой секции родительские классы на только что созданные. А также идентификаторы на MemberHeaderType и MemberArticleType. Снимем галочку с поля Selectable, так как не хотим нажимать на эти секции. Открыв Assistant editor, создадим для label’ов IBOutlet’ы— nameLbl и articleLbl.
Теперь нам нужен класс для нашего нового контроллера. Назовем его MemberDetailInterfaceController. Также установим его для нашего контроллера в storyboard’е. И добавим IBOutlet для таблицы.
Теперь пора написать код, отвечающий за заполнение таблицы. Перейдем в MemberDetailInterfaceController:
override func awake(withContext context: Any?) {
super.awake(withContext: context)
// 1
if let member = context as? Member {
// 2
let rowTypes: [String] = ["MemberHeaderType"] + member.articles!.map({ _ in
"MemberArticleType"
})
// 3
table.setRowTypes(rowTypes)
for i in 0..<table.numberOfRows {
let row = table.rowController(at: i)
// 4
if let header = row as? MemberHeaderController {
header.nameLbl.setText(member.name)
} else if let article = row as? MemberArticleController {
article.articleLbl.setText("\(i). \(member.articles![i - 1])")
}
}
}
}
// 1 — проверяем, что передаётся нам именно объект Member;
// 2 — создаём массив строк, которые являются идентификаторами будущих секций;
// 3 — метод setRowTypes(_ rowTypes:) позволяет создавать контроллеры секций на основе массива идентификаторов;
// 4 — в зависимости от типа контроллера заполняем необходимы поля.
Запустим проект и убедимся, что всё правильно работает.
MULTIPLE SECTION
Как уже отмечалось ранее, WKInterfaceTable не имеет в своем API delegate’ов и datasource’ов, которые бы позволили гибко настраивать таблицу. Другими словами, методы WKInterfaceTable работают с одномерными списками данных(массивами) — у нас нет возможности работать со вложенными структурами. Разумеется, чтобы обойти данное ограничение, нужно преобразовывать многомерные структуры в плоские. Давайте посмотрим, как это можно сделать.
Есть три метода добавления данных в таблицу:
- setRowTypes(_ rowTypes: [String]) — позволяет передавать массивом идентификаторы строк;
- setNumberOfRows(_ numberOfRows: Int, withRowType rowType: String) — вставляет строки, имеющие одни и те же идентификаторы. Этот метод не подходит для создания разделов;
- insertRows(at rows: IndexSet, withRowType rowType: String) — вставляет строку определенного типа по NSIndexSet. Нет никаких ограничений на количество раз, когда можно вызывать этот метод. Отвечает только за вставку ячейки, но не за её настройку.
Перед тем, как начать добавлять новые ячейки, давайте изменим наш интерфейс. Выберем в srotyboard’е таблицу в InterfaceController и сделаем ей количество прототипов равное двум. Перенесем в новую группу image и label. Картинке зададим фиксированные высоту и ширину, а у label’а изменим шрифт на Headline. Также сделаем прозрачным background группы. Теперь контроллер примет вид:
Создадим для новой ячейки новый контроллер и назовём его TeamHeaderController. Установим его классом нашему контроллеру секции, изменим идентификатор на TeamHeaderType и снимем галочку Selectable. А также добавим два IBOutlet’а для image и label’а. Теперь контроллер готов и можно приступить к добавлению ячеек.
Мы будем использовать метод insertRows(at:withRowType:) для добавления и настройки секций. Добавим следующий метод в InterfaceController:
func addRow(withType type: String, members: [Member]) {
// 1
let rows = table.numberOfRows
// 2
table.insertRows(at: NSIndexSet(index: rows) as IndexSet, withRowType: "TeamHeaderType")
// 3
let itemRow = NSIndexSet(indexesIn: NSRange(location: rows + 1, length: members.count))
table.insertRows(at: itemRow as IndexSet, withRowType: "MemberRowType")
for i in rows..<table.numberOfRows {
// 4
let controller = table.rowController(at: i)
// 5
if let controller = controller as? TeamHeaderController {
controller.image.setImageNamed(type)
controller.label.setText(type)
// 6
} else if let controller = controller as? MemberRowController {
let member = members[i - rows - 1]
controller.nameLbl.setText(member.name)
controller.skillsLbl.setText(member.skills)
}
}
}
// 1 — так как функцию addRow мы будем вызывать несколько раз, перед добавлением новых строк нам необходимо знать текущее количество секций в таблице;
// 2 — вставляем нашу заголовочную ячейку;
// 3 — вставляем ячейку участника после заголовка;
// 4 — перебираем все строки, которые уже добавлены в таблицу, и выбираем контроллер по индексу;
// 5 — если контроллером является объект TeamHeaderController, то устанавливаем картинку и текст для заголовка;
// 6 — если контроллер является объектом MemberRowController, то устанавливаем имя и навыки.
Теперь заменим предыдущую реализацию метода awake(withContext context: Any?):
override func awake(withContext context: Any?) {
super.awake(withContext: context)
// 1
var map = [String: [Member]]()
for member in team.members {
var arr = map[member.post] ?? [Member]()
arr.append(member)
map[member.post] = arr
}
// 2
for (post, members) in map {
addRow(withType: post, members: members)
}
// 1 — создаем словарь для систематизации участников команды по их направлениям;
// 2 — добавляем каждую секцию в соответствии с направлениям и участниками из нашего только что созданного словаря.
Теперь приложение должно выглядеть более привлекательно:
На сегодня на этом всё.
Исходный код примера можно скачать здесь.
Спасибо за внимание.
Автор снимка: paulbanday
Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.