Merge pull request #2 from TouchInstinct/feature/expandable
Feature/expandable
This commit is contained in:
commit
7f349c6944
|
|
@ -1,45 +0,0 @@
|
|||
import UIKit
|
||||
|
||||
public class AccurateCellHeightCalculator: RowHeightCalculator {
|
||||
|
||||
private(set) weak var tableView: UITableView?
|
||||
private var prototypes = [String: UITableViewCell]()
|
||||
private var cachedHeights = [Int: CGFloat]()
|
||||
|
||||
public init(tableView: UITableView?) {
|
||||
self.tableView = tableView
|
||||
}
|
||||
|
||||
open func height(forRow row: Row, at indexPath: IndexPath) -> CGFloat {
|
||||
|
||||
guard let tableView = tableView else { return 0 }
|
||||
|
||||
let hash = row.hashValue ^ Int(tableView.bounds.size.width).hashValue
|
||||
|
||||
if let height = cachedHeights[hash] {
|
||||
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 }
|
||||
let height = row.height(for: cell)
|
||||
|
||||
cachedHeights[hash] = height
|
||||
|
||||
return height
|
||||
}
|
||||
|
||||
open func estimatedHeight(forRow row: Row, at indexPath: IndexPath) -> CGFloat {
|
||||
return height(forRow: row, at: indexPath)
|
||||
}
|
||||
|
||||
open func invalidate() {
|
||||
cachedHeights.removeAll()
|
||||
}
|
||||
}
|
||||
|
|
@ -25,22 +25,17 @@ public protocol ConfigurableCell {
|
|||
associatedtype CellData
|
||||
|
||||
static var reuseIdentifier: String { get }
|
||||
|
||||
static var estimatedHeight: CGFloat? { get }
|
||||
|
||||
static var defaultHeight: CGFloat? { get }
|
||||
|
||||
static var layoutType: LayoutType { get }
|
||||
|
||||
func configure(with _: CellData)
|
||||
func height(for _: CellData) -> CGFloat
|
||||
|
||||
}
|
||||
|
||||
public extension ConfigurableCell {
|
||||
|
||||
func height(for _: CellData) -> CGFloat {
|
||||
return UITableView.automaticDimension
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension ConfigurableCell where Self: UITableViewCell {
|
||||
|
||||
static var reuseIdentifier: String {
|
||||
|
|
@ -54,4 +49,9 @@ public extension ConfigurableCell where Self: UITableViewCell {
|
|||
static var defaultHeight: CGFloat? {
|
||||
return nil
|
||||
}
|
||||
|
||||
static var layoutType: LayoutType {
|
||||
return .auto
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
import UIKit
|
||||
|
||||
public protocol Expandable {
|
||||
|
||||
associatedtype ViewModelType: ExpandableCellViewModel
|
||||
|
||||
var viewModel: ViewModelType? { get }
|
||||
|
||||
func configureAppearance(isCollapsed: Bool)
|
||||
|
||||
}
|
||||
|
||||
extension Expandable where Self: UITableViewCell & ConfigurableCell {
|
||||
|
||||
public func initState() {
|
||||
guard let viewModel = viewModel else {
|
||||
return
|
||||
}
|
||||
|
||||
changeState(isCollapsed: viewModel.isCollapsed)
|
||||
}
|
||||
|
||||
private func changeState(isCollapsed: Bool) {
|
||||
// layout to get right frames, frame of bottom subview can be used to get expanded height
|
||||
layoutIfNeeded()
|
||||
|
||||
// apply changes
|
||||
configureAppearance(isCollapsed: isCollapsed)
|
||||
layoutIfNeeded()
|
||||
}
|
||||
|
||||
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) {
|
||||
changeState(isCollapsed: isCollapsed)
|
||||
|
||||
if let indexPath = indexPath,
|
||||
let tableDirector = (tableView?.delegate as? TableDirector),
|
||||
let cellHeightCalculator = tableDirector.rowHeightCalculator as? ExpandableCellHeightCalculator {
|
||||
cellHeightCalculator.updateCached(height: height(layoutType: Self.layoutType), for: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import UIKit
|
||||
|
||||
public final class ExpandableCellHeightCalculator: RowHeightCalculator {
|
||||
|
||||
private 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(layoutType: row.layoutType)
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
public protocol ExpandableCellViewModel: class {
|
||||
|
||||
var isCollapsed: Bool { get set }
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
public enum LayoutType {
|
||||
|
||||
case manual
|
||||
|
||||
case auto
|
||||
|
||||
}
|
||||
|
|
@ -32,7 +32,6 @@ public struct TableKitUserInfoKeys {
|
|||
public protocol RowConfigurable {
|
||||
|
||||
func configure(_ cell: UITableViewCell)
|
||||
func height(for _: UITableViewCell) -> CGFloat
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +58,8 @@ public protocol Row: RowConfigurable, RowActionable, RowHashable {
|
|||
|
||||
var reuseIdentifier: String { get }
|
||||
var cellType: AnyClass { get }
|
||||
|
||||
|
||||
var layoutType: LayoutType { get }
|
||||
var estimatedHeight: CGFloat? { get }
|
||||
var defaultHeight: CGFloat? { get }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ open class TableRow<CellType: ConfigurableCell>: Row where CellType: UITableView
|
|||
open var defaultHeight: CGFloat? {
|
||||
return CellType.defaultHeight
|
||||
}
|
||||
|
||||
open var layoutType: LayoutType {
|
||||
return CellType.layoutType
|
||||
}
|
||||
|
||||
open var cellType: AnyClass {
|
||||
return CellType.self
|
||||
|
|
@ -59,12 +63,7 @@ open class TableRow<CellType: ConfigurableCell>: Row where CellType: UITableView
|
|||
|
||||
(cell as? CellType)?.configure(with: item)
|
||||
}
|
||||
|
||||
open func height(for cell: UITableViewCell) -> CGFloat {
|
||||
|
||||
return (cell as? CellType)?.height(for: item) ?? UITableView.automaticDimension
|
||||
}
|
||||
|
||||
|
||||
// MARK: - RowActionable -
|
||||
|
||||
open func invoke(action: TableRowActionType, cell: UITableViewCell?, path: IndexPath, userInfo: [AnyHashable: Any]? = nil) -> Any? {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import UIKit
|
||||
|
||||
extension UITableViewCell {
|
||||
|
||||
var tableView: UITableView? {
|
||||
var view = superview
|
||||
|
||||
while view != nil && !(view is UITableView) {
|
||||
view = view?.superview
|
||||
}
|
||||
|
||||
return view as? UITableView
|
||||
}
|
||||
|
||||
var indexPath: IndexPath? {
|
||||
guard let indexPath = tableView?.indexPath(for: self) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return indexPath
|
||||
}
|
||||
|
||||
public func height(layoutType: LayoutType) -> CGFloat {
|
||||
switch layoutType {
|
||||
case .auto:
|
||||
return contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
|
||||
case .manual:
|
||||
return contentView.subviews.map { $0.frame.maxY }.max() ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -7,7 +7,11 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
320C5280218EB9A7004EAD1C /* AccurateCellHeightCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 320C527F218EB9A7004EAD1C /* AccurateCellHeightCalculator.swift */; };
|
||||
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 */; };
|
||||
32BDFE9F21C167F400D0BBB4 /* LayoutType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32BDFE9E21C167F400D0BBB4 /* LayoutType.swift */; };
|
||||
50CF6E6B1D6704FE004746FF /* TableCellRegisterer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF6E6A1D6704FE004746FF /* TableCellRegisterer.swift */; };
|
||||
50E858581DB153F500A9AA55 /* TableKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E858571DB153F500A9AA55 /* TableKit.swift */; };
|
||||
DA9EA7AF1D0EC2C90021F650 /* ConfigurableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9EA7A61D0EC2C90021F650 /* ConfigurableCell.swift */; };
|
||||
|
|
@ -33,7 +37,11 @@
|
|||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
320C527F218EB9A7004EAD1C /* AccurateCellHeightCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccurateCellHeightCalculator.swift; sourceTree = "<group>"; };
|
||||
3201E78321BE9DE1001DF9E7 /* ExpandableCellHeightCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableCellHeightCalculator.swift; sourceTree = "<group>"; };
|
||||
3201E78521BE9E25001DF9E7 /* UITableViewCell+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+Extensions.swift"; sourceTree = "<group>"; };
|
||||
3201E78721BE9EB2001DF9E7 /* Expandable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Expandable.swift; sourceTree = "<group>"; };
|
||||
3201E78921BE9ED4001DF9E7 /* ExpandableCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableCellViewModel.swift; sourceTree = "<group>"; };
|
||||
32BDFE9E21C167F400D0BBB4 /* LayoutType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutType.swift; sourceTree = "<group>"; };
|
||||
50CF6E6A1D6704FE004746FF /* TableCellRegisterer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableCellRegisterer.swift; sourceTree = "<group>"; };
|
||||
50E858571DB153F500A9AA55 /* TableKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableKit.swift; sourceTree = "<group>"; };
|
||||
DA9EA7561D0B679A0021F650 /* TableKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TableKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
|
@ -92,17 +100,21 @@
|
|||
DA9EA7A51D0EC2B90021F650 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
50E858571DB153F500A9AA55 /* TableKit.swift */,
|
||||
DA9EA7AA1D0EC2C90021F650 /* TableDirector.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 */,
|
||||
32BDFE9E21C167F400D0BBB4 /* LayoutType.swift */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -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 */,
|
||||
320C5280218EB9A7004EAD1C /* AccurateCellHeightCalculator.swift in Sources */,
|
||||
3201E78821BE9EB2001DF9E7 /* Expandable.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 */,
|
||||
32BDFE9F21C167F400D0BBB4 /* LayoutType.swift in Sources */,
|
||||
3201E78621BE9E25001DF9E7 /* UITableViewCell+Extensions.swift in Sources */,
|
||||
DA9EA7B11D0EC2C90021F650 /* Operators.swift in Sources */,
|
||||
DA9EA7B41D0EC2C90021F650 /* TableRow.swift in Sources */,
|
||||
50E858581DB153F500A9AA55 /* TableKit.swift in Sources */,
|
||||
|
|
|
|||
Loading…
Reference in New Issue