Программирование
Делаем разделы UITableView с вложенными типами
В сегодняшней статье я покажу вам практический пример использования вложенных (nested) типов, создавая UITableView с несколькими разделами.
В сегодняшней статье я покажу вам практический пример использования вложенных (nested) типов, создавая UITableView с несколькими разделами.
Я собираюсь написать вики о Гарри Поттере, которая покажет пользователю некоторую основную информацию о персонажах, местах и классах Хогвартса. Итак, наш список будет состоять из трех разделов, каждый из которых будет содержать список элементов.
Вот каким будет окончательный результат:
Очень простым подходом было бы создание трех объектов, каждый со своим массивом.
При реализации основных функций UITableViewDataSource вам нужно будет считать индексы IndexPath, создав что-то вроде этого:
func numberOfSections(in tableView: UITableView) -> Int { | |
3 | |
} | |
func tableView(_ tableView: UITableView, | |
numberOfRowsInSection section: Int) -> Int { | |
if indexPath.section == 0 { | |
// return something... | |
} else if indexPath.section == 1 { | |
// return something different... | |
} else if indexPath.section == 2 { | |
// return something different again... | |
} | |
} | |
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | |
if indexPath.section == 0 { | |
// Do stuff... | |
} else if indexPath.section == 1 { | |
// Do other stuff... | |
} else if indexPath.section == 2 { | |
// Do other different stuff... | |
} | |
// ... | |
} |
Такой подход имеет один большой недостаток. Что произойдет, если вы решите изменить порядок разделов?
В таком сценарии вам придется изменить весь код, что сделает практически невозможным создание динамической таблицы, например, с использованием Server-Driven UI.
Гораздо более разумным решением было бы использовать перечисление (enums) для описания всех наших возможных разделов. Еще лучше, что мы можем использовать связанные значения для наших случаев. Но что такое связанные значения в перечислениях? Официальная документация Swift гласит:
…иногда полезно иметь возможность хранить значения других типов вместе с этими значениями перечислений (кейсами, case). Эта дополнительная информация называется ассоциированным значением (associated value) и меняется каждый раз, когда вы используете case в качестве значения в своем коде.
Вы можете использовать перечисления Swift для хранения связанных значений любого заданного типа, и типы значений могут быть разными для каждого случая перечисления, если это необходимо. Перечисления, подобные этим, известны как размеченные объединения (discriminated unions), объединения с тегами (tagged unions) или варианты (variants) в других языках программирования.
По сути, вы можете добавить объект любого типа в перечисления… и эта вещь чрезвычайно полезна в разделах TableView. Давайте посмотрим, как мы можем создать перечисление Section:
class ViewController: UIViewController { | |
enum Section { | |
case character(items: [Character]) | |
case location(items: [Location]) | |
case course(items: [Course]) | |
var title: String { | |
switch self { | |
case .character : return "Characters" | |
case .location. : return "Locations" | |
case .course. : return "Courses" | |
} | |
} | |
} | |
// more to come... | |
} |
Как видите, я создал перечисление с нашими секциями в качестве кейсов.
У каждого варианта есть связанное значение, которое представляет собой массив объектов в этом разделе.
Я также добавил вычисляемую переменную для заголовка самого раздела. Обычно я предпочитаю добавлять вычисляемую переменную, а не прямое значение для подобных вещей. Так понятнее.
Вы могли заметить, что перечисление является вложенным типом класса ViewController. Я запрограммировал так, потому что эти разделы принадлежат только этому классу. Мы не будем использовать их в других контроллерах нашего приложения — поэтому вложенный тип лучше представляет эту связь.
Идеально.
Вторым шагом будет создание источника данных для нашего UITableView.
Для этого мы будем использовать массив Section! Да, массив перечислений.
Давайте создадим функцию, способную это сделать:
private extension ViewController { | |
func createDataSource() -> [Section] { | |
let charactersItm = Section.character(items: characters) | |
let locationsItm = Section.location(items: locations) | |
let coursesItm = Section.course(items: courses) | |
return [charactersItm, locationsItm, coursesItm] | |
} | |
} |
Обратите внимание, что персонажи, локации и курсы были созданы ранее. Их также можно получить с сервера… но для простоты я прописал их в своем коде:
privare var characters = Character.exampleList
Где наш Character представляет собой следующую структуру:
struct Character { | |
enum SideType: String { | |
case light = "Light" | |
case dark = "Dark" | |
case unknown = "Unknown" | |
} | |
var name: String | |
var side: SideType | |
var image: String | |
} | |
extension Character { | |
static var exampleList: [Character] { | |
let char01 = Character(name: "Harry Potter", side: .light, image: "harryPotter") | |
let char02 = Character(name: "Lord Voldemort", side: .dark, image: "lordVoldemort") | |
return [char01, char02] | |
} | |
} |
Как вы можете видеть в структуре, я использовал вложенные типы по той же причине.
Хорошо, с нашим массивом секций пора реализовать функции UITableViewDataSource и мы увидим, что с перечислением все будет супер чисто и супер понятно:
extension ViewController: UITableViewDataSource { | |
func numberOfSections(in tableView: UITableView) -> Int { | |
dataSource.count | |
} | |
func tableView(_ tableView: UITableView, | |
numberOfRowsInSection section: Int) -> Int { | |
switch dataSource[section] { | |
case .character(let items) : return items.count | |
case .location(let items) : return items.count | |
case .course(let items) : return items.count | |
} | |
} | |
func tableView(_ tableView: UITableView, | |
cellForRowAt indexPath: IndexPath) -> UITableViewCell { | |
let section = dataSource[indexPath.section] | |
switch section { | |
case .character(let items): | |
let cell = tableView.dequeueReusableCell( | |
withIdentifier: CellIdentifier.character, | |
for: indexPath | |
) as? CharacterCell | |
let item = items[indexPath.row] | |
cell?.character = item | |
return cell ?? UITableViewCell() | |
case .location(let items): | |
let cell = tableView.dequeueReusableCell( | |
withIdentifier: CellIdentifier.location, | |
for: indexPath | |
) as? LocationCell | |
let item = items[indexPath.row] | |
cell?.location = item | |
return cell ?? UITableViewCell() | |
case .course(let items): | |
let cell = tableView.dequeueReusableCell( | |
withIdentifier: CellIdentifier.course, | |
for: indexPath | |
) as? CourseCell | |
let item = items[indexPath.row] | |
cell?.course = item | |
return cell ?? UITableViewCell() | |
} | |
} | |
func tableView(_ tableView: UITableView, | |
titleForHeaderInSection section: Int) -> String? { | |
dataSource[section].title | |
} | |
} |
Давайте проанализируем методы один за другим.
- Функция numberOfSections просто возвращает размер массива. Если мы добавляем или удаляем элементы в списке, разделы всегда будут синхронизированы. Очень круто!
- Функция numberOfRowsInSection имеет переключатель для перечисления, охватывающий все случаи и просто возвращающий размер связанного массива. Как видите, даже здесь, если вы измените количество или порядок ваших разделов, вам не нужно ничего менять.
- Аналогичный подход реализован в функции cellForRowAt. Использование Switch покрывает все случаи и создает ячейки. Обратите внимание на cellIdentifier… мы вернемся к нему позже.
- Последний метод, titleForHeaderInSection, создает заголовок раздела, используя вычисленную переменную перечисления.
Супер просто!
Теперь все наши секции динамические и мы можем их менять просто в функции createDatasource не трогая код где-либо ещё! И это действительно здорово!
Ранее я обращал ваше внимание на идентификатор ячейки… как вы знаете, это постоянная строка, поэтому мы могли бы использовать и вложенные типы, создав такую структуру:
struct CellIdentifier { | |
static var character = "CharacterCell" | |
static var location = "LocationCell" | |
static var course = "CourseCell" | |
} |
С такой структурой у нас будут все постоянные строки в одном месте, и поддерживать весь код будет намного проще.
Приведенный выше метод позволяет нам настроить динамическую логику для всего источника данных.
Например, нашему пользователю может быть разрешено просматривать определенный раздел или нет. Может быть, бэкэнд может управлять этим. Или, может быть, пользователю нужны особые привилегии.
С помощью простой модификации функции createDatasource мы можем создать эту логику. Перепишем нашу функцию:
private extension ViewController { | |
func createDataSource() -> [Section] { | |
var sections = [Section]() | |
if isCharactersEnabled { | |
sections.append(Section.character(items: characters)) | |
} | |
if isLocationsEnabled { | |
sections.append(Section.location(items: locations)) | |
} | |
if isCoursesEnabled { | |
sections.append(Section.course(items: courses)) | |
} | |
return sections | |
} | |
} |
Вот и все!
А теперь я могу показать вам весь код класса контроллера:
import UIKit | |
class ViewController: UIViewController { | |
// MARK: - Nested Types | |
enum Section { | |
case character(items: [Character]) | |
case location(items: [Location]) | |
case course(items: [Course]) | |
var title: String { | |
switch self { | |
case .character : return "Characters" | |
case .location : return "Locations" | |
case .course. : return "Courses" | |
} | |
} | |
} | |
struct CellIdentifier { | |
static var character = "CharacterCell" | |
static var location = "LocationCell" | |
static var course = "CourseCell" | |
} | |
// MARK: - Properties | |
@IBOutlet weak var tableView: UITableView! | |
private var isCharactersEnabled = true | |
private var isLocationsEnabled = true | |
private var isCoursesEnabled = true | |
private var characters = Character.exampleList | |
private var locations = Location.exampleList | |
private var courses = Course.exampleList | |
private var dataSource = [Section]() | |
// MARK: - Lifecycle | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
title = "Wizarding World Wiki" | |
tableView.dataSource = self | |
dataSource = createDataSource() | |
} | |
} | |
// MARK: - Private | |
private extension ViewController { | |
func createDataSource() -> [Section] { | |
var sections = [Section]() | |
if isCharactersEnabled { | |
sections.append(Section.character(items: characters)) | |
} | |
if isLocationsEnabled { | |
sections.append(Section.location(items: locations)) | |
} | |
if isCoursesEnabled { | |
sections.append(Section.course(items: courses)) | |
} | |
return sections | |
} | |
} | |
// MARK: - UITableViewDataSource | |
extension ViewController: UITableViewDataSource { | |
func numberOfSections(in tableView: UITableView) -> Int { | |
dataSource.count | |
} | |
func tableView(_ tableView: UITableView, | |
numberOfRowsInSection section: Int) -> Int { | |
switch dataSource[section] { | |
case .character(let items) : return items.count | |
case .location(let items) : return items.count | |
case .course(let items) : return items.count | |
} | |
} | |
func tableView(_ tableView: UITableView, | |
cellForRowAt indexPath: IndexPath) -> UITableViewCell { | |
let section = dataSource[indexPath.section] | |
switch section { | |
case .character(let items): | |
let cell = tableView.dequeueReusableCell( | |
withIdentifier: CellIdentifier.character, | |
for: indexPath | |
) as? CharacterCell | |
let item = items[indexPath.row] | |
cell?.character = item | |
return cell ?? UITableViewCell() | |
case .location(let items): | |
let cell = tableView.dequeueReusableCell( | |
withIdentifier: CellIdentifier.location, | |
for: indexPath | |
) as? LocationCell | |
let item = items[indexPath.row] | |
cell?.location = item | |
return cell ?? UITableViewCell() | |
case .course(let items): | |
let cell = tableView.dequeueReusableCell( | |
withIdentifier: CellIdentifier.course, | |
for: indexPath | |
) as? CourseCell | |
let item = items[indexPath.row] | |
cell?.course = item | |
return cell ?? UITableViewCell() | |
} | |
} | |
func tableView(_ tableView: UITableView, | |
titleForHeaderInSection section: Int) -> String? { | |
dataSource[section].title | |
} | |
} |
Надеюсь, вам понравилась эта статья. Удачного программирования и спасибо за чтение.
-
Программирование3 недели назад
Конец программирования в том виде, в котором мы его знаем
-
Видео и подкасты для разработчиков7 дней назад
Как устроена мобильная архитектура. Интервью с тех. лидером юнита «Mobile Architecture» из AvitoTech
-
Магазины приложений3 недели назад
Магазин игр Aptoide запустился на iOS в Европе
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2025.8