Выпадающий список в UITableView в iOS

введите описание изображения здесь

Как создать этот тип табличного представления в iOS

Здесь, если мы коснемся 1-й строке "Учетная запись", затем автоматически прокручивается еще несколько строк, которые отображаются в "Изображение". И если мы снова коснемся учетной записи, то это будет скрыто.

Ответ 1

Вы можете легко настроить ячейку для просмотра как заголовок и настроить tableView: didSelectRowAtIndexPath, чтобы развернуть или свернуть раздел, который он находится в ручном режиме. Если бы я сохранил массив логических значений, соответствующих "затраченное" значение каждого из ваших разделов. Затем вы могли бы tableView:didSelectRowAtIndexPath на каждой из ваших собственных строк заголовка переключить это значение, а затем перезагрузить этот конкретный раздел.

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.row == 0) {
        ///it the first row of any section so it would be your custom section header

        ///put in your code to toggle your boolean value here
        mybooleans[indexPath.section] = !mybooleans[indexPath.section];

        ///reload this section
        [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationFade];
    }
}

Затем вы установили свой номер numberOfRowsInSection, чтобы проверить значение mybooleans, и верните либо 1, если раздел не будет расширен, или 1+ количество элементов в разделе, если оно было расширено.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

    if (mybooleans[section]) {
        ///we want the number of people plus the header cell
        return [self numberOfPeopleInGroup:section] + 1;
    } else {
        ///we just want the header cell
        return 1;
    }
}

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

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section - лучший способ предоставить свой "собственный пользовательский заголовок", как и то, что он разработал.

Для получения дополнительной информации см. этот Ответ или этот PKCollapsingTableViewSections.

Также вы можете получить этот тип табличных просмотров с помощью setIndentationLevel. Пожалуйста, обратитесь к этому DemoCode для этого примера. Я считаю это лучшим решением для раскрывающихся таблиц.

Если вы хотите, чтобы простой заголовок и ячейка выпадала, пожалуйста, обратитесь STCollapseTableView.

Надеюсь, это то, что вы ищете. Меня волнует любая забота.:)

Ответ 2

Самый простой и естественный способ реализовать это, если через ячейки табличного представления. Никаких расширяющихся представлений ячеек, никаких заголовков разделов, простых и простых ячеек (в конце концов, мы в табличном представлении).

Дизайн выглядит следующим образом:

  • используя подход MVVM, создайте класс CollapsableViewModel который содержит информацию, необходимую для настройки ячейки: метка, изображение
  • Помимо вышеупомянутого, есть два дополнительных поля: children, которое является массивом объектов CollapsableViewModel, и isCollapsed, который содержит состояние раскрывающегося isCollapsed
  • контроллер зрения содержит ссылку на иерархию CollapsableViewModel, а также плоский список, содержащие модели представлений, которые будут отображены на экране (displayedRows свойство)
  • при каждом касании ячейки проверяйте, есть ли у нее дочерние deleteRowsAtIndexPaths(), и добавляйте или удаляйте строки в обоих displayedRows строках и в табличном представлении с помощью функций insertRowsAtIndexPaths() и deleteRowsAtIndexPaths().

Код Swift выглядит следующим образом (обратите внимание, что в коде используется только свойство label модели представления, чтобы сохранить его в чистоте):

import UIKit

class CollapsableViewModel {
    let label: String
    let image: UIImage?
    let children: [CollapsableViewModel]
    var isCollapsed: Bool

    init(label: String, image: UIImage? = nil, children: [CollapsableViewModel] = [], isCollapsed: Bool = true) {
        self.label = label
        self.image = image
        self.children = children
        self.isCollapsed = isCollapsed
    }
}

class CollapsableTableViewController: UITableViewController {
    let data = [
        CollapsableViewModel(label: "Account", image: nil, children: [
            CollapsableViewModel(label: "Profile"),
            CollapsableViewModel(label: "Activate account"),
            CollapsableViewModel(label: "Change password")]),
        CollapsableViewModel(label: "Group"),
        CollapsableViewModel(label: "Events", image: nil, children: [
            CollapsableViewModel(label: "Nearby"),
            CollapsableViewModel(label: "Global"),
            ]),
        CollapsableViewModel(label: "Deals"),
    ]

    var displayedRows: [CollapsableViewModel] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        displayedRows = data
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return displayedRows.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CellIdentifier") ?? UITableViewCell()
        let viewModel = displayedRows[indexPath.row]
        cell.textLabel!.text = viewModel.label
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: false)
        let viewModel = displayedRows[indexPath.row]
        if viewModel.children.count > 0 {
            let range = indexPath.row+1...indexPath.row+viewModel.children.count
            let indexPaths = range.map { IndexPath(row: $0, section: indexPath.section) }
            tableView.beginUpdates()
            if viewModel.isCollapsed {
                displayedRows.insert(contentsOf: viewModel.children, at: indexPath.row + 1)
                tableView.insertRows(at: indexPaths, with: .automatic)
            } else {
                displayedRows.removeSubrange(range)
                tableView.deleteRows(at: indexPaths, with: .automatic)
            }
            tableView.endUpdates()
        }
        viewModel.isCollapsed = !viewModel.isCollapsed
    }
}

Аналог Objective-C легко перевести, я добавил версию Swift только потому, что она короче и более читабельна.

С парой небольших изменений, код может быть использован для создания раскрывающихся списков нескольких уровней.

редактировать

Люди спрашивали меня о разделителях, это может быть достигнуто путем добавления пользовательского класса CollapsibleTableViewCell который настраивается с помощью модели представления (наконец, переместите логику конфигурации ячейки из контроллера в место, где она принадлежит - ячейка). Кредиты на логику разделителя только для некоторых ячеек получают люди, отвечающие на этот вопрос.

Во-первых, обновите модель, добавьте свойство needsSeparator которое сообщает ячейке табличного представления, отображать или нет разделитель:

class CollapsableViewModel {
    let label: String
    let image: UIImage?
    let children: [CollapsableViewModel]
    var isCollapsed: Bool
    var needsSeparator: Bool = true

    init(label: String, image: UIImage? = nil, children: [CollapsableViewModel] = [], isCollapsed: Bool = true) {
        self.label = label
        self.image = image
        self.children = children
        self.isCollapsed = isCollapsed

        for child in self.children {
            child.needsSeparator = false
        }
        self.children.last?.needsSeparator = true
    }
}

Затем добавьте класс ячейки:

class CollapsibleTableViewCell: UITableViewCell {
    let separator = UIView(frame: .zero)

    func configure(withViewModel viewModel: CollapsableViewModel) {
        self.textLabel?.text = viewModel.label
        if(viewModel.needsSeparator) {
            separator.backgroundColor = .gray
            contentView.addSubview(separator)
        } else {
            separator.removeFromSuperview()
        }
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        let separatorHeight = 1 / UIScreen.main.scale
        separator.frame = CGRect(x: separatorInset.left,
                                 y: contentView.bounds.height - separatorHeight,
                                 width: contentView.bounds.width-separatorInset.left-separatorInset.right,
                                 height: separatorHeight)
    }
}

cellForRowAtIndexPath будет необходимо изменить cellForRowAtIndexPath, чтобы он возвращал такие ячейки:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = (tableView.dequeueReusableCell(withIdentifier: "CollapsibleTableViewCell") as? CollapsibleTableViewCell) ?? CollapsibleTableViewCell(style: .default, reuseIdentifier: "CollapsibleTableViewCell")
        cell.configure(withViewModel: displayedRows[indexPath.row])
        return cell
    }

Последний шаг: удалите разделители ячеек табличного представления по умолчанию - либо из xib, либо из кода (tableView.separatorStyle =.none).

Ответ 3

Вот решение на основе MVC.

Создайте класс модели ClsMenuGroup для ваших разделов

class ClsMenuGroup: NSObject {

    // We can also add Menu group name and other details here.
    var isSelected:Bool = false
    var arrMenus:[ClsMenu]!
}

Создайте класс модели ClsMenu для ваших рядов

class ClsMenu: NSObject {

    var strMenuTitle:String!
    var strImageNameSuffix:String!
    var objSelector:Selector!   // This is the selector method which will be called when this menu is selected.
    var isSelected:Bool = false

    init(pstrTitle:String, pstrImageName:String, pactionMehod:Selector) {

        strMenuTitle = pstrTitle
        strImageNameSuffix = pstrImageName
        objSelector = pactionMehod
    }
}

Создать массив групп в вашем ViewController

 class YourViewController: UIViewController, UITableViewDelegate {

    @IBOutlet var tblMenu: UITableView!
    var objTableDataSource:HDTableDataSource!
    var arrMenuGroups:[AnyObject]!

    // MARK: - View Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        if arrMenuGroups == nil {
            arrMenuGroups = Array()
        }

        let objMenuGroup = ClsMenuGroup()
        objMenuGroup.arrMenus = Array()

        var objMenu = ClsMenu(pstrTitle: "Manu1", pstrImageName: "Manu1.png", pactionMehod: "menuAction1")
        objMenuGroup.arrMenus.append(objMenu)

        objMenu = ClsMenu(pstrTitle: "Menu2", pstrImageName: "Menu2.png", pactionMehod: "menuAction2")
        objMenuGroup.arrMenus.append(objMenu)

        arrMenuGroups.append(objMenuGroup)
        configureTable()
    }


    func configureTable(){

        objTableDataSource = HDTableDataSource(items: nil, cellIdentifier: "SideMenuCell", configureCellBlock: { (cell, item, indexPath) -> Void in

            let objTmpGroup = self.arrMenuGroups[indexPath.section] as! ClsMenuGroup
            let objTmpMenu = objTmpGroup.arrMenus[indexPath.row]
            let objCell:YourCell = cell as! YourCell

            objCell.configureCell(objTmpMenu)  // This method sets the IBOutlets of cell in YourCell.m file.
        })

        objTableDataSource.sectionItemBlock = {(objSection:AnyObject!) -> [AnyObject]! in

            let objMenuGroup = objSection as! ClsMenuGroup
            return (objMenuGroup.isSelected == true) ? objMenuGroup.arrMenus : 0
        }

        objTableDataSource.arrSections = self.arrMenuGroups
        tblMenu.dataSource = objTableDataSource
        tblMenu.reloadData()
    }

    // MARK: - Tableview Delegate

    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {

        let objTmpGroup = self.arrMenuGroups[indexPath.section] as! ClsMenuGroup
        let objTmpMenu = objTmpGroup.arrMenus[indexPath.row]

        if objTmpMenu.objSelector != nil && self.respondsToSelector(objTmpMenu.objSelector) == true {
            self.performSelector(objTmpMenu.objSelector)  // Call the method for the selected menu.
        }

        tableView.reloadData()
    }

    func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {

        let arrViews:[AnyObject] = NSBundle.mainBundle().loadNibNamed("YourCustomSectionView", owner: self, options: nil)
        let objHeaderView = arrViews[0] as! UIView
        objHeaderView.sectionToggleBlock = {(objSection:AnyObject!) -> Void in

            let objMenuGroup = objSection as! ClsMenuGroup
            objMenuGroup.isSelected = !objMenuGroup.isSelected
            tableView.reloadData()
        }
        return objHeaderView
    }

    // MARK: - Menu methods

    func menuAction1(){

    }

    func menuAction2(){

    }
}

Я использовал HDTableDataSource вместо методов источника данных Tableview. Вы можете найти пример HDTableDataSource из Github.

Преимущества кода выше

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

Ответ 4

Обычно я делаю это, устанавливая высоту строки. Например, у вас есть два пункта меню с раскрывающимися списками:

  • Меню 1
    • Пункт 1.1
    • Пункт 1.2
    • Пункт 1.3
  • Меню 2
    • Пункт 2.1
    • Пункт 2.2

Итак, вам нужно создать табличное представление с двумя разделами. Первый раздел содержит 4 строки (меню 1 и элементы), а секция секунд содержит 3 строки (меню 2 и элементы).

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

Ответ 5

простой способ сделать это - использовать заголовок раздела UITableView, поскольку cell- > и заданное число строк равно 0 и section.count для состояния свернуть и развернуть.

  • . Это заголовок TableViewSection, isExpand → section.count else return 0.

    -Нормальная ячейка

    -Нормальная ячейка

    -Нормальная ячейка

  • . Это заголовок TableViewSection, isExpand → section.count else return 0.

    -Нормальная ячейка

    -Нормальная ячейка

    -Нормальная ячейка

Ответ 6

Не существует встроенного элемента управления для древовидных представлений в iOS-структуре - UIKit. Как было отмечено другими пользователями, возможно, самым простым решением (без использования каких-либо внешних библиотек) является добавление некоторой пользовательской логики в делегат UITableView и источник данных для имитации желаемого поведения.

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

Лично я являюсь автором библиотеки RATreeView, целью которой является минимизация затрат, необходимых для создать древовидный вид, например, в iOS. Вы можете проверить примеры проектов (доступно в Objective-c и Swift), чтобы проверить, как этот контроль работает и ведет себя. Используя мой элемент управления, очень просто создать представление, которое вы хотите:

  • DataObject struct будет использоваться для хранения информации о древовидном представлении node - он будет отвечать за сохранение информации о названии ячейки, ее изображении (если ячейка имеет изображение) и ее дочерних элементов (если ячейка имеет дети).
class DataObject
{
    let name : String
    let imageURL : NSURL?
    private(set) var children : [DataObject]

    init(name : String, imageURL : NSURL?, children: [DataObject]) {
        self.name = name
        self.imageURL = imageURL
        self.children = children
    }

    convenience init(name : String) {
        self.init(name: name, imageURL: nil, children: [DataObject]())
    }
}
  1. Мы объявим протокол TreeTableViewCell и реализуем две ячейки, которые соответствуют этому протоколу. Одна из этих ячеек будет использоваться для отображения корневых элементов, а другая - для отображения дочерних элементов корневых элементов.
protocol TreeTableViewCell {
    func setup(withTitle title: String, imageURL: NSURL?, isExpanded: Bool)
}

class ChildTreeTableViewCell : UITableViewCell, TreeTableViewCell {
    func setup(withTitle title: String, imageURL: NSURL?, isExpanded: Bool) {
       //implementation goes here 
    }
}

class RootTreeTableViewCell : UITableViewCell, TreeTableViewCell {
    func setup(withTitle title: String, imageURL: NSURL?, isExpanded: Bool) {
       //implementation goes here
    }
}
  1. В контроллере внешнего вида (MVC) или модели представления (MVVM) мы определяем структуру данных, ответственную за резервное копирование нашего древовидного представления.
let profileDataObject = DataObject(name: "Profile")
let privateAccountDataObject = DataObject(name: "Private Account")
let changePasswordDataObject = DataObject(name: "Change Password")
let accountDataObject = DataObject(name: "Account", imageURL: NSURL(string: "AccountImage"), children: [profileDataObject, privateAccountDataObject, changePasswordDataObject])

let groupDataObject = DataObject(name: "Group", imageURL: NSURL(string: "GroupImage"), children: [])
let eventDataObject = DataObject(name: "Event", imageURL: NSURL(string: "EventImage"), children: [])
let dealsDataObject = DataObject(name: "Deals", imageURL: NSURL(string: "DealsImage"), children: [])

data = [accountDataObject, groupDataObject, eventDataObject, dealsDataObject]
  1. Далее нам понадобится реализовать несколько методов из источника данных RATreeView.
func treeView(treeView: RATreeView, numberOfChildrenOfItem item: AnyObject?) -> Int {
    if let item = item as? DataObject {
        return item.children.count //return number of children of specified item
    } else {
        return self.data.count //return number of top level items here
    }
}

func treeView(treeView: RATreeView, child index: Int, ofItem item: AnyObject?) -> AnyObject {
    if let item = item as? DataObject {
        return item.children[index] //we return child of specified item here (using provided `index` variable)
    } else {
        return data[index] as AnyObject //we return root item here (using provided `index` variable)
    }
}

func treeView(treeView: RATreeView, cellForItem item: AnyObject?) -> UITableViewCell {
    let cellIdentifier = item ? "TreeTableViewChildCell" : "TreeTableViewCellRootCell"
    let cell = treeView.dequeueReusableCellWithIdentifier(cellIdentifier) as! TreeTableViewCell

    //TreeTableViewCell is a protocol which is implemented by two kinds of
    //cells - the one responsible for root items in the tree view and another 
    //one responsible for children. As we use protocol we don't care
    //which one is truly being used here. Both of them can be
    //configured using data from `DataItem` object.

    let item = item as! DataObject
    let isExpanded = treeView.isCellForItemExpanded(item) //this variable can be used to adjust look of the cell by determining whether cell is expanded or not

    cell.setup(withTitle: item.name, imageURL: item.imageURL, expanded: isExpanded)

    return cell
}

Обратите внимание, что с помощью моей библиотеки вам не нужно заботиться о расширении и сворачивании ячейки - она ​​обрабатывается RATreeView. Вы отвечаете только за данные, которые используются для настройки ячеек - остальные обрабатываются самим элементом управления.

Ответ 7

@interface TestTableViewController ()
{
    BOOL showMenu;
}

@implementation TestTableViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // Uncomment the following line to preserve selection between presentations.
    // self.clearsSelectionOnViewWillAppear = NO;

    // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
    // self.navigationItem.rightBarButtonItem = self.editButtonItem;
    [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"accountMenu"];
    [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"accountSubMenu"];
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 2;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    if (section == 0) {
        // Account Menu
        return 1;
    }
    if (showMenu) {
        // Profile/Private Account/Change Password
        return 3;
    }
    // Hidden Account Menu
    return 0;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell;

    if (indexPath.section == 0) {
        cell = [tableView dequeueReusableCellWithIdentifier:@"accountMenu" forIndexPath:indexPath];
        cell.textLabel.text = @"Account";
    }
    else
    {
        cell = [tableView dequeueReusableCellWithIdentifier:@"accountSubMenu" forIndexPath:indexPath];
        switch (indexPath.row) {
            case 0:
                cell.textLabel.text = @"Profile";
                break;
            case 1:
                cell.textLabel.text = @"Private Account";
                break;
            case 2:
                cell.textLabel.text = @"Change Password";
                break;

            default:
                break;
        }
    }


    return cell;
}

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (indexPath.section == 0) {
        // Click on Account Menu
        showMenu = !showMenu;
        [tableView reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationAutomatic];
    }
}

Надеюсь, что это поможет:)

Ответ 8

Если вам не нравится использовать внешнюю библиотеку, вы можете создать 2 пользовательских ячейки. Один, который показывает перед расширением, а другой после расширения (с разными идентификаторами). И когда вы нажимаете на ячейку, проверьте, расширена ли ячейка или нет. Если нет, используйте расширенный идентификатор ячейки, иначе идентификатор нерасширенной ячейки.

Это лучший и чистый способ создания расширенной таблицы представления таблицы.

Ответ 9

Вам нужен Collapsable TableView. Чтобы достичь этого, в вашем TableView вы должны отслеживать, какие разделы сжимаются (сокращаются) и какие из них расширяются. Для этого вам необходимо поддерживать набор индексов разделенных разделов или булевский массив, где значение каждого индекса указывает, расширена ли соответствующая секция или нет. Проверьте значения для определенного индекса при назначении высоты определенной строке. эту ссылку для получения дополнительной справки.

Вы можете узнать о разделе TableViews здесь.

В Github есть сторонние библиотеки, которые могут спасти вас от hustel. Посмотри на CollapsableTableView или CollapsableTable-Swift

Ответ 10

У вас может быть Учетная запись как ячейка, которая расширяется при нажатии, чтобы открыть три кнопки ( "Профиль", "Активировать учетную запись", "Сменить пароль" ), но это создает проблему: нажатие каждой из трех кнопок будет считаться "пользователь выбрал ячейку" Учетная запись "и триггер -tableView:didSelectRowAtIndexPath: с результатом расширения/сглаживания ячейки.

Или вы можете сделать каждую из скрытых опций ( "Профиль", "Активировать учетную запись", "Изменить пароль" ) отдельную ячейку представления таблицы. Но я не знаю, как вы могли бы оживить три ячейки в целом, расширяясь и сокращаясь (вместо того, чтобы каждый расширялся отдельно от нулевой высоты до полностью расширенного).

Итак, возможно, лучшее решение:

  • Пусть четные ячейки (индексы: 0, 2, 4...) выполняют как роль "Название меню", так и "Открыть/закрытие меню" (в сторону связанных нечетных ячеек, описанных ниже).
  • Перемещать ячейки тела меню (первоначально сложенные), каждая с одной кнопкой для каждого параметра (например, "Профиль", "Активировать учетную запись", "Сменить пароль" ), выровненную вертикально, по нечетным индексам (1, 3, 5...). Используйте целевое действие для ответа пользователю на выбор каждой кнопки /.
  • Реализовать метод делегата представления таблицы, чтобы можно было выбрать только четные ячейки (заголовки меню) и реализовать логику выбора для расширения/сглаживания соответствующей нечетной ячейки (внутри-tableView: didSelectRowAtIndexPath:). Например, выбор ячейки по индексу 0 ( "Учетная запись" ) приводит к расширению/свертыванию ячейки по индексу 1 (меню с опциями "Профиль", "Активировать учетную запись", "Сменить пароль" ).

Это не самое элегантное использование UITableView, но выполнит эту работу.

Ответ 11

В соответствии с ответом @sticker вы можете привязать время выполнения

objc_setAssociatedObject

для индекса раздела и используйте его логику. И при использовании tapgesture в представлении заголовка вы можете получить индекс раздела как

objc_getAssociatedObject.

UITapGestureRecognizer *singleTapRecogniser = [[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(gestureHandler:)] autorelease];
[singleTapRecogniser setDelegate:self];
singleTapRecogniser.numberOfTouchesRequired = 1;
singleTapRecogniser.numberOfTapsRequired = 1;   
[sectionHeaderView addGestureRecognizer:singleTapRecogniser];

Если вам нужна какая-либо сторонняя библиотека, вы можете попробовать это решение.

Ответ 12

Мне нравится решение @Cristik, некоторое время назад у меня была такая же проблема, и мое решение вытекало из тех же принципов; поэтому это то, что я предлагаю, исходя из требований, которые у меня были:

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

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

  • В представлении таблицы должны отображаться или скрываться ячейки с использованием любой из обычных анимаций (нет reloadData)

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

Упрощенная версия реализации (https://github.com/JuanjoArreola/ExpandableCells) выглядит следующим образом:

Сначала протокол:

protocol CellDescriptor: class {
    var count: Int { get }
    var identifier: String! { get }
}

Нерасширяемая ячейка всегда имеет счетчик 1:

extension CellDescriptor {
    var count: Int { return 1 }
}

Затем протокол расширяемой ячейки:

protocol ExpandableCellDescriptor: CellDescriptor {
    var active: Bool { get set }
    var children: [CellDescriptor] { get set }

    subscript(index: Int) -> CellDescriptor? { get }
    func indexOf(cellDescriptor: CellDescriptor) -> Int?
}

Прохладная вещь о быстром - это то, что мы можем написать часть реализации в расширении протокола, и все соответствующие классы могут использовать реализацию по умолчанию, поэтому мы можем написать реализацию count subscript и indexOf, а также пара других полезных функций:

extension ExpandableCellDescriptor {
    var count: Int {
        var total = 1
        if active {
            children.forEach({ total += $0.count })
        }
        return total
    }

    var countIfActive: Int {
        ...
    }

    subscript(index: Int) -> CellDescriptor? {
        ...
    }

    func indexOf(cellDescriptor: CellDescriptor) -> Int? {
        ...
    }

    func append(cellDescriptor: CellDescriptor) {
        children.append(cellDescriptor)
    }
}

Полная реализация находится в файле CellDescriptor.swift

Кроме того, в том же файле есть класс с именем CellDescriptionArray, который реализует ExpandableCellDescriptor и сам не показывает ячейку

Теперь любой класс может соответствовать предыдущим протоколам без необходимости наследовать от определенного класса, для примера кода в github я создал пару классов: Option и ExpandableOption, вот как ExpandableOption выглядит следующим образом:

class ExpandableOption: ExpandableCellDescriptor {

    var delegate: ExpandableCellDelegate?

    var identifier: String!
    var active: Bool = false {
        didSet {
            delegate?.expandableCell(self, didChangeActive: active)
        }
    }

    var children: [CellDescriptor] = []
    var title: String?
}

И это один из подклассов UITableViewCell:

class SwitchTableViewCell: UITableViewCell, CellDescrptionConfigurable {

    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var switchControl: UISwitch!

    var cellDescription: CellDescriptor! {
        didSet {
            if let option = cellDescription as? ExpandableOption {
                titleLabel.text = option.title
                switchControl.on = option.active
            }
        }
    }

    @IBAction func activeChanged(sender: UISwitch) {
       let expandableCellDescriptor = cellDescription as! ExpandableCellDescriptor
       expandableCellDescriptor.active = sender.on
    }
}

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

Наконец, в TableViewController мы создаем дерево опций:

var options = CellDescriptionArray()

override func viewDidLoad() {
   super.viewDidLoad()

   let account = ExpandableOption(identifier: "ExpandableCell", title: "Account")
   let profile = Option(identifier: "SimpleCell", title: "Profile")
   let isPublic = ExpandableOption(identifier: "SwitchCell", title: "Public")
   let caption = Option(identifier: "SimpleCell", title: "Anyone can see this account")
   isPublic.append(caption)
   account.append(profile)
   account.append(isPublic)
   options.append(account)

   let group = ExpandableOption(identifier: "ExpandableCell", title: "Group")
   group.append(Option(identifier: "SimpleCell", title: "Group Settings"))
   options.append(group)
   ...
}

Остальная реализация теперь очень проста:

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
   return options.count
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
   let option = options[indexPath.row]!
   let cell = tableView.dequeueReusableCellWithIdentifier(option.identifier, forIndexPath: indexPath)

   (cell as! CellDescrptionConfigurable).cellDescription = option
   (option as? ExpandCellInformer)?.delegate = self
   return cell
}

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    guard let option = options[indexPath.row] else { return }
    guard let expandableOption = option as? ExpandableOption else { return }
    if expandableOption.identifier == "ExpandableCell" {
        expandableOption.active = !expandableOption.active
    }
}

func expandableCell(expandableCell: ExpandableCellDescriptor, didChangeActive active: Bool) {
    guard let index = options.indexOf(expandableCell) else { return }
    var indexPaths = [NSIndexPath]()
    for row in 1..<expandableCell.countIfActive {
        indexPaths.append(NSIndexPath(forRow: index + row, inSection: 0))
    }
    if active {
        tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.Fade)
    } else {
        tableView.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.Fade)
    }
}

Это может выглядеть как много кода, но большая часть его написана только один раз, большая часть информации, необходимой для правильного отображения табличного представления, существует в файле CellDescriptor.swift, код конфигурации ячейки существует внутри подклассов UITableViewCell и в самом TableViewController имеется относительно мало кода.

Надеюсь, что это поможет.