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

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

Как создать такой вид таблиц в iOS ??

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


person Meet Doshi    schedule 17.10.2015    source источник


Ответы (12)


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

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

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

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

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

person Nicolas Miari    schedule 04.01.2016

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

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.row == 0) {
        ///it's 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.

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

person Meet Doshi    schedule 04.01.2016
comment
Другой метод, использующий файл списка свойств, упоминается в этом руководстве appcoda.com/expandable-table-view Учебник в быстром темпе. Полный проект находится здесь github.com/appcoda/expandable-table -view - person user3354805; 08.01.2016
comment
Небольшой вариант - использовать insertRowsAtIndexPaths:withRowAnimation: вместо reloadSections:, поскольку иногда анимация выглядит не совсем правильно, когда вы перезагружаете весь раздел. - person chedabob; 10.01.2016

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

Дизайн такой:

  • используя подход MVVM, создайте класс CollapsableViewModel, содержащий информацию, необходимую для настройки ячейки: метка, изображение
  • помимо указанного выше, есть два дополнительных поля: children, которое представляет собой массив CollapsableViewModel объектов, и isCollapsed, которое содержит состояние раскрывающегося списка
  • контроллер представления содержит ссылку на иерархию CollapsableViewModel, а также плоский список, содержащий модели представлений, которые будут отображаться на экране (свойство displayedRows)
  • при каждом нажатии на ячейку проверяйте, есть ли у нее дочерние элементы, и добавляйте или удаляйте строки как в 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, который настраивается с помощью модели представления (наконец, переместите логику конфигурации ячейки с контроллера туда, где она принадлежит - ячейку). Кредиты на логику разделителя только для некоторых ячеек достаются людям, отвечающим на этот вопрос SO.

Во-первых, обновите модель, добавьте свойство 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 необходимо будет изменить, чтобы он возвращал такие ячейки:

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).

person Cristik    schedule 05.01.2016
comment
@Cristik, я использовал ваш код .. но мне нужно изменить некоторые изменения, я хочу удалить единственную строку separatorStyle в подменю (Дети), но у родителей она будет однострочной ... вы можете помочь в этом ... !! - person Balaji Gupta; 07.01.2016
comment
@BalajiGupta проверьте этот пост, чтобы узнать, как использовать разделители только для некоторых ячеек stackoverflow.com/questions/8561774/. - person Cristik; 07.01.2016
comment
@BalajiGupta Я создаю код, касающийся разделителей ячеек, пожалуйста, проверьте обновленный ответ. - person Cristik; 07.01.2016
comment
@Cristik Эй, ваш код работает потрясающе, мне просто интересно, хочу ли я сделать так, чтобы при щелчке по новой ячейке каждая другая открытая ячейка закрывалась. Например. если ячейка учетной записи открыта, когда я щелкаю ячейку события, ячейка учетной записи закрывается и открывается событие. Любая помощь / совет по логике приветствуется :) Спасибо - person Kashish Goel; 27.07.2016
comment
@MarcusWayne, если вы примените логику из tableView: didSelectRowAtIndexPath: для двух моделей представления: развернутой в данный момент и той, которая задействована, вы добьетесь того, что вам нужно. Одно незначительное изменение заключалось бы в том, чтобы иметь только один набор _2 _ + _ 3_. - person Cristik; 30.07.2016

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

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

class ClsMenuGroup: NSObject {

    // We can also add Menu group's 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.

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

  1. Вы можете в любое время изменить порядок любого меню или раздела или поменять меню и раздел без изменения других функций.
  2. Вам не нужно будет добавлять длинный код else if в методы делегата tableview.
  3. Вы можете указать значок, заголовок или другой атрибут для своего пункта меню отдельно, например, добавить количество значков, изменить цвет выбранного меню и т. Д.
  4. Вы также можете использовать несколько ячеек или разделов, внеся незначительные изменения в существующий код.
person HarshIT    schedule 06.01.2016
comment
Почему венгерская нотация? - person Cristik; 07.01.2016
comment
@Cristik, стандартное соглашение о кодировании в нашей компании. Мы добавляем префикс, такой как str, int и т. Д., Чтобы идентифицировать тип данных, вместо того, чтобы каждый раз выполнять Cmd + Click, поэтому это считается хорошей практикой для длительного кодирования. - person HarshIT; 07.01.2016
comment
Вы должны проверить это: Почему мне не использовать «Венгерскую нотацию»? - person Cristik; 07.01.2016
comment
@Cristik, Спасибо, прочитал блог Джоэла. Он звучит правильно. Но если вы видите его пример, связанный с C ++, тогда наша венгерская нотация необходима. Также другие ответы на странице Почему мне не использовать «Венгерскую нотацию»? были правильными для использования венгерской нотации. Я думаю, что нам следует улучшить соглашения о кодировании, добавив также поведение переменной вместо простого типа данных (например, необязательную привязку и т. Д.) - person HarshIT; 07.01.2016
comment
Венгерская нотация была хорошей в свое время и, вероятно, до сих пор подходит для некоторых языков. Swift не является одним из них, здесь каждая переменная имеет четко определенный тип, который устраняет необходимость префикса ее имени. И xcode позволяет очень легко определить, что это за тип (проверьте вид сбоку инспектора). - person Cristik; 09.01.2016

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

  • Menu 1
    • Item 1.1
    • Пункт 1.2
    • Пункт 1.3
  • Menu 2
    • Item 2.1
    • Пункт 2.2

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

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

person Alexander Perechnev    schedule 17.10.2015

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

  • . Это заголовок TableViewSection, isExpand -> section. Count иначе вернет 0.

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

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

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

  • . Это заголовок TableViewSection, isExpand -> section. Count иначе вернет 0.

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

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

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

person Trung Phan    schedule 04.01.2016
comment
Можете ли вы обнаружить нажатия на заголовок раздела, как с ячейками (-tableView:didSelectRowAtIndexPath:)? - person Nicolas Miari; 04.01.2016
comment
Хотите поделиться ссылкой или хотя бы названием метода? Не могу найти в документации Apple. - person Nicolas Miari; 04.01.2016
comment
Извините, я делаю это давно, поэтому я забыл об этом, вы можете использовать кнопку или жест касания, как этот: stackoverflow.com/questions/7750720/ - person Trung Phan; 05.01.2016

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

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

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

  1. DataObject структура будет использоваться для хранения информации об узле древовидного представления - она ​​будет отвечать за хранение информации о заголовке ячейки, ее изображении (если в ячейке есть изображение) и ее дочерних элементах (если у ячейки есть дочерние элементы).
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. Вы несете ответственность только за данные, которые используются для настройки ячеек, остальное обрабатывает сам элемент управления.

person Rafał Augustyniak    schedule 09.01.2016
comment
Августыняк, у меня аналогичное требование, и я использовал для этого RATreeView. Во-первых, я хочу, чтобы родительские строки были привязаны сверху, когда пользователь начинает прокрутку, как это делают обычные заголовки UITableView. Есть идеи, как это сделать? - person iAmd; 19.05.2016
comment
Это невозможно с текущей реализацией RATreeView. ???? - person Rafał Augustyniak; 19.05.2016

@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];
    }
}

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

person Vũ Ngọc Giang    schedule 08.01.2016
comment
как выполнить действие с этими элементами расширяемого меню в цели c @ Vũ Ngọc Giang - person Sagar Rathode; 07.04.2017

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

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

person Bhavuk Jain    schedule 17.10.2015

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

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

На Github доступны сторонние библиотеки, которые могут спасти вас от хастеля. Взгляните на CollapsableTableView или CollapsableTable-Swift

person luckyShubhra    schedule 04.01.2016

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

objc_setAssociatedObject

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

objc_getAssociatedObject.

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

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

person Ankit Thakur    schedule 06.01.2016

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

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

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

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

  4. Действие развертывания не обязательно должно быть привязано к пользователю, выбирающему ячейку, например, ячейка может иметь 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?
}

В Swift замечательно то, что мы можем написать часть реализации в расширении протокола, и все соответствующие классы могут использовать реализацию по умолчанию, поэтому мы можем написать реализацию 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.

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

person juanjo    schedule 10.01.2016