Skip to content

Instantly share code, notes, and snippets.

@Eridana
Last active November 8, 2018 13:55
Show Gist options
  • Save Eridana/5fe4511c5d1c258f2af63fe8b9b13341 to your computer and use it in GitHub Desktop.
Save Eridana/5fe4511c5d1c258f2af63fe8b9b13341 to your computer and use it in GitHub Desktop.
Expandable UITableView
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)
}
}
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
}
}
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
}
}
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