你可以轻松地设置一个单元格,使其看起来像是标题,并手动设置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
是提供自己的 "自定义标题" 的更好方法,因为它正是为此而设计的。setIndentationLevel
来获得此类型的表视图。请参考此演示代码。我认为这是下拉表视图的最佳解决方案。实现这个功能最简洁、自然的方式是使用表格视图单元格。没有展开单元格视图,也没有分区标题,只有简单的单元格(毕竟我们在一个表格视图中)。
设计如下:
CollapsableViewModel
类,其中包含配置单元格所需的信息:标签、图像。children
,它是CollapsableViewModel
对象数组,和isCollapsed
,它保存下拉状态。CollapsableViewModel
层次结构的引用,以及包含要呈现在屏幕上的视图模型的平面列表(displayedRows
属性)。insertRowsAtIndexPaths()
和deleteRowsAtIndexPaths()
函数在displayedRows
和表格视图中添加或删除行。以下是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
}
}
然后,添加cell类:
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
)。
tableView: didSelectRowAtIndexPath:
中的逻辑应用于两个视图模型:当前扩展的模型和被点击的模型,就可以实现你需要的效果。一个小修改是只使用一组 beginUpdates
+ endUpdates
。 - Cristik这里提供一个基于MVC的解决方案。
创建一个名为ClsMenuGroup的模型类来表示你的章节内容。
class ClsMenuGroup: NSObject {
// We can also add Menu group's name and other details here.
var isSelected:Bool = false
var arrMenus:[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
}
}
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的数据源方法。您可以在Github上找到HDTableDataSource的示例。
以上代码的优点是:
- 您可以随时更改任何菜单或部分的顺序或交换菜单和部分,而不必更改其他功能。
- 您将不需要在tableview的委托方法中添加长代码的if else梯子。
- 您可以单独指定菜单项的图标、标题或其他属性,例如添加徽章计数、更改所选菜单的颜色等。
- 您还可以通过对现有代码进行微小更改来使用多个单元格或部分。
实现这个功能的简单方法是使用UITableView的section header作为cell,设置行数为0和section.count以实现折叠和展开状态。
这是TableView的section header,isExpand->section.count,否则返回0。
-普通cell
-普通cell
-普通cell
这是TableView的section header,isExpand->section.count,否则返回0。
-普通cell
-普通cell
-普通cell
-tableView:didSelectRowAtIndexPath:
)? - Nicolas MiariUITableView
的委托和数据源添加一些自定义逻辑,以模拟所需的行为。UITableView
,并为您提供了编程友好的接口,使您可以专注于问题而不是树形视图的实现细节。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]())
}
}
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
}
}
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]
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
控件处理。 您只需要负责配置单元格所使用的数据 - 其余部分由控件本身处理。
@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];
}
}
如果您不想使用任何外部库,那么可以制作2个自定义单元格。一个显示在展开之前,另一个显示在展开后(具有不同的标识符)。当您点击单元格时,请检查单元格是否已展开。如果没有展开,则使用展开的单元格标识符,否则使用未展开的单元格标识符。
这是制作扩展表视图单元格的最佳和清洁的方法。
-tableView:didSelectRowAtIndexPath:
导致单元格的展开/折叠。
insertRowsAtIndexPaths:withRowAnimation:
而不是reloadSections:
,因为有时候重载整个部分的动画效果不太好看。 - chedabob