Merge pull request #2 from TouchInstinct/feature/expandable

Feature/expandable
This commit is contained in:
Ivan Zinovyev 2019-01-09 19:13:15 +03:00 committed by GitHub
commit 7f349c6944
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 211 additions and 72 deletions

View File

@ -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()
}
}

View File

@ -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
}
}

70
Sources/Expandable.swift Normal file
View File

@ -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)
}
}
}

View File

@ -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()
}
}

View File

@ -0,0 +1,5 @@
public protocol ExpandableCellViewModel: class {
var isCollapsed: Bool { get set }
}

7
Sources/LayoutType.swift Normal file
View File

@ -0,0 +1,7 @@
public enum LayoutType {
case manual
case auto
}

View File

@ -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 }
}

View File

@ -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? {

View File

@ -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
}
}
}

View File

@ -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 */,