Skip to content

Add Expandable protocol and ExpandableCellHeightCalculator #87

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Sources/ConfigurableCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,15 @@ 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)

}

public extension ConfigurableCell where Self: UITableViewCell {
Expand All @@ -44,4 +49,9 @@ public extension ConfigurableCell where Self: UITableViewCell {
static var defaultHeight: CGFloat? {
return nil
}

static var layoutType: LayoutType {
return .auto
}

}
70 changes: 70 additions & 0 deletions Sources/Expandable.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}

}
55 changes: 55 additions & 0 deletions Sources/ExpandableCellHeightCalculator.swift
Original file line number Diff line number Diff line change
@@ -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()
}

}
5 changes: 5 additions & 0 deletions Sources/ExpandableCellViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
public protocol ExpandableCellViewModel: class {

var isCollapsed: Bool { get set }

}
7 changes: 7 additions & 0 deletions Sources/LayoutType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
public enum LayoutType {

case manual

case auto

}
4 changes: 3 additions & 1 deletion Sources/TableKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public struct TableKitUserInfoKeys {
public protocol RowConfigurable {

func configure(_ cell: UITableViewCell)

}

public protocol RowActionable {
Expand All @@ -58,7 +59,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 }
}
Expand Down
6 changes: 5 additions & 1 deletion Sources/TableRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -59,7 +63,7 @@ open class TableRow<CellType: ConfigurableCell>: Row where CellType: UITableView

(cell as? CellType)?.configure(with: item)
}

// MARK: - RowActionable -

open func invoke(action: TableRowActionType, cell: UITableViewCell?, path: IndexPath, userInfo: [AnyHashable: Any]? = nil) -> Any? {
Expand Down
32 changes: 32 additions & 0 deletions Sources/UITableViewCell+Extensions.swift
Original file line number Diff line number Diff line change
@@ -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
}
}

}
32 changes: 26 additions & 6 deletions TableKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
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 */; };
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 */; };
Expand All @@ -32,6 +37,11 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
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; };
Expand Down Expand Up @@ -90,16 +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 */,
3201E78521BE9E25001DF9E7 /* UITableViewCell+Extensions.swift */,
3201E78721BE9EB2001DF9E7 /* Expandable.swift */,
3201E78921BE9ED4001DF9E7 /* ExpandableCellViewModel.swift */,
32BDFE9E21C167F400D0BBB4 /* LayoutType.swift */,
);
path = Sources;
sourceTree = "<group>";
Expand Down Expand Up @@ -231,13 +246,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 */,
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 */,
Expand Down