Last active
November 8, 2018 13:55
-
-
Save Eridana/5fe4511c5d1c258f2af63fe8b9b13341 to your computer and use it in GitHub Desktop.
Expandable UITableView
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
class ExpandableObject { | |
public var subItems = [ExpandableObject]() | |
public var title: String? | |
public var isExpanded: Bool = false | |
public var isChild: Bool = false | |
public var cellIdentifier: String? | |
public var canExpand: Bool { | |
return self.subItems.count > 0 | |
} | |
init(_ title: String?, isChild: Bool = false) { | |
self.title = title | |
self.isChild = isChild | |
} | |
init(cellIdentifier: String?, isChild: Bool = false) { | |
self.cellIdentifier = cellIdentifier | |
self.isChild = isChild | |
} | |
} | |
protocol ExpandableCell { | |
func canExpandCell() -> Bool | |
func isCellExpanded() -> Bool | |
func setCellIsExpanded(expanded: Bool) | |
func numberOfExpandableElements() -> Int | |
} | |
protocol ExpandableTableViewDataSource { | |
func expand(_ tableView: ExpandableTableView, at indexPath: IndexPath, expand: Bool) | |
func isCellExpanded(_ tableView: ExpandableTableView, at indexPath: IndexPath) -> Bool | |
func expandableTableView(_ tableView: ExpandableTableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell | |
func expandableTableView(_ tableView: ExpandableTableView, heightForRowAt indexPath: IndexPath) -> CGFloat | |
func expandableTableView(_ tableView: ExpandableTableView, didSelectRowAt indexPath: IndexPath) | |
} | |
protocol ExpandableTableViewDelegate { | |
func scrollViewDidScroll(_ scrollView: UIScrollView) | |
} | |
class ExpandableTableView: UITableView, UITableViewDataSource, UITableViewDelegate { | |
public var customDataSource: ExpandableTableViewDataSource? | |
public var customDelegate: ExpandableTableViewDelegate? | |
public var expandableData = Dictionary<Int, [String?]>() | |
internal var cellsData = [ExpandableObject]() | |
internal var mappedCellsData: [ExpandableObject] { | |
get { | |
var newArray = [ExpandableObject]() | |
for obj in self.cellsData { | |
newArray.append(obj) | |
if obj.isExpanded { | |
for subItem in obj.subItems { | |
newArray.append(subItem) | |
if subItem.isExpanded { | |
newArray.append(contentsOf: subItem.subItems) | |
} | |
} | |
} | |
} | |
return newArray | |
} | |
} | |
override init(frame: CGRect, style: UITableViewStyle) { | |
super.init(frame: frame, style: style) | |
self.setup() | |
} | |
required init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
self.setup() | |
} | |
private func setup() { | |
self.dataSource = self | |
self.delegate = self | |
self.estimatedRowHeight = 44.0 | |
self.backgroundColor = .clear | |
} | |
public func setCellsData(_ data: [ExpandableObject]) { | |
self.cellsData = data | |
} | |
public func registerCell(with name: String) { | |
self.register(UINib(nibName: String(describing: name), bundle: nil), forCellReuseIdentifier: String(describing: name)) | |
} | |
public func object(at indexPath: IndexPath) -> ExpandableObject { | |
return self.mappedCellsData[indexPath.row] | |
} | |
public func isLast(_ indexPath: IndexPath) -> Bool { | |
return indexPath.row == self.mappedCellsData.count - 1 | |
} | |
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { | |
return self.mappedCellsData.count | |
} | |
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | |
guard let ds = self.customDataSource else { | |
return UITableViewCell() | |
} | |
return ds.expandableTableView(self, cellForRowAt:indexPath) | |
} | |
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { | |
if let cell = tableView.cellForRow(at: indexPath) as? ExpandableCell { | |
guard let ds = self.customDataSource else { | |
return | |
} | |
if cell.canExpandCell() { | |
ds.expand(self, at: indexPath, expand: !cell.isCellExpanded()) | |
self.reloadData() | |
} else { | |
ds.expandableTableView(self, didSelectRowAt: indexPath) | |
} | |
} | |
} | |
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { | |
guard let ds = self.customDataSource else { | |
return self.estimatedRowHeight | |
} | |
return ds.expandableTableView(self, heightForRowAt:indexPath) | |
} | |
func scrollViewDidScroll(_ scrollView: UIScrollView) { | |
self.customDelegate?.scrollViewDidScroll(scrollView) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
class ExpandableTVC: UITableViewCell, ExpandableCell { | |
@IBOutlet weak var cellTitleLabel: UILabel! | |
@IBOutlet weak var expandableImage: UIImageView! | |
private var cellObject: ExpandableObject? | |
override func awakeFromNib() { | |
super.awakeFromNib() | |
} | |
override func setSelected(_ selected: Bool, animated: Bool) { | |
super.setSelected(selected, animated: animated) | |
} | |
func setup(for object: ExpandableObject) { | |
self.cellObject = object | |
self.cellTitleLabel.text = self.cellObject?.title | |
UIView.animate(withDuration: 0.15) { | |
self.expandableImage.isHighlighted = (self.cellObject?.isExpanded ?? false) | |
} | |
} | |
func canExpandCell() -> Bool { | |
return self.cellObject?.canExpand ?? false | |
} | |
func isCellExpanded() -> Bool { | |
return self.cellObject?.isExpanded ?? false | |
} | |
func setCellIsExpanded(expanded: Bool) { | |
} | |
func numberOfExpandableElements() -> Int { | |
return self.cellObject?.subItems.count ?? 0 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class AnyTableCell: UITableViewCell, ExpandableCell { | |
override func awakeFromNib() { | |
super.awakeFromNib() | |
} | |
override func setSelected(_ selected: Bool, animated: Bool) { | |
super.setSelected(selected, animated: animated) | |
} | |
func setup() { | |
} | |
func canExpandCell() -> Bool { | |
return false | |
} | |
func isCellExpanded() -> Bool { | |
return false | |
} | |
func setCellIsExpanded(expanded: Bool) { | |
} | |
func numberOfExpandableElements() -> Int { | |
return 0 // or if you setup cell with object: return self.cellObject?.subItems.count ?? 0 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
func setupTableView() { | |
self.tableView.customDataSource = self | |
self.tableView.customDelegate = self | |
self.tableView.estimatedRowHeight = 0 | |
self.tableView.registerCell(with: String(describing: ExpandableTVC.self)) // base cell | |
} | |
func setupCellsData() { | |
var cellsData = [ExpandableObject]() | |
// obj1 is expandable cell with 1 not expandable child cell | |
let obj1 = ExpandableObject(cellIdentifier: String(describing: ExpandableTVC.self)) | |
obj1.title = "title1" | |
obj1.subItems.append(ExpandableObject(cellIdentifier: String(describing: AnyTVC.self), isChild: true)) | |
cellsData.append(obj1) | |
// obj1 is not expandable cell (no childs) | |
let obj2 = ExpandableObject(cellIdentifier: String(describing: ExpandableTVC.self)) | |
obj2.title = "title2" | |
cellsData.append(obj2) | |
// obj1 is expandable cell with 2 childs | |
let obj3 = ExpandableObject(cellIdentifier: String(describing: ExpandableTVC.self)) | |
obj3.title = "title3" | |
obj3.subItems.append(ExpandableObject(cellIdentifier: String(describing: AnyTVC.self), isChild: true)) | |
obj3.subItems.append(ExpandableObject(cellIdentifier: String(describing: AnyTVC.self), isChild: true)) | |
cellsData.append(obj3) | |
self.tableView.setCellsData(cellsData) | |
} | |
// MARK: - Expandable Table | |
// for cell which are not expandable | |
func expandableTableView(_ tableView: ExpandableTableView, didSelectRowAt indexPath: IndexPath) { | |
} | |
// for cell which are expandable | |
func expand(_ tableView: ExpandableTableView, at indexPath: IndexPath, expand: Bool) { | |
let obj = tableView.object(at: indexPath) | |
obj.isExpanded = expand | |
let newCellIndexPath = IndexPath(row: indexPath.row + 1, section: 0) | |
if expand { | |
self.tableView.insertRows(at: [newCellIndexPath], with: .none) | |
} else { | |
self.tableView.deleteRows(at: [newCellIndexPath], with: .none) | |
} | |
self.tableView.beginUpdates() | |
self.tableView.endUpdates() | |
// or use this line instead of lines above | |
//self.tableView.reloadData() | |
self.tableView.scrollToRow(at: indexPath, at: .top, animated: false) | |
} | |
func isCellExpanded(_ tableView: ExpandableTableView, at indexPath: IndexPath) -> Bool { | |
let obj = tableView.object(at: indexPath) | |
return obj.isExpanded | |
} | |
func expandableTableView(_ tableView: ExpandableTableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | |
let obj = tableView.object(at: indexPath) | |
guard let cellId = obj.cellIdentifier else { | |
return UITableViewCell() | |
} | |
if cellId == String(describing: ExpandableTVC.self) { | |
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as? ExpandableTVC else { | |
return UITableViewCell() | |
} | |
cell.setup(for: obj) | |
return cell | |
} | |
if cellId == String(describing: SubDetailsTableCell.self) { | |
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as? SubDetailsTableCell else { | |
return UITableViewCell() | |
} | |
cell.setup(...) | |
return cell | |
} | |
return UITableViewCell() | |
} | |
func expandableTableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { | |
// if you need data from object: let obj = tableView.object(at: indexPath) | |
return 50.0 | |
} | |
// from delegate | |
func scrollViewDidScroll(_ scrollView: UIScrollView) { | |
// ... | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment