iOS UITableView 中的下拉列表
Drop-Down List in UITableView in iOS
如何在 iOS 中创建这种类型的表视图??
在这里,如果我们点击第 1 行 'Account',它会自动滚动更多行,显示在图像中。
如果我们再次点击帐户,则该视图将被隐藏。
通常我通过设置行高来实现。例如,您有两个带有下拉列表的菜单项:
- 菜单 1
- 项目 1.1
- 项目 1.2
- 项目 1.3
- 菜单 2
- 项目 2.1
- 项目 2.2
因此您必须创建一个包含 2 个部分的 table 视图。第一部分包含 4 行(菜单 1 及其项目),第二部分包含 3 行(菜单 2 及其项目)。
您始终只为部分的第一行设置高度。如果用户点击第一行,您可以通过设置高度展开此部分行并重新加载此部分。
如果您不喜欢使用任何外部库,那么您可以制作 2 个自定义单元格。一个在扩展前显示,另一个在扩展后显示(具有不同的标识符)。当您单击该单元格时,检查该单元格是否已展开。如果不是,则使用扩展单元标识符,否则使用非扩展单元标识符。
这是制作展开的 table 视图单元格的最佳且干净的方法。
执行此操作的简单方法是使用 UITableView 部分 header 作为单元格-> 并将行数设置为 0,将 section.count 用于折叠和展开状态。
.This is TableViewSection Header, isExpand -> section.count else
return0.
-正常细胞
-正常细胞
-正常细胞
.This is TableViewSection Header, isExpand -> section.count else
return0.
-正常细胞
-正常细胞
-正常细胞
您需要一个可折叠的 TableView。为了实现这一点,在您的 TableView 中,您必须跟踪哪些部分折叠(收缩),哪些部分展开。为此,您需要维护一组展开部分的索引,或一个布尔数组,其中每个索引的值指示相应部分是否展开。在为特定行分配高度时检查特定索引处的值。查看 this link 以获得更多帮助。
您可以了解分段 TableViews here。
Github 上提供了第三方库,可以让您远离喧嚣。看一下
CollapsableTableView or CollapsableTable-Swift
您可以将帐户作为一个单元格,点击它会展开以显示三个按钮("Profile"、"Activate Account"、"Change Password"),但这会产生一个问题:在每个按钮周围点击这三个按钮将算作 "user selected the Account cell" 并触发 -tableView:didSelectRowAtIndexPath:
,结果是单元格的 expand/collapse。
或者您可以将每个隐藏选项("Profile"、"Activate Account"、"Change Password")设为单独的 table 视图单元格。但我不知道如何将三个单元格 作为一个整体 展开和收缩进行动画处理(而不是分别从零高度展开到完全展开)。
所以,也许最好的解决办法是:
- 有 even 个单元格(索引:0、2、4...)来完成 "Menu title" 和 "Toggle menu open/close" 的角色(朝向下面描述的相关奇数单元格)。
- 交错(最初折叠的)"menu body" 个单元格,每个单元格每个选项有一个按钮(例如 "Profile"、"Activate Account"、"Change Password"),垂直布局,在奇数索引(1、3、5 ...)。使用 target-action 响应用户选择每个 option/button.
- 实现table视图委托方法,以便只有偶数单元格(菜单headers)被选择table,并将选择逻辑实现到expand/collapse相应的奇数单元格(在 -tableView:didSelectRowAtIndexPath: 内)。例如,选择索引 0 ("Account") 处的单元格会导致 expanding/collapsing 索引 1 处的单元格(带有选项 "Profile"、"Activate Account"、"Change Password" 的菜单) .
这不是 UITableView 最优雅的用法,但可以完成工作。
您可以轻松地将单元格设置为看起来像 header,并设置 tableView: didSelectRowAtIndexPath
以手动展开或折叠它所在的部分。 如果我存储一个布尔值数组,对应于每个部分的 "expended" 值。然后,您可以让每个自定义 header 行上的 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
值和 return 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
更新为 return 任何 section
中第一行的自定义 header 单元格。
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
是提供 "own custom header" 的更好方式,因为这正是它的设计目的。
有关详细信息,请参阅此 Answer or this PKCollapsingTableViewSections。
此外,您可以使用 setIndentationLevel
获得此类表格视图。此示例请参考此 DemoCode。我认为这是 Drop-Down tableviews 的最佳解决方案。
如果你想做一个简单的header和单元格下拉,那么请参考STCollapseTableView。
希望,这就是您要找的。有任何疑虑,请回复我。 :)
如果通过 table 视图单元格,则实现这一点的更简单和最自然的方法。没有展开的单元格视图,没有 headers 部分,简单的单元格(毕竟我们在 table 视图中)。
设计如下:
- 使用 MVVM 方法,创建一个
CollapsableViewModel
class 来保存配置单元格所需的信息:标签、图像
- 除了上面那个,还有两个额外的字段:
children
,它是一个CollapsableViewModel
objects的数组,和isCollapsed
,它保存着状态下拉菜单
- 视图控制器包含对
CollapsableViewModel
层次结构的引用,以及包含将在屏幕上呈现的视图模型的平面列表(displayedRows
属性 )
- 每当点击一个单元格时,检查它是否有 children,并通过
insertRowsAtIndexPaths()
在 displayedRows
和 table 视图中添加或删除行和 deleteRowsAtIndexPaths()
个函数。
Swift代码如下(注意代码只使用了view model的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: [=10=], 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 版本只是因为它更短且更易读。
通过几处小改动,代码可用于生成多级下拉列表。
编辑
人们问我关于分隔符的问题,这可以通过添加一个自定义的 class CollapsibleTableViewCell
来实现,它配置了一个视图模型(最后,将单元格配置逻辑从控制器移动到哪里它属于 - 细胞)。仅部分单元格的分隔符逻辑归功于回答 this SO 问题的人。
首先,更新模型,添加一个 needsSeparator
属性 告诉 table 视图单元格是否呈现分隔符:
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:
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
则需要修改为return这种单元格:
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
}
最后一步,删除默认的 table 视图单元格分隔符 - 来自 xib 或代码 (tableView.separatorStyle = .none
)。
根据@sticker 的回答,您可以绑定运行时
objc_setAssociatedObject
用于章节索引,并使用他的逻辑。在 header 视图上使用触屏时,您可以获得部分索引
objc_getAssociatedObject.
UITapGestureRecognizer *singleTapRecogniser = [[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(gestureHandler:)] autorelease];
[singleTapRecogniser setDelegate:self];
singleTapRecogniser.numberOfTouchesRequired = 1;
singleTapRecogniser.numberOfTapsRequired = 1;
[sectionHeaderView addGestureRecognizer:singleTapRecogniser];
如果你想要任何第三方库,那么你可以尝试this解决方案。
这是一个基于 MVC 的解决方案。
Create a Model class ClsMenuGroup for your Sections
class ClsMenuGroup: NSObject {
// We can also add Menu group's name and other details here.
var isSelected:Bool = false
var arrMenus:[ClsMenu]!
}
Create a Model class ClsMenu for your Rows
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
}
}
Create groups array in your 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 的数据源方法。您可以从 Github.
中找到 HDTableDataSource 的示例
Advantages of above code is
- You can anytime change the order of any menu or section or interchange menu and section, without changing other functions.
- You will not need to add long code of else if ladder in your tableview's delegate methods
- You can specify icon, title or other attribute for your menu item separately like adding badge count, changing selected menu's color
etc.
- You may also use multiple cells or sections by applying minor changes to existing code
@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];
}
}
希望对您有所帮助:)
tree-view 没有像 iOS 框架中的视图那样的内置控件 - UIKit。正如其他用户指出的那样,可能最简单的解决方案(不使用任何外部库)是向 UITableView
的委托和数据源添加一些自定义逻辑以模仿所需的行为。
幸运的是,有一些开源库允许您实现所需的树视图,如视图,而无需担心 expand/collapse 操作的细节。 iOS 平台有 couple of them 可用。在大多数情况下,这些库只是包装 UITableView
并为您提供 programmer-friendly 接口,使您可以专注于您的问题,而不是树视图的实现细节。
就我个人而言,我是 RATreeView library which purpose it to minimalize the cost needed to create tree view like views on iOS. You can check out example projects (available in Objective-c and Swift) 的作者,负责检查此控件的工作方式和行为方式。使用我的控件,创建你想要的视图真的很简单:
DataObject
结构将用于保存有关树视图节点的信息——它将负责保存有关单元格标题、其图像(如果单元格有图像)及其 children(如果单元格有 children)。
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
并实现两个符合该协议的单元格。其中一个单元格将用于显示根项,另一个单元格将用于显示 children 个根项。
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
}
}
- 在视图控制器 (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]
- 接下来我们需要从
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
处理。您只需对用于配置单元格的数据负责 - 其余部分由控件本身处理。
我喜欢@Cristik 的解决方案,前段时间我遇到了同样的问题,我的解决方案遵循了相同的原则;所以这就是我根据我的要求提出的建议:
为了更通用,table 的项目不应该继承自专门用于扩展功能的 class,而是应该有一个协议来定义必要的属性
我们可以扩展的关卡数量应该没有限制。所以table可以有option,sub option,sub sub option等
table 视图应使用任何常用动画显示或隐藏单元格(无 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?
}
关于 swift 的一个很酷的事情是我们可以在协议扩展中编写一些实现,所有符合的 classes 都可以使用默认实现,所以我们可以编写 count
subscript
和 indexOf
实现以及其他一些有用的功能,例如:
extension ExpandableCellDescriptor {
var count: Int {
var total = 1
if active {
children.forEach({ total += [=13=].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
的 class 实现了 ExpandableCellDescriptor
并且它本身不显示单元格
现在,任何 class 都可以符合以前的协议,而无需从特定的 class 继承,对于 github 中的示例代码,我创建了几个 classes: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之一:
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
}
}
请注意,您可以配置单元格,它是 class 您喜欢的方式,您可以添加图像、标签、开关等;没有限制,也没有改变所需的协议。
最后在 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)
}
}
它可能看起来很多代码,但大部分只写一次,正确绘制 table 视图所需的大部分信息都存在于 CellDescriptor.swift 文件中,单元格配置代码存在于 UITableViewCell subclasses 中,而 TableViewController 本身的代码相对较少。
希望对您有所帮助。
如何在 iOS 中创建这种类型的表视图??
在这里,如果我们点击第 1 行 'Account',它会自动滚动更多行,显示在图像中。 如果我们再次点击帐户,则该视图将被隐藏。
通常我通过设置行高来实现。例如,您有两个带有下拉列表的菜单项:
- 菜单 1
- 项目 1.1
- 项目 1.2
- 项目 1.3
- 菜单 2
- 项目 2.1
- 项目 2.2
因此您必须创建一个包含 2 个部分的 table 视图。第一部分包含 4 行(菜单 1 及其项目),第二部分包含 3 行(菜单 2 及其项目)。
您始终只为部分的第一行设置高度。如果用户点击第一行,您可以通过设置高度展开此部分行并重新加载此部分。
如果您不喜欢使用任何外部库,那么您可以制作 2 个自定义单元格。一个在扩展前显示,另一个在扩展后显示(具有不同的标识符)。当您单击该单元格时,检查该单元格是否已展开。如果不是,则使用扩展单元标识符,否则使用非扩展单元标识符。
这是制作展开的 table 视图单元格的最佳且干净的方法。
执行此操作的简单方法是使用 UITableView 部分 header 作为单元格-> 并将行数设置为 0,将 section.count 用于折叠和展开状态。
.This is TableViewSection Header, isExpand -> section.count else return0.
-正常细胞
-正常细胞
-正常细胞
.This is TableViewSection Header, isExpand -> section.count else return0.
-正常细胞
-正常细胞
-正常细胞
您需要一个可折叠的 TableView。为了实现这一点,在您的 TableView 中,您必须跟踪哪些部分折叠(收缩),哪些部分展开。为此,您需要维护一组展开部分的索引,或一个布尔数组,其中每个索引的值指示相应部分是否展开。在为特定行分配高度时检查特定索引处的值。查看 this link 以获得更多帮助。
您可以了解分段 TableViews here。
Github 上提供了第三方库,可以让您远离喧嚣。看一下 CollapsableTableView or CollapsableTable-Swift
您可以将帐户作为一个单元格,点击它会展开以显示三个按钮("Profile"、"Activate Account"、"Change Password"),但这会产生一个问题:在每个按钮周围点击这三个按钮将算作 "user selected the Account cell" 并触发 -tableView:didSelectRowAtIndexPath:
,结果是单元格的 expand/collapse。
或者您可以将每个隐藏选项("Profile"、"Activate Account"、"Change Password")设为单独的 table 视图单元格。但我不知道如何将三个单元格 作为一个整体 展开和收缩进行动画处理(而不是分别从零高度展开到完全展开)。
所以,也许最好的解决办法是:
- 有 even 个单元格(索引:0、2、4...)来完成 "Menu title" 和 "Toggle menu open/close" 的角色(朝向下面描述的相关奇数单元格)。
- 交错(最初折叠的)"menu body" 个单元格,每个单元格每个选项有一个按钮(例如 "Profile"、"Activate Account"、"Change Password"),垂直布局,在奇数索引(1、3、5 ...)。使用 target-action 响应用户选择每个 option/button.
- 实现table视图委托方法,以便只有偶数单元格(菜单headers)被选择table,并将选择逻辑实现到expand/collapse相应的奇数单元格(在 -tableView:didSelectRowAtIndexPath: 内)。例如,选择索引 0 ("Account") 处的单元格会导致 expanding/collapsing 索引 1 处的单元格(带有选项 "Profile"、"Activate Account"、"Change Password" 的菜单) .
这不是 UITableView 最优雅的用法,但可以完成工作。
您可以轻松地将单元格设置为看起来像 header,并设置 tableView: didSelectRowAtIndexPath
以手动展开或折叠它所在的部分。 如果我存储一个布尔值数组,对应于每个部分的 "expended" 值。然后,您可以让每个自定义 header 行上的 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
值和 return 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
更新为 return 任何 section
中第一行的自定义 header 单元格。
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
是提供 "own custom header" 的更好方式,因为这正是它的设计目的。
有关详细信息,请参阅此 Answer or this PKCollapsingTableViewSections。
此外,您可以使用 setIndentationLevel
获得此类表格视图。此示例请参考此 DemoCode。我认为这是 Drop-Down tableviews 的最佳解决方案。
如果你想做一个简单的header和单元格下拉,那么请参考STCollapseTableView。
希望,这就是您要找的。有任何疑虑,请回复我。 :)
如果通过 table 视图单元格,则实现这一点的更简单和最自然的方法。没有展开的单元格视图,没有 headers 部分,简单的单元格(毕竟我们在 table 视图中)。
设计如下:
- 使用 MVVM 方法,创建一个
CollapsableViewModel
class 来保存配置单元格所需的信息:标签、图像 - 除了上面那个,还有两个额外的字段:
children
,它是一个CollapsableViewModel
objects的数组,和isCollapsed
,它保存着状态下拉菜单 - 视图控制器包含对
CollapsableViewModel
层次结构的引用,以及包含将在屏幕上呈现的视图模型的平面列表(displayedRows
属性 ) - 每当点击一个单元格时,检查它是否有 children,并通过
insertRowsAtIndexPaths()
在displayedRows
和 table 视图中添加或删除行和deleteRowsAtIndexPaths()
个函数。
Swift代码如下(注意代码只使用了view model的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: [=10=], 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 版本只是因为它更短且更易读。
通过几处小改动,代码可用于生成多级下拉列表。
编辑
人们问我关于分隔符的问题,这可以通过添加一个自定义的 class CollapsibleTableViewCell
来实现,它配置了一个视图模型(最后,将单元格配置逻辑从控制器移动到哪里它属于 - 细胞)。仅部分单元格的分隔符逻辑归功于回答 this SO 问题的人。
首先,更新模型,添加一个 needsSeparator
属性 告诉 table 视图单元格是否呈现分隔符:
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:
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
则需要修改为return这种单元格:
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
}
最后一步,删除默认的 table 视图单元格分隔符 - 来自 xib 或代码 (tableView.separatorStyle = .none
)。
根据@sticker 的回答,您可以绑定运行时
objc_setAssociatedObject
用于章节索引,并使用他的逻辑。在 header 视图上使用触屏时,您可以获得部分索引
objc_getAssociatedObject.
UITapGestureRecognizer *singleTapRecogniser = [[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(gestureHandler:)] autorelease];
[singleTapRecogniser setDelegate:self];
singleTapRecogniser.numberOfTouchesRequired = 1;
singleTapRecogniser.numberOfTapsRequired = 1;
[sectionHeaderView addGestureRecognizer:singleTapRecogniser];
如果你想要任何第三方库,那么你可以尝试this解决方案。
这是一个基于 MVC 的解决方案。
Create a Model class ClsMenuGroup for your Sections
class ClsMenuGroup: NSObject {
// We can also add Menu group's name and other details here.
var isSelected:Bool = false
var arrMenus:[ClsMenu]!
}
Create a Model class ClsMenu for your Rows
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
}
}
Create groups array in your 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 的数据源方法。您可以从 Github.
中找到 HDTableDataSource 的示例Advantages of above code is
- You can anytime change the order of any menu or section or interchange menu and section, without changing other functions.
- You will not need to add long code of else if ladder in your tableview's delegate methods
- You can specify icon, title or other attribute for your menu item separately like adding badge count, changing selected menu's color etc.
- You may also use multiple cells or sections by applying minor changes to existing code
@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];
}
}
希望对您有所帮助:)
tree-view 没有像 iOS 框架中的视图那样的内置控件 - UIKit。正如其他用户指出的那样,可能最简单的解决方案(不使用任何外部库)是向 UITableView
的委托和数据源添加一些自定义逻辑以模仿所需的行为。
幸运的是,有一些开源库允许您实现所需的树视图,如视图,而无需担心 expand/collapse 操作的细节。 iOS 平台有 couple of them 可用。在大多数情况下,这些库只是包装 UITableView
并为您提供 programmer-friendly 接口,使您可以专注于您的问题,而不是树视图的实现细节。
就我个人而言,我是 RATreeView library which purpose it to minimalize the cost needed to create tree view like views on iOS. You can check out example projects (available in Objective-c and Swift) 的作者,负责检查此控件的工作方式和行为方式。使用我的控件,创建你想要的视图真的很简单:
DataObject
结构将用于保存有关树视图节点的信息——它将负责保存有关单元格标题、其图像(如果单元格有图像)及其 children(如果单元格有 children)。
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
并实现两个符合该协议的单元格。其中一个单元格将用于显示根项,另一个单元格将用于显示 children 个根项。
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
}
}
- 在视图控制器 (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]
- 接下来我们需要从
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
处理。您只需对用于配置单元格的数据负责 - 其余部分由控件本身处理。
我喜欢@Cristik 的解决方案,前段时间我遇到了同样的问题,我的解决方案遵循了相同的原则;所以这就是我根据我的要求提出的建议:
为了更通用,table 的项目不应该继承自专门用于扩展功能的 class,而是应该有一个协议来定义必要的属性
我们可以扩展的关卡数量应该没有限制。所以table可以有option,sub option,sub sub option等
table 视图应使用任何常用动画显示或隐藏单元格(无
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?
}
关于 swift 的一个很酷的事情是我们可以在协议扩展中编写一些实现,所有符合的 classes 都可以使用默认实现,所以我们可以编写 count
subscript
和 indexOf
实现以及其他一些有用的功能,例如:
extension ExpandableCellDescriptor {
var count: Int {
var total = 1
if active {
children.forEach({ total += [=13=].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
的 class 实现了 ExpandableCellDescriptor
并且它本身不显示单元格
现在,任何 class 都可以符合以前的协议,而无需从特定的 class 继承,对于 github 中的示例代码,我创建了几个 classes: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之一:
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
}
}
请注意,您可以配置单元格,它是 class 您喜欢的方式,您可以添加图像、标签、开关等;没有限制,也没有改变所需的协议。
最后在 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)
}
}
它可能看起来很多代码,但大部分只写一次,正确绘制 table 视图所需的大部分信息都存在于 CellDescriptor.swift 文件中,单元格配置代码存在于 UITableViewCell subclasses 中,而 TableViewController 本身的代码相对较少。
希望对您有所帮助。