Skip to content

Commit 94c5ec6

Browse files
authored
Update README.md
1 parent a31c661 commit 94c5ec6

File tree

1 file changed

+134
-53
lines changed

1 file changed

+134
-53
lines changed

README.md

Lines changed: 134 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
# How to Implement Collapsible Table Section in iOS
2-
A simple iOS swift project demonstrates how to implement collapsible table section.
32

4-
[![Language](https://img.shields.io/badge/swift-2.3-brightgreen.svg?style=flat)]()
3+
A simple iOS swift project demonstrates how to implement collapsible table section programmatically, that is no main storyboard, no XIB, no need to register nib, just purley Swift!
54

6-
### Coming Soon ###
7-
- Use `UITableViewHeaderFooterView` instead of `UITableViewCell` according to [Apple API reference](https://developer.apple.com/reference/uikit/uitableviewheaderfooterview).
8-
- Auto layout programmatically
9-
- Remove storyboardy
5+
[![Language](https://img.shields.io/badge/swift-2.3-brightgreen.svg?style=flat)]()
106

117
### Demo ###
12-
![demo](screenshots/demo.gif)<br />
8+
9+
<img src="screenshots/demo.gif" width="400px">
1310

1411
### How to implement collapsible table sections? ###
1512

@@ -40,37 +37,104 @@ sections = [
4037
```
4138
`collapsed` indicates whether the current section is collapsed or not, by default is `false`.
4239

43-
#### Step 2. Design the Header and Cell ####
40+
#### Step 2. The Section Header ####
4441

45-
Select the `Table View` in the story board, choose `Dynamic Prototypes` and set `Prototype Cells` to `2`, one for the custom header and one for the row cell, and assign the `Identifier` to `header` and `cell` respectively.
42+
According to [Apple API reference](https://developer.apple.com/reference/uikit/uitableviewheaderfooterview), we should use `UITableViewHeaderFooterView`. Let's subclass it and implement the section header `CollapsibleTableViewHeader`:
4643

47-
![cell](screenshots/cell.png)<br />
48-
49-
Add a UIButton (the toggler) and a Label to the header prototype cell, create a swift file which extends `UITableViewCell` and name it `CollapsibleTableViewHeader.swift`. The file is super simple, it defines two IBOutlets for the toggle button and label. Finally set the header cell class to our custom header `CollapsibleTableViewHeader` and link the IBOutlets.
44+
```swift
45+
class CollapsibleTableViewHeader: UITableViewHeaderFooterView {
46+
let titleLabel = UILabel()
47+
let arrowLabel = UILabel()
48+
49+
override init(reuseIdentifier: String?) {
50+
super.init(reuseIdentifier: reuseIdentifier)
51+
52+
contentView.addSubview(titleLabel)
53+
contentView.addSubview(arrowLabel)
54+
}
55+
56+
required init?(coder aDecoder: NSCoder) {
57+
fatalError("init(coder:) has not been implemented")
58+
}
59+
}
60+
```
5061

51-
Now the file should look like this:
62+
We need to collapse or expand the section when user taps on the header, to achieve this, let's borrow `UITapGestureRecognizer`. Also we need to delegate this event to the table view to update the `collapsed` property.
5263

5364
```swift
54-
import UIKit
65+
protocol CollapsibleTableViewHeaderDelegate {
66+
func toggleSection(header: CollapsibleTableViewHeader, section: Int)
67+
}
5568

56-
class CollapsibleTableViewHeader: UITableViewCell {
57-
58-
@IBOutlet var titleLabel: UILabel!
59-
@IBOutlet var toggleButton: UIButton!
69+
class CollapsibleTableViewHeader: UITableViewHeaderFooterView {
70+
var delegate: CollapsibleTableViewHeaderDelegate?
71+
var section: Int = 0
72+
...
73+
override init(reuseIdentifier: String?) {
74+
super.init(reuseIdentifier: reuseIdentifier)
75+
...
76+
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(CollapsibleTableViewHeader.tapHeader(_:))))
77+
}
78+
...
79+
func tapHeader(gestureRecognizer: UITapGestureRecognizer) {
80+
guard let cell = gestureRecognizer.view as? CollapsibleTableViewHeader else {
81+
return
82+
}
83+
delegate?.toggleSection(self, section: cell.section)
84+
}
6085

86+
func setCollapsed(collapsed: Bool) {
87+
// Animate the arrow rotation (see Extensions.swf)
88+
arrowLabel.rotate(collapsed ? 0.0 : CGFloat(M_PI_2))
89+
}
6190
}
6291
```
6392

64-
By creating a prototype cell and subclassing UITableViewCell, we have the following benefits:
65-
* We can visually design the custom header
66-
* We shouldn't need to create a nib and register it to the the tableView like so:
93+
Since we are not using any storyboard or XIB, how to do auto layout programmatically? The answer is `NSLayoutConstraint`'s `constraintsWithVisualFormat` function.
94+
6795
```swift
68-
let nib = UINib(nibName: "TableSectionHeader", bundle: nil)
69-
tableView.registerNib(nib, forHeaderFooterViewReuseIdentifier: "TableSectionHeader")
96+
override init(reuseIdentifier: String?) {
97+
...
98+
// arrowLabel must have fixed width and height
99+
arrowLabel.widthAnchor.constraintEqualToConstant(12).active = true
100+
arrowLabel.heightAnchor.constraintEqualToConstant(12).active = true
101+
102+
titleLabel.translatesAutoresizingMaskIntoConstraints = false
103+
arrowLabel.translatesAutoresizingMaskIntoConstraints = false
104+
}
105+
106+
override func layoutSubviews() {
107+
super.layoutSubviews()
108+
...
109+
let views = [
110+
"titleLabel" : titleLabel,
111+
"arrowLabel" : arrowLabel,
112+
]
113+
114+
contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat(
115+
"H:|-20-[titleLabel]-[arrowLabel]-20-|",
116+
options: [],
117+
metrics: nil,
118+
views: views
119+
))
120+
121+
contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat(
122+
"V:|-[titleLabel]-|",
123+
options: [],
124+
metrics: nil,
125+
views: views
126+
))
127+
128+
contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat(
129+
"V:|-[arrowLabel]-|",
130+
options: [],
131+
metrics: nil,
132+
views: views
133+
))
134+
}
70135
```
71-
personally I don't like having nibs in my project and if we use `dequeueReusableHeaderFooterViewWithIdentifier`, seems like we must have at least 1 row in that section, but we need to have 0 row!
72136

73-
#### Step 3. The UITableViewDelegate ####
137+
#### Step 3. The UITableView DataSource and Delegate ####
74138

75139
First the number of sections is `sections.count`:
76140

@@ -80,59 +144,76 @@ override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
80144
}
81145
```
82146

83-
For the number of rows in each section, we use `collapsed` property to control it, if `collapsed` is true, then return 0, otherwise return items count:
147+
and the number of rows in each section is:
84148

85149
```swift
86150
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
87-
return (sections[section].collapsed!) ? 0 : sections[section].items.count
151+
return sections[section].items.count
88152
}
89153
```
90154

91155
We use tableView's viewForHeaderInSection function to hook up our custom header:
92156

93157
```swift
94158
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
95-
let header = tableView.dequeueReusableCellWithIdentifier("header") as! CollapsibleTableViewHeader
96-
97-
header.titleLabel.text = sections[section].name
98-
header.toggleButton.tag = section
99-
header.toggleButton.addTarget(self, action: #selector(CollapsibleTableViewController.toggleCollapse), forControlEvents: .TouchUpInside)
100-
101-
header.toggleButton.rotate(sections[section].collapsed! ? 0.0 : CGFloat(M_PI_2))
102-
103-
return header.contentView
159+
let header = tableView.dequeueReusableHeaderFooterViewWithIdentifier("header") as? CollapsibleTableViewHeader ?? CollapsibleTableViewHeader(reuseIdentifier: "header")
160+
161+
header.titleLabel.text = sections[section].name
162+
header.arrowLabel.text = ">"
163+
header.setCollapsed(sections[section].collapsed)
164+
165+
header.section = section
166+
header.delegate = self
167+
168+
return header
104169
}
105170
```
106171

107-
noticed that we register the touch up inside event for the toggler, once it's tapped, it will trigger the `toggleCollapse` function.
108-
109-
Last, the normal row cell is pretty straightforward:
172+
The normal row cell is pretty straightforward:
110173

111174
```swift
112175
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
113-
let cell = tableView.dequeueReusableCellWithIdentifier("cell") as UITableViewCell!
114-
115-
cell.textLabel?.text = sections[indexPath.section].items[indexPath.row]
116-
117-
return cell
176+
let cell = tableView.dequeueReusableCellWithIdentifier("cell") as UITableViewCell? ?? UITableViewCell(style: .Default, reuseIdentifier: "cell")
177+
178+
cell.textLabel?.text = sections[indexPath.section].items[indexPath.row]
179+
180+
return cell
181+
}
182+
```
183+
184+
#### Step 4. How to Toggle Collapse and Expand ####
185+
186+
The idea is really simple, if a section's `collapsed` property is `true`, we set the height of the rows inside that section to be `0`, otherwise `44.0`!
187+
188+
```swift
189+
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
190+
return sections[indexPath.section].collapsed! ? 0 : 44.0
118191
}
119192
```
120193

121194
And here is the toggle function:
122195

123196
```swift
124-
func toggleCollapse(sender: UIButton) {
125-
let section = sender.tag
126-
let collapsed = sections[section].collapsed
127-
128-
// Toggle collapse
129-
sections[section].collapsed = !collapsed
130-
131-
// Reload section
132-
tableView.reloadSections(NSIndexSet(index: section), withRowAnimation: .Automatic)
197+
extension CollapsibleTableViewController: CollapsibleTableViewHeaderDelegate {
198+
func toggleSection(header: CollapsibleTableViewHeader, section: Int) {
199+
let collapsed = !sections[section].collapsed
200+
201+
// Toggle collapse
202+
sections[section].collapsed = collapsed
203+
header.setCollapsed(collapsed)
204+
205+
// Adjust the height of the rows inside the section
206+
tableView.beginUpdates()
207+
for i in 0 ..< sections[section].items.count {
208+
tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: i, inSection: section)], withRowAnimation: .Automatic)
209+
}
210+
tableView.endUpdates()
211+
}
133212
}
134213
```
135214

215+
Noticed that we don't lazily just reload the whole section, we only reload the rows inside that section, so that we won't see the refresh of the section header, and most importantly it will allow us to animate anything in the section header smoothly, i.e., rotate the arrow label, change the background etc.
216+
136217
That's it, please refer to the source code and see the detailed implementation.
137218

138219
### More Collapsible Demo ###

0 commit comments

Comments
 (0)