From 6eaf2cf3a26a36a1473d880ee3b3bb2045e9059d Mon Sep 17 00:00:00 2001 From: Ivan Zinovyev Date: Mon, 10 Dec 2018 17:15:43 +0300 Subject: [PATCH] Add Expandable protocol to expand/collapse cells --- Sources/ConfigurableCell.swift | 3 ++ Sources/Expandable.swift | 54 +++++++++++++++++++ Sources/ExpandableCellHeightCalculator.swift | 55 ++++++++++++++++++++ Sources/ExpandableCellViewModel.swift | 5 ++ Sources/UITableViewCell+Extensions.swift | 27 ++++++++++ TableKit.xcodeproj/project.pbxproj | 30 ++++++++--- 6 files changed, 167 insertions(+), 7 deletions(-) create mode 100644 Sources/Expandable.swift create mode 100644 Sources/ExpandableCellHeightCalculator.swift create mode 100644 Sources/ExpandableCellViewModel.swift create mode 100644 Sources/UITableViewCell+Extensions.swift diff --git a/Sources/ConfigurableCell.swift b/Sources/ConfigurableCell.swift index dee4db2..6454dc3 100644 --- a/Sources/ConfigurableCell.swift +++ b/Sources/ConfigurableCell.swift @@ -25,7 +25,10 @@ public protocol ConfigurableCell { associatedtype CellData static var reuseIdentifier: String { get } + + @available(*, deprecated, message: "For static cells use defaultHeight, height of self-sized cells will be calculated automatically") static var estimatedHeight: CGFloat? { get } + static var defaultHeight: CGFloat? { get } func configure(with _: CellData) diff --git a/Sources/Expandable.swift b/Sources/Expandable.swift new file mode 100644 index 0000000..51112de --- /dev/null +++ b/Sources/Expandable.swift @@ -0,0 +1,54 @@ +import UIKit + +public protocol Expandable { + + associatedtype ViewModelType: ExpandableCellViewModel + + var viewModel: ViewModelType? { get } + + func configureAppearance(isCollapsed: Bool) + +} + +extension Expandable where Self: UITableViewCell & ConfigurableCell { + + public func toggleState(animated: Bool = true, + animationDuration: TimeInterval = 0.3) { + + guard let tableView = tableView, + let viewModel = viewModel else { + return + } + + let contentOffset = tableView.contentOffset + + if animated { + UIView.animate(withDuration: animationDuration, + animations: { [weak self] in + self?.applyChanges(isCollapsed: !viewModel.isCollapsed) + }, completion: { _ in + viewModel.isCollapsed.toggle() + }) + } else { + applyChanges(isCollapsed: !viewModel.isCollapsed) + viewModel.isCollapsed.toggle() + } + + tableView.beginUpdates() + tableView.endUpdates() + + tableView.setContentOffset(contentOffset, animated: false) + } + + private func applyChanges(isCollapsed: Bool) { + configureAppearance(isCollapsed: isCollapsed) + layoutIfNeeded() + + if let indexPath = indexPath, + let tableDirector = (tableView?.delegate as? TableDirector), + let cellHeightCalculator = tableDirector.rowHeightCalculator as? ExpandableCellHeightCalculator { + cellHeightCalculator.updateCached(height: height, for: indexPath) + } + } + +} diff --git a/Sources/ExpandableCellHeightCalculator.swift b/Sources/ExpandableCellHeightCalculator.swift new file mode 100644 index 0000000..a9bf8ee --- /dev/null +++ b/Sources/ExpandableCellHeightCalculator.swift @@ -0,0 +1,55 @@ +import UIKit + +public final class ExpandableCellHeightCalculator: RowHeightCalculator { + + private(set) weak var tableView: UITableView? + + private var prototypes = [String: UITableViewCell]() + + private var cachedHeights = [IndexPath: CGFloat]() + + public init(tableView: UITableView?) { + self.tableView = tableView + } + + public func updateCached(height: CGFloat, for indexPath: IndexPath) { + cachedHeights[indexPath] = height + } + + public func height(forRow row: Row, at indexPath: IndexPath) -> CGFloat { + + guard let tableView = tableView else { + return 0 + } + + if let height = cachedHeights[indexPath] { + return height + } + + var prototypeCell = prototypes[row.reuseIdentifier] + if prototypeCell == nil { + prototypeCell = tableView.dequeueReusableCell(withIdentifier: row.reuseIdentifier) + prototypes[row.reuseIdentifier] = prototypeCell + } + + guard let cell = prototypeCell else { + return 0 + } + + row.configure(cell) + cell.layoutIfNeeded() + + let height = cell.height + cachedHeights[indexPath] = height + return height + } + + public func estimatedHeight(forRow row: Row, at indexPath: IndexPath) -> CGFloat { + return height(forRow: row, at: indexPath) + } + + public func invalidate() { + cachedHeights.removeAll() + } + +} diff --git a/Sources/ExpandableCellViewModel.swift b/Sources/ExpandableCellViewModel.swift new file mode 100644 index 0000000..f3dd8ea --- /dev/null +++ b/Sources/ExpandableCellViewModel.swift @@ -0,0 +1,5 @@ +public protocol ExpandableCellViewModel: class { + + var isCollapsed: Bool { get set } + +} diff --git a/Sources/UITableViewCell+Extensions.swift b/Sources/UITableViewCell+Extensions.swift new file mode 100644 index 0000000..dee1042 --- /dev/null +++ b/Sources/UITableViewCell+Extensions.swift @@ -0,0 +1,27 @@ +import UIKit + +extension UITableViewCell { + + public var tableView: UITableView? { + var view = superview + + while view != nil && !(view is UITableView) { + view = view?.superview + } + + return view as? UITableView + } + + public var indexPath: IndexPath? { + guard let indexPath = tableView?.indexPath(for: self) else { + return nil + } + + return indexPath + } + + public var height: CGFloat { + return contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height + } + +} diff --git a/TableKit.xcodeproj/project.pbxproj b/TableKit.xcodeproj/project.pbxproj index b30ee04..de4d250 100644 --- a/TableKit.xcodeproj/project.pbxproj +++ b/TableKit.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 3201E78421BE9DE1001DF9E7 /* ExpandableCellHeightCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3201E78321BE9DE1001DF9E7 /* ExpandableCellHeightCalculator.swift */; }; + 3201E78621BE9E25001DF9E7 /* UITableViewCell+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3201E78521BE9E25001DF9E7 /* UITableViewCell+Extensions.swift */; }; + 3201E78821BE9EB2001DF9E7 /* Expandable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3201E78721BE9EB2001DF9E7 /* Expandable.swift */; }; + 3201E78A21BE9ED4001DF9E7 /* ExpandableCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3201E78921BE9ED4001DF9E7 /* ExpandableCellViewModel.swift */; }; 320C5280218EB9A7004EAD1C /* AccurateCellHeightCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 320C527F218EB9A7004EAD1C /* AccurateCellHeightCalculator.swift */; }; 50CF6E6B1D6704FE004746FF /* TableCellRegisterer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF6E6A1D6704FE004746FF /* TableCellRegisterer.swift */; }; 50E858581DB153F500A9AA55 /* TableKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E858571DB153F500A9AA55 /* TableKit.swift */; }; @@ -33,6 +37,10 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 3201E78321BE9DE1001DF9E7 /* ExpandableCellHeightCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableCellHeightCalculator.swift; sourceTree = ""; }; + 3201E78521BE9E25001DF9E7 /* UITableViewCell+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+Extensions.swift"; sourceTree = ""; }; + 3201E78721BE9EB2001DF9E7 /* Expandable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Expandable.swift; sourceTree = ""; }; + 3201E78921BE9ED4001DF9E7 /* ExpandableCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableCellViewModel.swift; sourceTree = ""; }; 320C527F218EB9A7004EAD1C /* AccurateCellHeightCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccurateCellHeightCalculator.swift; sourceTree = ""; }; 50CF6E6A1D6704FE004746FF /* TableCellRegisterer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableCellRegisterer.swift; sourceTree = ""; }; 50E858571DB153F500A9AA55 /* TableKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableKit.swift; sourceTree = ""; }; @@ -92,17 +100,21 @@ DA9EA7A51D0EC2B90021F650 /* Sources */ = { isa = PBXGroup; children = ( - 50E858571DB153F500A9AA55 /* TableKit.swift */, - DA9EA7AA1D0EC2C90021F650 /* TableDirector.swift */, + 320C527F218EB9A7004EAD1C /* AccurateCellHeightCalculator.swift */, + DA9EA7A61D0EC2C90021F650 /* ConfigurableCell.swift */, + 3201E78321BE9DE1001DF9E7 /* ExpandableCellHeightCalculator.swift */, + DA9EA7A81D0EC2C90021F650 /* Operators.swift */, + DA9EA7A91D0EC2C90021F650 /* TableCellAction.swift */, 50CF6E6A1D6704FE004746FF /* TableCellRegisterer.swift */, + DA9EA7AA1D0EC2C90021F650 /* TableDirector.swift */, + 50E858571DB153F500A9AA55 /* TableKit.swift */, + DA9EA7A71D0EC2C90021F650 /* TablePrototypeCellHeightCalculator.swift */, DA9EA7AB1D0EC2C90021F650 /* TableRow.swift */, DA9EA7AC1D0EC2C90021F650 /* TableRowAction.swift */, DA9EA7AE1D0EC2C90021F650 /* TableSection.swift */, - DA9EA7A91D0EC2C90021F650 /* TableCellAction.swift */, - DA9EA7A71D0EC2C90021F650 /* TablePrototypeCellHeightCalculator.swift */, - DA9EA7A61D0EC2C90021F650 /* ConfigurableCell.swift */, - DA9EA7A81D0EC2C90021F650 /* Operators.swift */, - 320C527F218EB9A7004EAD1C /* AccurateCellHeightCalculator.swift */, + 3201E78521BE9E25001DF9E7 /* UITableViewCell+Extensions.swift */, + 3201E78721BE9EB2001DF9E7 /* Expandable.swift */, + 3201E78921BE9ED4001DF9E7 /* ExpandableCellViewModel.swift */, ); path = Sources; sourceTree = ""; @@ -233,14 +245,18 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3201E78A21BE9ED4001DF9E7 /* ExpandableCellViewModel.swift in Sources */, 50CF6E6B1D6704FE004746FF /* TableCellRegisterer.swift in Sources */, DA9EA7AF1D0EC2C90021F650 /* ConfigurableCell.swift in Sources */, DA9EA7B31D0EC2C90021F650 /* TableDirector.swift in Sources */, + 3201E78821BE9EB2001DF9E7 /* Expandable.swift in Sources */, 320C5280218EB9A7004EAD1C /* AccurateCellHeightCalculator.swift in Sources */, DA9EA7B71D0EC2C90021F650 /* TableSection.swift in Sources */, DA9EA7B01D0EC2C90021F650 /* TablePrototypeCellHeightCalculator.swift in Sources */, + 3201E78421BE9DE1001DF9E7 /* ExpandableCellHeightCalculator.swift in Sources */, DA9EA7B51D0EC2C90021F650 /* TableRowAction.swift in Sources */, DA9EA7B21D0EC2C90021F650 /* TableCellAction.swift in Sources */, + 3201E78621BE9E25001DF9E7 /* UITableViewCell+Extensions.swift in Sources */, DA9EA7B11D0EC2C90021F650 /* Operators.swift in Sources */, DA9EA7B41D0EC2C90021F650 /* TableRow.swift in Sources */, 50E858581DB153F500A9AA55 /* TableKit.swift in Sources */,