Connect with us

Программирование

Делаем разделы UITableView с вложенными типами

В сегодняшней статье я покажу вам практический пример использования вложенных (nested) типов, создавая  UITableView с несколькими разделами.

Опубликовано

/

     
     

В сегодняшней статье я покажу вам практический пример использования вложенных (nested) типов, создавая  UITableView с несколькими разделами.

Я собираюсь написать вики о Гарри Поттере, которая покажет пользователю некоторую основную информацию о персонажах, местах и классах Хогвартса. Итак, наш список будет состоять из трех разделов, каждый из которых будет содержать список элементов.

Вот каким будет окончательный результат:

Делаем разделы 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
}
}

Надеюсь, вам понравилась эта статья. Удачного программирования и спасибо за чтение.

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: