Expanded TableView Cells

Туториалы

Expanded TableView Cells

Доброго времени, друзья! 

UITableView является основополагающими для понимания разработчиками UIKit и разработки iOS. 

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

Нужно ли использовать подобную возможность UITableView или нет, зависит от характера разрабатываемого вами приложения. Однако, поскольку ячейки могут быть настроены путем создания подкласса класса UITableViewCell и создания дополнительных файлов XIB, внешний вид приложения обычно не должен быть проблемой. Итак, в конце концов, это просто вопрос требований.

Обратите внимание  то, что вы увидите здесь, не является “серебряной пулей” или панацеей. К сожалению в мире программирования такого не бывает и существует миллионы разных реализаций подобного функционала. Но моя цель здесь - предоставить довольно-таки общий способ, который вы сможете использовать в большинстве случаев. И так поехали! 

Подготовление демо проекта

С нуля создайте новый проект на XCode. В Мain.storyboard в ViewController перетащите UITableView и растяните по краям. Далее нам нужно создать ссылку на объект UITableView в нашем коде, чтобы управлять его функциональностью. А также подписать ViewController на delegate и dataSource UITableView. Это можно сделать как в коде в методе viewDidLoad(), так и Interface Builder. И нам нужно не забыть дать identifier для нашего UITableViewCell. В этом примере я буду использовать базовый UITableViewCell с identifier - “cell”.

На данный момент, если запустить приложение, то нам покажет ошибку, несмотря на все наши усилия. Оказывается, у нас есть еще один шаг в нашем коде, прежде чем мы сможем увидеть пустой UITableView. Нам нужно реализовать протоколы delegate и dataSource!

Для начала наша “программа минимум” это 3 метода:

  • numberOfSection(in tableView: UITableView) -> Int (этот метод настраивает количество секций в UITableView
  • tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int (этот метод настраивает количество строк в каждой из секций в UITableView)
  • tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell (этот метод настраивает непосредственно саму ячейку, которая будет отображаться в UITableView
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    @IBOutlet weak var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    // MARK: UITableViewDelegate, UITableViewDataSource
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return 4
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 4
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = "Section: \(indexPath.section), row: \(indexPath.row)"
        return cell
    }

Пока в качестве примера можно задать любые значения, лишь для того, чтобы убедиться, что мы все сделали правильно и приложение запускается без ошибок.

Дальше нам следует создать сами секции, так как именно при нажатии на секцию у нас будут пропадать и появляться участки ячеек. 

Для этого нам пригодится метод tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?, который позволяет отображать в секции не просто название, а еще любой кастомный объект, являющийся собой UIView

Поэтому создаем отдельный класс, который будет наследоваться от UITableViewHeaderFooterView

И так как для CustomHeaderView нужен User Interface, создаем XIB файл.

 

Не забываем про правило называть XIB файлы так же как и .swift файлы. 

Далее нам нужно привязать их вместе. 

Для этого заходим в XIB файл -> Show Identity inspector, добавляем CustomHeaderView в качестве класса, и назначаем Restoration ID для того, чтобы в будущем зарегистрировать CustomHeaderView в UITableView.

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

Теперь переходим в ViewController и создаем метод, в котором зарегистрируем CustomHeaderView

Для регистрации CustomHeaderView в качестве секции нужно пройти 3 этапа:

  1. создать headerID, который по сути и является Restoration ID, созданный нами ранее в Identity inspector
  2. создаем nib, объект, который содержит или содержит nib файлы Interface Builder - let nib = UINib(nibName: headerID, bundle: nil)
  3. сама регистрация - tableView.register(nib, forHeaderFooterViewReuseIdentifier: headerID)

И добавим метод tableView.tableFooterView = UIView(), который позволяет убрать лишние полоски внизу таблицы.

let headerID = String(describing: CustomHeaderView.self)
    
    private func tableViewConfig() {
        let nib = UINib(nibName: headerID, bundle: nil)
        tableView.register(nib, forHeaderFooterViewReuseIdentifier: headerID)
       
        tableView.tableFooterView = UIView()
    }

Метод tableViewConfig() будет вызываться в viewDidLoad()

Благодаря этому теперь мы можем поработать с методом делегата tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?.

Создаем header через метод tableView.dequeueReusableHeaderFooterView(withIdentifier: _), где withIdentifier - это идентификатор CustomHeaderView: let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: headerID) as! CustomHeaderView.

Теперь мы можем задать текст для UILabel и поменять цвет заливки: header.titleLabel.text = "Section: \(section)". И завершая настройку вида секции нужно вернуть header.

Также нужно задать высоту для секции. Это можно сделать как в Interface Builder, так и в коде методом tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat. Устанавливаем значение 60.

func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: headerID) as! CustomHeaderView
        
        header.titleLabel.text = "Section: \(section)"
        
        return header
    }
    
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 60
    }

Если вы все сделали правильно, то на данном этапе у вас должно получиться как-то так:

Хорошая работа! Сейчас мы подошли к работе с моделью данных.

Для этого создаем структуру ExpandedModel, которая будет хранить в себе 3 свойства:

  • isExpanded (булевое значение, которое будет проверять расширились ячейки или нет)
  • title (заголовок для секции)
  • array (непосредственно массив значений)
import Foundation

struct ExpandedModel {
    var isExpanded: Bool
    let title: String
    let array: [String]
}

В ViewController создаем пустой массив с новой моделью [ExpandedModel], и в методе viewDidLoad(), заполняем массив значениями. У меня это получилось так:

import UIKit

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    let headerID = String(describing: CustomHeaderView.self)
    var arrayOfData = [ExpandedModel]()
    
    @IBOutlet weak var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        arrayOfData = [
            ExpandedModel(isExpanded: true, title: "Words", array: ["One", "Two", "Three", "Four", "Five"]),
            ExpandedModel(isExpanded: true, title: "Numbers", array: ["6", "7", "8", "9", "10"]),
            ExpandedModel(isExpanded: true, title: "Сharacters", array: ["Q", "W", "E", "R", "T", "Y"]),
            ExpandedModel(isExpanded: true, title: "Emojis", array: ["😀", "😡", "🥶", "😱", "😈"])
        ]
        
        tableViewConfig()
    }

Далее нам нужно заполнить методы UITableViewDelegate и UITableViewDataSource новыми значениями массива arrayOfData:

 

func numberOfSections(in tableView: UITableView) -> Int {
        return arrayOfData.count
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if !arrayOfData[section].isExpanded {
            return 0
        }
        
        return arrayOfData[section].array.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = arrayOfData[indexPath.section].array[indexPath.row]
        return cell
    }

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: headerID) as! CustomHeaderView
        
        header.configure(title: arrayOfData[section].title, section: section)
        header.rotateImage(arrayOfData[section].isExpanded)
        header.delegate = self
        
        return header
    }

И у нас остается обработка нажатия на секцию. Для этого нужно перейти и поработать с файлом CustomHeaderView.

Начнем с нажатия. Есть много вариантов, но я покажу на примере с UIButton. Добавляем кнопку растягиваем по всем краям и оставляем слева и справа отступ по 20. К кнопке добавляем картинку (я добавлю стрелочку), и располагаем ее справа.

Добавляем @IBOutlet и @IBAction в файл CustomHeaderView. Чтобы можно было работать с нажатием на кнопку в секции нужно создать делегат. Но это немного позже. Сейчас мы настроим конфигурацию CustomHeaderView

Для этого нам понадобится 2 метода:

  • первый, будет настраивать UILabel и UIButton. Соответственно для этого нам нужно будет 2 параметра: title: String, section: Int
  • второй, будет переворачивать стрелочку. Этот метод будет принимать параметр: expanded: Bool
func configure(title: String, section: Int) {
        titleLabel.text = title
        headerButton.tag = section
    }
    
    func rotateImage(_ expanded: Bool) {
        if expanded {
            headerButton.imageView?.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
        } else {
            headerButton.imageView?.transform = CGAffineTransform(rotationAngle: CGFloat.zero)
        }
    }

Теперь дело подошло к делегату, который будет позволять пользователям динамически расширять или сворачивать участки ячеек, в 3 шага:

  1. Создаем HeaderViewDelegate c одним методом: expandedSection(button: UIButton)
  2. Создаем слабую ссылку на делегат: weak var delegate: HeaderViewDelegate?
  3. В @IBAction вызываем метод делегата delegate?.expandedSection(button: sender)
import UIKit

protocol HeaderViewDelegate: class {
    func expandedSection(button: UIButton)
}

class CustomHeaderView: UITableViewHeaderFooterView {
    
    weak var delegate: HeaderViewDelegate?
    
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var headerButton: UIButton!
   
    func configure(title: String, section: Int) {
        titleLabel.text = title
        headerButton.tag = section
    }
    
    func rotateImage(_ expanded: Bool) {
        if expanded {
            headerButton.imageView?.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
        } else {
            headerButton.imageView?.transform = CGAffineTransform(rotationAngle: CGFloat.zero)
        }
    }
    
    @IBAction func tapHeader(sender: UIButton) {
        delegate?.expandedSection(button: sender)
    }
}

Для чего нам кнопка в виде параметра? Она нужна для установки UIButton.tag. Таким образом каждая секция будет равна тегу кнопки. 

В ViewController в методе tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?, подписываемся под протокол HeaderViewDelegate, и вызываем методы конфигурации CustomHeaderView, которые мы создали ранее.

func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: headerID) as! CustomHeaderView
        
        header.configure(title: arrayOfData[section].title, section: section)
        header.rotateImage(arrayOfData[section].isExpanded)
        header.delegate = self
        
        return header
    }

И последний шаг - реализация HeaderViewDelegate в ViewController:

  • отслеживаем на какую секцию было нажатие: let section = button.tag
  • определяем было ли нажатие на секцию: let isExpanded = arrayOfData[section].isExpanded
  • меняем значение секции isExpanded на противоположное:       arrayOfData[section].isExpanded = !isExpanded
  • перезагружаем секцию: tableView.reloadSections(IndexSet(integer: section), with: .automatic)
extension ViewController: HeaderViewDelegate {
func expandedSection(button: UIButton) {
let section = button.tag

let isExpanded = arrayOfData[section].isExpanded
arrayOfData[section].isExpanded = !isExpanded

tableView.reloadSections(IndexSet(integer: section), with: .automatic)
}
}

И это еще не все. Если вы сейчас попробуете запустить приложение и нажать на секцию, то ничего не произойдет. Потому что нужно немного изменить метод tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int и добавить небольшую логику.

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if !arrayOfData[section].isExpanded {
            return 0
        }
        
        return arrayOfData[section].array.count
    }

Поздравляю! Теперь вы знаете и умеете работать с UITableView чуть больше.

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

Автор статьи Михаил Цейтлин.

Комментарии

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

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