在iOS中的UITableView中展开/折叠部分

118

请问如何在以下UITableViewsections中实现可展开/可折叠动画效果?

或者


试试这个: https://dev59.com/ZFwX5IYBdhLWcg3w8TXM - Meet Doshi
请查看我对同一问题的自定义标题的回答:http://stackoverflow.com/questions/18203434/uitableview-with-open-closed-sections/40932319#40932319 - Vaibhav Gaikwad
17个回答

110

你需要创建自己的自定义表头行,并将其作为每个部分的第一行。子类化UITableView或已经存在的表头可能会很麻烦。基于它们现在的工作方式,我不确定你能否轻松地从中获取操作。你可以设置一个单元格看起来像是表头,并设置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方法以返回自定义的标题单元格作为任何部分中第一行的标题。


2
如果您使用过Beejive应用程序,您会知道他们的可折叠部分标题实际上会“浮动”在表格顶部,即使您已经滚动了部分其部分,就像常规的Apple部分标题一样。如果您只是在部分开头添加单元格,则不可能实现这一点。 - user102008
很优雅的解决方案!user102008提到了浮动标题的问题,但在实际需要“章节”滚动的情况下,这是一个很好的方法。 - Nick Cipollina
@mjdth请给我一些示例代码,因为我需要隐藏/显示特定单元格...提前感谢。 - Bajaj
11
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section是提供自定义表头的更好方式,因为它正是为此而设计的。 - William Denniss
当我只有一个部分时,这最初对我起作用,但是一旦我有了更多的部分,我就会收到“无效更新无效行数”错误。我知道这个解决方案比较老,但如果我们有多个部分,这只能适用于一个部分吗?如果我们有多个部分,我们需要添加实际添加/删除行的代码吗? - skinsfan00atg
这可能是过去的一个好解决方案,但现在更好的方法是简单地子类化UITableViewHeaderFooterView并使用索引跟踪数据。 - GetSwifty

103

苹果提供了一些用于使用表格视图部分标题进行展开/折叠动画的示例代码,可以在此处找到:表格视图动画和手势

这种方法的关键是实现- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section并返回一个自定义的UIView,其中包括一个按钮(通常与标头视图本身大小相同)。通过对UIView进行子类化并将其用作标题视图(如此示例所示),您可以轻松存储附加数据,例如部分号码。


29
太棒了,谢谢:http://developer.apple.com/library/ios/#samplecode/TableViewUpdates/Introduction/Intro.html - EightyEight
不记得了,但为什么示例代码在iOS 4之前的版本上无法运行? - samwize
1
我不知道。它只是说“iOS 4.0.2或更高版本”。 - user102008
@user102008,它可能在iOS 4之前无法工作,因为它使用了ARC和其他技术。 - David
1
链接中的当前更新代码存在漏洞,很容易崩溃。 - Ankit Srivastava
1
就像Ankit Srivastava之前提到的那样,破解这个代码示例很容易:只需将所有项目字典复制并粘贴到PlaysAndQuotations.plist中(我已经在根字典中测试了30个条目)。现在启动应用程序并打开第一个节目 - 之后您向下滚动,直到看到一个向下箭头(我认为这来自dequeueReusableHeaderFooterViewWithIdentifier)- 点击该箭头,然后向上滚动到第一个播放并尝试关闭它-> NSInternalInconsistencyException(iOS 8.4 / iPhone 5s)。 - Raimund Wege

25

我受到了苹果公司Table View Animations and Gestures的启发,得到了一个不错的解决方案。我从苹果公司的示例中删去了不必要的部分,并将其翻译成了Swift。

我知道答案有点长,但所有的代码都是必要的。幸运的是,你可以复制粘贴大部分代码,只需要在步骤1和3上做一点修改。

1.创建SectionHeaderView.swiftSectionHeaderView.xib

import UIKit

protocol SectionHeaderViewDelegate {
    func sectionHeaderView(sectionHeaderView: SectionHeaderView, sectionOpened: Int)
    func sectionHeaderView(sectionHeaderView: SectionHeaderView, sectionClosed: Int)
}

class SectionHeaderView: UITableViewHeaderFooterView {
    
    var section: Int?
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var disclosureButton: UIButton!
    @IBAction func toggleOpen() {
        self.toggleOpenWithUserAction(true)
    }
    var delegate: SectionHeaderViewDelegate?
    
    func toggleOpenWithUserAction(userAction: Bool) {
        self.disclosureButton.selected = !self.disclosureButton.selected
        
        if userAction {
            if self.disclosureButton.selected {
                self.delegate?.sectionHeaderView(self, sectionClosed: self.section!)
            } else {
                self.delegate?.sectionHeaderView(self, sectionOpened: self.section!)
            }
        }
    }
    
    override func awakeFromNib() {
        var tapGesture: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: "toggleOpen")
        self.addGestureRecognizer(tapGesture)
        // change the button image here, you can also set image via IB.
        self.disclosureButton.setImage(UIImage(named: "arrow_up"), forState: UIControlState.Selected)
        self.disclosureButton.setImage(UIImage(named: "arrow_down"), forState: UIControlState.Normal)
    }
    
}

在tableview中,灰色背景的SectionHeaderView.xib应该长成这样(当然你可以根据需要进行自定义):enter image description here

注意:

a) toggleOpen 操作应该链接到 disclosureButton

b) 如果不需要按钮,可以删除 disclosureButtontoggleOpen 操作。

2.创建SectionInfo.swift

import UIKit

class SectionInfo: NSObject {
    var open: Bool = true
    var itemsInSection: NSMutableArray = []
    var sectionTitle: String?
    
    init(itemsInSection: NSMutableArray, sectionTitle: String) {
        self.itemsInSection = itemsInSection
        self.sectionTitle = sectionTitle
    }
}

在您的表视图中

import UIKit

class TableViewController: UITableViewController, SectionHeaderViewDelegate  {
    
    let SectionHeaderViewIdentifier = "SectionHeaderViewIdentifier"
    
    var sectionInfoArray: NSMutableArray = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let sectionHeaderNib: UINib = UINib(nibName: "SectionHeaderView", bundle: nil)
        self.tableView.registerNib(sectionHeaderNib, forHeaderFooterViewReuseIdentifier: SectionHeaderViewIdentifier)
        
        // you can change section height based on your needs
        self.tableView.sectionHeaderHeight = 30
        
        // You should set up your SectionInfo here
        var firstSection: SectionInfo = SectionInfo(itemsInSection: ["1"], sectionTitle: "firstSection")
        var secondSection: SectionInfo = SectionInfo(itemsInSection: ["2"], sectionTitle: "secondSection"))
        sectionInfoArray.addObjectsFromArray([firstSection, secondSection])
    }
    
    // MARK: - Table view data source
    
    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return sectionInfoArray.count
    }
    
    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if self.sectionInfoArray.count > 0 {
            var sectionInfo: SectionInfo = sectionInfoArray[section] as! SectionInfo
            if sectionInfo.open {
                return sectionInfo.open ? sectionInfo.itemsInSection.count : 0
            }
        }
        return 0
    }
    
    override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let sectionHeaderView: SectionHeaderView! = self.tableView.dequeueReusableHeaderFooterViewWithIdentifier(SectionHeaderViewIdentifier) as! SectionHeaderView
        var sectionInfo: SectionInfo = sectionInfoArray[section] as! SectionInfo
        
        sectionHeaderView.titleLabel.text = sectionInfo.sectionTitle
        sectionHeaderView.section = section
        sectionHeaderView.delegate = self
        let backGroundView = UIView()
        // you can customize the background color of the header here
        backGroundView.backgroundColor = UIColor(red:0.89, green:0.89, blue:0.89, alpha:1)
        sectionHeaderView.backgroundView = backGroundView
        return sectionHeaderView
    }
    
    func sectionHeaderView(sectionHeaderView: SectionHeaderView, sectionOpened: Int) {
        var sectionInfo: SectionInfo = sectionInfoArray[sectionOpened] as! SectionInfo
        var countOfRowsToInsert = sectionInfo.itemsInSection.count
        sectionInfo.open = true
        
        var indexPathToInsert: NSMutableArray = NSMutableArray()
        for i in 0..<countOfRowsToInsert {
            indexPathToInsert.addObject(NSIndexPath(forRow: i, inSection: sectionOpened))
        }
        self.tableView.insertRowsAtIndexPaths(indexPathToInsert as [AnyObject], withRowAnimation: .Top)
    }
    
    func sectionHeaderView(sectionHeaderView: SectionHeaderView, sectionClosed: Int) {
        var sectionInfo: SectionInfo = sectionInfoArray[sectionClosed] as! SectionInfo
        var countOfRowsToDelete = sectionInfo.itemsInSection.count
        sectionInfo.open = false
        if countOfRowsToDelete > 0 {
            var indexPathToDelete: NSMutableArray = NSMutableArray()
            for i in 0..<countOfRowsToDelete {
                indexPathToDelete.addObject(NSIndexPath(forRow: i, inSection: sectionClosed))
            }
            self.tableView.deleteRowsAtIndexPaths(indexPathToDelete as [AnyObject], withRowAnimation: .Top)
        }
    }
}

1
感谢您在此方面的努力!如果有一个小的Github示例项目,那将是更好的答案。 - Max MacLeod
感谢提供详细的答案。提供示例项目会更好。 - Thiha Aung

23
为了在iOS中实现可折叠表格部分,关键在于如何控制每个部分的行数,或者我们可以管理每个部分的行高。
此外,我们需要自定义部分标题,以便可以监听来自标题区域(无论是按钮还是整个标题)的点击事件。
如何处理标题?很简单,我们扩展UITableViewCell类并创建自定义标题单元格,如下所示:
import UIKit

class CollapsibleTableViewHeader: UITableViewCell {

    @IBOutlet var titleLabel: UILabel!
    @IBOutlet var toggleButton: UIButton!

}

然后使用viewForHeaderInSection来连接标题单元格:
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
  let header = tableView.dequeueReusableCellWithIdentifier("header") as! CollapsibleTableViewHeader

  header.titleLabel.text = sections[section].name
  header.toggleButton.tag = section
  header.toggleButton.addTarget(self, action: #selector(CollapsibleTableViewController.toggleCollapse), forControlEvents: .TouchUpInside)

  header.toggleButton.rotate(sections[section].collapsed! ? 0.0 : CGFloat(M_PI_2))

  return header.contentView
}

记得我们必须返回contentView,因为这个函数期望返回一个UIView。
现在让我们处理可折叠部分,这是切换每个部分可折叠属性的切换函数:
func toggleCollapse(sender: UIButton) {
  let section = sender.tag
  let collapsed = sections[section].collapsed

  // Toggle collapse
  sections[section].collapsed = !collapsed

  // Reload section
  tableView.reloadSections(NSIndexSet(index: section), withRowAnimation: .Automatic)
}

这取决于您如何管理部分数据,在这种情况下,我有类似于以下的部分数据:

struct Section {
  var name: String!
  var items: [String]!
  var collapsed: Bool!

  init(name: String, items: [String]) {
    self.name = name
    self.items = items
    self.collapsed = false
  }
}

var sections = [Section]()

sections = [
  Section(name: "Mac", items: ["MacBook", "MacBook Air", "MacBook Pro", "iMac", "Mac Pro", "Mac mini", "Accessories", "OS X El Capitan"]),
  Section(name: "iPad", items: ["iPad Pro", "iPad Air 2", "iPad mini 4", "Accessories"]),
  Section(name: "iPhone", items: ["iPhone 6s", "iPhone 6", "iPhone SE", "Accessories"])
]

最后,我们需要根据每个部分的可折叠属性来控制该部分的行数:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  return (sections[section].collapsed!) ? 0 : sections[section].items.count
}

我在Github上有一个完全可用的演示:https://github.com/jeantimex/ios-swift-collapsible-table-section

demo

如果您想在分组样式表中实现可折叠的部分,我在这里有另一个带有源代码的演示:https://github.com/jeantimex/ios-swift-collapsible-table-section-in-grouped-section。希望能帮到您。

嗨,我在xib文件上完成了自定义标头部分,并将nib注册到我的Table View Controller。但是当我删除一个部分并尝试再次展开/折叠时,我会收到一个致命错误,指出索引超出范围。有没有什么办法可以解决这个问题?谢谢! - iamhx
非常好的、干净的解决方案! - Joel
Swift 5: tableView.reloadSections([section], with: .automatic) - Sourabh Sharma

10

我有一个更好的解决方案,你可以在节头中添加一个UIButton,并将此按钮的大小设置为与节大小相同,但通过清除背景颜色来隐藏它,这样你就可以轻松地检查哪个部分被点击以扩展或折叠


4
我认为,这种解决方案比被接受的答案更好,因为从语义上讲,你保持了表头作为表头的特性,而不是使用虚假行来模拟表头。方法 tableView:numberOfRowsInSection: 不会被改动,您可以继续将其用于实际意义。同样适用于 tableView:cellForRowAtIndexPath: 方法。 - Cœur
那么,你在部分标题中点击按钮,但是你将如何确定应重新加载哪个部分? - memmons
@Answerbot 你好,通过使用与部分索引相同的值设置按钮标签非常容易。 - Son Nguyen
害怕你会这么说。滥用标签属性来处理诸如tableView索引之类的事情是一个糟糕的设计选择。 - memmons
我从未见过任何“伟大”的解决方案来解决这个问题,这就是为什么我希望你有不同的方法。我看到的最好的答案是苹果公司的参考项目。苹果公司对UITableViewHeaderFooterView进行了子类化,并添加了一个section属性,并定义了一个SectionHeaderViewDelegate,它提供了打开/关闭部分的回调。(https://developer.apple.com/library/ios/samplecode/TableViewUpdates/Introduction/Intro.html) - memmons

7
我最终创建了一个包含按钮的headerView(在事后看到了Son Nguyen's solution,但这是我的代码...看起来很多,但其实很简单):
声明一些布尔变量来表示你的部分。
bool customerIsCollapsed = NO;
bool siteIsCollapsed = NO;

现在在您的表视图委托方法中...

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
    UIView *headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, _tblSearchResults.frame.size.width, 35)];

    UILabel *lblSection = [UILabel new];
    [lblSection setFrame:CGRectMake(0, 0, 300, 30)];
    [lblSection setFont:[UIFont fontWithName:@"Helvetica-Bold" size:17]];
    [lblSection setBackgroundColor:[UIColor clearColor]];
    lblSection.alpha = 0.5;
    if(section == 0)
    {
        if(!customerIsCollapsed)
            [lblSection setText:@"Customers    --touch to show--"];
        else
            [lblSection setText:@"Customers    --touch to hide--"];
    }
    else
    {
        if(!siteIsCollapsed)
            [lblSection setText:@"Sites    --touch to show--"];
        else
            [lblSection setText:@"Sites    --touch to hide--"];    }

    UIButton *btnCollapse = [UIButton buttonWithType:UIButtonTypeCustom];
    [btnCollapse setFrame:CGRectMake(0, 0, _tblSearchResults.frame.size.width, 35)];
    [btnCollapse setBackgroundColor:[UIColor clearColor]];
    [btnCollapse addTarget:self action:@selector(touchedSection:) forControlEvents:UIControlEventTouchUpInside];
    btnCollapse.tag = section;


    [headerView addSubview:lblSection];
    [headerView addSubview:btnCollapse];

    return headerView;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // Return the number of rows in the section.
    if(section == 0)
    {
        if(customerIsCollapsed)
            return 0;
        else
            return _customerArray.count;
    }
    else if (section == 1)
    {
        if(siteIsCollapsed)
            return 0;
        else
        return _siteArray.count;

    }
    return 0;
}

最后是当您触摸其中一个章节标题按钮时调用的函数:

- (IBAction)touchedSection:(id)sender
{
    UIButton *btnSection = (UIButton *)sender;

    if(btnSection.tag == 0)
    {
        NSLog(@"Touched Customers header");
        if(!customerIsCollapsed)
            customerIsCollapsed = YES;
        else
            customerIsCollapsed = NO;

    }
    else if(btnSection.tag == 1)
    {
        NSLog(@"Touched Site header");
        if(!siteIsCollapsed)
            siteIsCollapsed = YES;
        else
            siteIsCollapsed = NO;

    }
    [_tblSearchResults reloadData];
}

我只是在想,这个部分会不会以动画的形式展开和折叠。如果没有动画,它看起来会很糟糕。我们该如何添加动画呢? - Sam
@Sam 如果你在折叠/展开方法中使用类似 [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationFade]; 这样的东西,它应该可以很好地进行动画。 - William Denniss

5
这是我发现的创建可扩展表视图单元格的最佳方法。
.h文件
  NSMutableIndexSet *expandedSections;

.m file

if (!expandedSections)
    {
        expandedSections = [[NSMutableIndexSet alloc] init];
    }
   UITableView *masterTable = [[UITableView alloc] initWithFrame:CGRectMake(0,100,1024,648) style:UITableViewStyleGrouped];
    masterTable.delegate = self;
    masterTable.dataSource = self;
    [self.view addSubview:masterTable];

表格视图代理方法

- (BOOL)tableView:(UITableView *)tableView canCollapseSection:(NSInteger)section
{
    // if (section>0) return YES;

    return YES;
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    // Return the number of sections.
    return 4;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    if ([self tableView:tableView canCollapseSection:section])
    {
        if ([expandedSections containsIndex:section])
        {
            return 5; // return rows when expanded
        }

        return 1; // only top row showing
    }

    // Return the number of rows in the section.
    return 1;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] ;
    }

    // Configure the cell...

    if ([self tableView:tableView canCollapseSection:indexPath.section])
    {
        if (!indexPath.row)
        {
            // first row
            cell.textLabel.text = @"Expandable"; // only top row showing

            if ([expandedSections containsIndex:indexPath.section])
            {

                UIImageView *imView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"UITableContract"]];
                cell.accessoryView = imView;
            }
            else
            {

                UIImageView *imView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"UITableExpand"]];
                cell.accessoryView = imView;
            }
        }
        else
        {
            // all other rows
            if (indexPath.section == 0) {
                cell.textLabel.text = @"section one";
            }else if (indexPath.section == 1) {
                cell.textLabel.text = @"section 2";
            }else if (indexPath.section == 2) {
                cell.textLabel.text = @"3";
            }else {
                cell.textLabel.text = @"some other sections";
            }

            cell.accessoryView = nil;
            cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
        }
    }
    else
    {
        cell.accessoryView = nil;
        cell.textLabel.text = @"Normal Cell";

    }

    return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if ([self tableView:tableView canCollapseSection:indexPath.section])
    {
        if (!indexPath.row)
        {
            // only first row toggles exapand/collapse
            [tableView deselectRowAtIndexPath:indexPath animated:YES];

            NSInteger section = indexPath.section;
            BOOL currentlyExpanded = [expandedSections containsIndex:section];
            NSInteger rows;


            NSMutableArray *tmpArray = [NSMutableArray array];

            if (currentlyExpanded)
            {
                rows = [self tableView:tableView numberOfRowsInSection:section];
                [expandedSections removeIndex:section];

            }
            else
            {
                [expandedSections addIndex:section];
                rows = [self tableView:tableView numberOfRowsInSection:section];
            }


            for (int i=1; i<rows; i++)
            {
                NSIndexPath *tmpIndexPath = [NSIndexPath indexPathForRow:i 
                                                               inSection:section];
                [tmpArray addObject:tmpIndexPath];
            }

            UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];

            if (currentlyExpanded)
            {
                [tableView deleteRowsAtIndexPaths:tmpArray 
                                 withRowAnimation:UITableViewRowAnimationTop];

                UIImageView *imView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"UITableExpand"]];
                cell.accessoryView = imView;
            }
            else
            {
                [tableView insertRowsAtIndexPaths:tmpArray 
                                 withRowAnimation:UITableViewRowAnimationTop];

                UIImageView *imView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"UITableContract"]];
                cell.accessoryView = imView;
            }
        }
    }

    NSLog(@"section :%d,row:%d",indexPath.section,indexPath.row);

}

8
你应该将问题标记为完全重复,而不是在所有问题上都发布相同的答案。 - casperOne
如果一个部分已经展开,而另一个部分被点击,则会出现错误。 - shivam
您好,如何更改所选索引的高度?在您的代码中如何使用heightForRowAtIndexPath? - Gami Nilesh
您好,如何在展开行的didselected事件中导航到另一个视图控制器? - Arbaz Shaikh

1
我已经使用了NSDictionary作为数据源,这看起来像是很多代码,但实际上非常简单且运行良好! 在这里看看它是什么样子 我创建了一个枚举用于表示各个部分。
typedef NS_ENUM(NSUInteger, TableViewSection) {

    TableViewSection0 = 0,
    TableViewSection1,
    TableViewSection2,
    TableViewSectionCount
};

sections属性:

@property (nonatomic, strong) NSMutableDictionary * sectionsDisctionary;

一个返回我的部分的方法:
-(NSArray <NSNumber *> * )sections{

    return @[@(TableViewSection0), @(TableViewSection1), @(TableViewSection2)];
}

然后设置我的数据源:
-(void)loadAndSetupData{

    self.sectionsDisctionary = [NSMutableDictionary dictionary];

    NSArray * sections = [self sections];

    for (NSNumber * section in sections) {

    NSArray * sectionObjects = [self objectsForSection:section.integerValue];

    [self.sectionsDisctionary setObject:[NSMutableDictionary dictionaryWithDictionary:@{@"visible" : @YES, @"objects" : sectionObjects}] forKey:section];
    }
}

-(NSArray *)objectsForSection:(NSInteger)section{

    NSArray * objects;

    switch (section) {

        case TableViewSection0:

            objects = @[] // objects for section 0;
            break;

        case TableViewSection1:

            objects = @[] // objects for section 1;
            break;

        case TableViewSection2:

            objects = @[] // objects for section 2;
            break;

        default:
            break;
    }

    return objects;
}

下面的方法将帮助您了解何时打开一个部分,以及如何响应tableview数据源:
响应数据源中的部分:
/**
 *  Asks the delegate for a view object to display in the header of the specified section of the table view.
 *
 *  @param tableView The table-view object asking for the view object.
 *  @param section   An index number identifying a section of tableView .
 *
 *  @return A view object to be displayed in the header of section .
 */
- (UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{

    NSString * headerName = [self titleForSection:section];

    YourCustomSectionHeaderClass * header = (YourCustomSectionHeaderClass *)[tableView dequeueReusableHeaderFooterViewWithIdentifier:YourCustomSectionHeaderClassIdentifier];

    [header setTag:section];
    [header addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]];
    header.title = headerName;
    header.collapsed = [self sectionIsOpened:section];


    return header;
}

/**
 * Asks the data source to return the number of sections in the table view
 *
 * @param An object representing the table view requesting this information.
 * @return The number of sections in tableView.
 */
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
    // Return the number of sections.

    return self.sectionsDisctionary.count;
}

/**
 * Tells the data source to return the number of rows in a given section of a table view
 *
 * @param tableView: The table-view object requesting this information.
 * @param section: An index number identifying a section in tableView.
 * @return The number of rows in section.
 */
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{

    BOOL sectionOpened = [self sectionIsOpened:section];
    return sectionOpened ? [[self objectsForSection:section] count] : 0;
}

工具:

/**
 Return the section at the given index

 @param index the index

 @return The section in the given index
 */
-(NSMutableDictionary *)sectionAtIndex:(NSInteger)index{

    NSString * asectionKey = [self.sectionsDisctionary.allKeys objectAtIndex:index];

    return [self.sectionsDisctionary objectForKey:asectionKey];
}

/**
 Check if a section is currently opened

 @param section the section to check

 @return YES if is opened
 */
-(BOOL)sectionIsOpened:(NSInteger)section{

    NSDictionary * asection = [self sectionAtIndex:section];
    BOOL sectionOpened = [[asection objectForKey:@"visible"] boolValue];

    return sectionOpened;
}


/**
 Handle the section tap

 @param tap the UITapGestureRecognizer
 */
- (void)handleTapGesture:(UITapGestureRecognizer*)tap{

    NSInteger index = tap.view.tag;

    [self toggleSection:index];
}

切换部分可见性

/**
 Switch the state of the section at the given section number

 @param section the section number
 */
-(void)toggleSection:(NSInteger)section{

    if (index >= 0){

        NSMutableDictionary * asection = [self sectionAtIndex:section];

        [asection setObject:@(![self sectionIsOpened:section]) forKey:@"visible"];

        [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:section] withRowAnimation:UITableViewRowAnimationFade];
    }
}

1

因此,基于“标题中的按钮”解决方案,这里有一个干净且极简的实现:

  • 您在属性中跟踪折叠(或展开)的部分
  • 您使用部分索引标记按钮
  • 您设置选定状态的按钮以更改箭头方向(如△和▽)

以下是代码:

@interface MyTableViewController ()
@property (nonatomic, strong) NSMutableIndexSet *collapsedSections;
@end

...

@implementation MyTableViewController

- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (!self)
        return;
    self.collapsedSections = [NSMutableIndexSet indexSet];
    return self;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // if section is collapsed
    if ([self.collapsedSections containsIndex:section])
        return 0;

    // if section is expanded
#warning incomplete implementation
    return [super tableView:tableView numberOfRowsInSection:section];
}

- (IBAction)toggleSectionHeader:(UIView *)sender
{
    UITableView *tableView = self.tableView;
    NSInteger section = sender.tag;

    MyTableViewHeaderFooterView *headerView = (MyTableViewHeaderFooterView *)[self tableView:tableView viewForHeaderInSection:section];

    if ([self.collapsedSections containsIndex:section])
    {
        // section is collapsed
        headerView.button.selected = YES;
        [self.collapsedSections removeIndex:section];
    }
    else
    {
        // section is expanded
        headerView.button.selected = NO;
        [self.collapsedSections addIndex:section];
    }

    [tableView beginUpdates];
    [tableView reloadSections:[NSIndexSet indexSetWithIndex:section] withRowAnimation:UITableViewRowAnimationAutomatic];
    [tableView endUpdates];
}

@end

1
我发现了另一种相对简单的解决方法。通过使用这种方法,我们不需要修改单元格,因为它几乎总是与数据数组索引相关联,可能会在我们的视图控制器中引起混乱。
首先,我们将以下属性添加到我们的控制器类中:
@property (strong, nonatomic) NSMutableArray* collapsedSections;
@property (strong, nonatomic) NSMutableArray* sectionViews;

collapsedSections将保存折叠部分的编号。 sectionViews将存储我们的自定义部分视图。

综合它:

@synthesize collapsedSections;
@synthesize sectionViews;

初始化它:

- (void) viewDidLoad
{
    [super viewDidLoad];

    self.collapsedSections = [NSMutableArray array];
    self.sectionViews      = [NSMutableArray array];
}

之后,我们必须连接UITableView,以便可以从我们的视图控制器类中访问它:
@property (strong, nonatomic) IBOutlet UITableView *tblMain;

通常情况下,使用 ctrl + drag 将其从 XIB 连接到视图控制器。

然后,通过实现 UITableView 委托来创建自定义的表格视图段头视图:

- (UIView*) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
    // Create View
    CGRect frame = CGRectZero;

    frame.origin = CGPointZero;

    frame.size.height = 30.f;
    frame.size.width  = tableView.bounds.size.width;

    UIView* view = [[UIView alloc] initWithFrame:frame];

    [view setBackgroundColor:[UIColor blueColor]];

    // Add label for title
    NSArray* titles = @[@"Title 1", @"Title 2", @"Title 3"];

    NSString* selectedTitle = [titles objectAtIndex:section];

    CGRect labelFrame = frame;

    labelFrame.size.height = 30.f;
    labelFrame.size.width -= 20.f;
    labelFrame.origin.x += 10.f;

    UILabel* titleLabel = [[UILabel alloc] initWithFrame:labelFrame];

    [titleLabel setText:selectedTitle];
    [titleLabel setTextColor:[UIColor whiteColor]];

    [view addSubview:titleLabel];

    // Add touch gesture
    [self attachTapGestureToView:view];

    // Save created view to our class property array
    [self saveSectionView:view inSection:section];

    return view;
}

接下来,我们实现一个方法将之前创建的自定义节标题保存到类属性中:
- (void) saveSectionView:(UIView*) view inSection:(NSInteger) section
{
    NSInteger sectionCount = [self numberOfSectionsInTableView:[self tblMain]];

    if(section < sectionCount)
    {
        if([[self sectionViews] indexOfObject:view] == NSNotFound)
        {
            [[self sectionViews] addObject:view];
        }
    }
}

UIGestureRecognizerDelegate添加到我们的视图控制器.h文件中:

@interface MyViewController : UIViewController<UITableViewDelegate, UITableViewDataSource, UIGestureRecognizerDelegate>

然后我们创建一个名为attachTapGestureToView:的方法。
- (void) attachTapGestureToView:(UIView*) view
{
    UITapGestureRecognizer* tapAction = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];

    [tapAction setDelegate:self];

    [view addGestureRecognizer:tapAction];
}

上面的方法将为我们之前创建的所有节视图添加轻拍手势识别器。接下来,我们应该实现 onTap: 选择器。
- (void) onTap:(UITapGestureRecognizer*) gestureRecognizer
{
    // Take view who attach current recognizer
    UIView* sectionView = [gestureRecognizer view]; 

    // [self sectionViews] is Array containing our custom section views
    NSInteger section = [self sectionNumberOfView:sectionView];

    // [self tblMain] is our connected IBOutlet table view
    NSInteger sectionCount = [self numberOfSectionsInTableView:[self tblMain]];

    // If section more than section count minus one set at last
    section = section > (sectionCount - 1) ? 2 : section;

    [self toggleCollapseSection:section];
}

当用户点击我们的表视图部分时,上述方法将被调用。该方法基于我们之前创建的sectionViews数组搜索正确的部分号码。
此外,我们实现了一个方法来获取头视图所属的部分。
- (NSInteger) sectionNumberOfView:(UIView*) view
{
    UILabel* label = [[view subviews] objectAtIndex:0];

    NSInteger sectionNum = 0;

    for(UIView* sectionView in [self sectionViews])
    {
        UILabel* sectionLabel = [[sectionView subviews] objectAtIndex:0];

        //NSLog(@"Section: %d -> %@ vs %@", sectionNum, [label text], [sectionLabel text]);

        if([[label text] isEqualToString:[sectionLabel text]])
        {
            return sectionNum;
        }

        sectionNum++;
    }

    return NSNotFound;
}

接下来,我们必须实现方法toggleCollapseSection:
- (void) toggleCollapseSection:(NSInteger) section
{
    if([self isCollapsedSection:section])
    {
        [self removeCollapsedSection:section];
    }
    else
    {
        [self addCollapsedSection:section];
    }

    [[self tblMain] reloadSections:[NSIndexSet indexSetWithIndex:section] withRowAnimation:UITableViewRowAnimationFade];
}

这个方法将会向我们之前创建的collapsedSections数组中插入/删除章节编号。当一个章节编号被插入到该数组中时,意味着该章节应该被折叠,否则就展开。
接下来,我们实现removeCollapsedSection:addCollapsedSection:sectionisCollapsedSection:section
- (BOOL)isCollapsedSection:(NSInteger) section
{
    for(NSNumber* existing in [self collapsedSections])
    {
        NSInteger current = [existing integerValue];

        if(current == section)
        {
            return YES;
        }
    }

    return NO;
}

- (void)removeCollapsedSection:(NSInteger) section
{
    [[self collapsedSections] removeObjectIdenticalTo:[NSNumber numberWithInteger:section]];
}

- (void)addCollapsedSection:(NSInteger) section
{
    [[self collapsedSections] addObject:[NSNumber numberWithInteger:section]];
}

这三个方法只是帮助我们更轻松地访问 collapsedSections 数组的辅助函数。
最后,实现这个表视图委托,使我们的自定义部分视图看起来更好。
- (CGFloat) tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
    return 30.f; // Same as each custom section view height
}

希望能有所帮助。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接