Merge pull request #4 from TouchInstinct/expandable_states
Expandable states
This commit is contained in:
commit
1b4a988b35
|
|
@ -4,7 +4,7 @@
|
|||
<a href="https://travis-ci.org/maxsokolov/TableKit"><img src="https://api.travis-ci.org/maxsokolov/TableKit.svg" alt="Build Status" /></a>
|
||||
<a href="https://developer.apple.com/swift"><img src="https://img.shields.io/badge/Swift_4.2-compatible-4BC51D.svg?style=flat" alt="Swift 4.2 compatible" /></a>
|
||||
<a href="https://github.com/Carthage/Carthage"><img src="https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat" alt="Carthage compatible" /></a>
|
||||
<a href="https://cocoapods.org/pods/tablekit"><img src="https://img.shields.io/badge/pod-2.8.0-blue.svg" alt="CocoaPods compatible" /></a>
|
||||
<a href="https://cocoapods.org/pods/tablekit"><img src="https://img.shields.io/badge/pod-2.8.1-blue.svg" alt="CocoaPods compatible" /></a>
|
||||
<img src="https://img.shields.io/badge/platform-iOS-blue.svg?style=flat" alt="Platform iOS" />
|
||||
<a href="https://raw.githubusercontent.com/maxsokolov/tablekit/master/LICENSE"><img src="http://img.shields.io/badge/license-MIT-blue.svg?style=flat" alt="License: MIT" /></a>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -1,70 +1,96 @@
|
|||
import UIKit
|
||||
|
||||
public extension TimeInterval {
|
||||
|
||||
static let defaultExpandableAnimationDuration: TimeInterval = 0.3
|
||||
|
||||
}
|
||||
|
||||
public protocol Expandable {
|
||||
|
||||
|
||||
associatedtype ViewModelType: ExpandableCellViewModel
|
||||
|
||||
|
||||
var viewModel: ViewModelType? { get }
|
||||
|
||||
func configureAppearance(isCollapsed: Bool)
|
||||
|
||||
|
||||
func configure(state: ExpandableState)
|
||||
|
||||
}
|
||||
|
||||
extension Expandable where Self: UITableViewCell & ConfigurableCell {
|
||||
|
||||
|
||||
public func initState() {
|
||||
guard let viewModel = viewModel else {
|
||||
return
|
||||
}
|
||||
|
||||
changeState(isCollapsed: viewModel.isCollapsed)
|
||||
|
||||
changeState(expandableState: viewModel.expandableState)
|
||||
}
|
||||
|
||||
private func changeState(isCollapsed: Bool) {
|
||||
|
||||
private func changeState(expandableState: ExpandableState) {
|
||||
// layout to get right frames, frame of bottom subview can be used to get expanded height
|
||||
setNeedsLayout()
|
||||
layoutIfNeeded()
|
||||
|
||||
|
||||
// apply changes
|
||||
configureAppearance(isCollapsed: isCollapsed)
|
||||
configure(state: expandableState)
|
||||
layoutIfNeeded()
|
||||
}
|
||||
|
||||
|
||||
public func toggleState(animated: Bool = true,
|
||||
animationDuration: TimeInterval = 0.3) {
|
||||
|
||||
guard let tableView = tableView,
|
||||
let viewModel = viewModel else {
|
||||
animationDuration: TimeInterval = .defaultExpandableAnimationDuration) {
|
||||
|
||||
guard let viewModel = viewModel,
|
||||
let stateIndex = viewModel.availableStates.index(where: { $0 == viewModel.expandableState }) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let targetState = stateIndex == viewModel.availableStates.count - 1
|
||||
? viewModel.availableStates[0]
|
||||
: viewModel.availableStates[stateIndex + 1]
|
||||
|
||||
transition(to: targetState,
|
||||
animated: animated,
|
||||
animationDuration: animationDuration)
|
||||
}
|
||||
|
||||
public func transition(to state: ExpandableState,
|
||||
animated: Bool = true,
|
||||
animationDuration: TimeInterval = .defaultExpandableAnimationDuration) {
|
||||
|
||||
guard let tableView = tableView,
|
||||
let viewModel = viewModel,
|
||||
viewModel.expandableState != state else {
|
||||
return
|
||||
}
|
||||
|
||||
let contentOffset = tableView.contentOffset
|
||||
|
||||
|
||||
if animated {
|
||||
UIView.animate(withDuration: animationDuration,
|
||||
animations: { [weak self] in
|
||||
self?.applyChanges(isCollapsed: !viewModel.isCollapsed)
|
||||
self?.applyChanges(expandableState: state)
|
||||
}, completion: { _ in
|
||||
viewModel.isCollapsed.toggle()
|
||||
viewModel.expandableState = state
|
||||
})
|
||||
} else {
|
||||
applyChanges(isCollapsed: !viewModel.isCollapsed)
|
||||
viewModel.isCollapsed.toggle()
|
||||
applyChanges(expandableState: state)
|
||||
viewModel.expandableState = state
|
||||
}
|
||||
|
||||
|
||||
tableView.beginUpdates()
|
||||
tableView.endUpdates()
|
||||
|
||||
|
||||
tableView.setContentOffset(contentOffset, animated: false)
|
||||
}
|
||||
|
||||
private func applyChanges(isCollapsed: Bool) {
|
||||
changeState(isCollapsed: isCollapsed)
|
||||
|
||||
|
||||
public func applyChanges(expandableState: ExpandableState) {
|
||||
changeState(expandableState: expandableState)
|
||||
|
||||
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)
|
||||
cellHeightCalculator.updateCached(height: expandableState.height ?? height(layoutType: Self.layoutType), for: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,15 @@
|
|||
public protocol ExpandableCellViewModel: class {
|
||||
|
||||
var isCollapsed: Bool { get set }
|
||||
|
||||
|
||||
var expandableState: ExpandableState { get set }
|
||||
|
||||
var availableStates: [ExpandableState] { get }
|
||||
|
||||
}
|
||||
|
||||
public extension ExpandableCellViewModel {
|
||||
|
||||
var availableStates: [ExpandableState] {
|
||||
return [.collapsed, .expanded]
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import UIKit
|
||||
|
||||
public enum ExpandableState {
|
||||
|
||||
case collapsed
|
||||
|
||||
case expanded
|
||||
|
||||
case height(value: CGFloat)
|
||||
|
||||
}
|
||||
|
||||
extension ExpandableState: Equatable { }
|
||||
|
||||
extension ExpandableState {
|
||||
|
||||
public var isCollapsed: Bool {
|
||||
guard case .collapsed = self else {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
public var isExpanded: Bool {
|
||||
guard case .expanded = self else {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
public var height: CGFloat? {
|
||||
guard case let .height(value: height) = self else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return height
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -95,6 +95,14 @@ open class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate {
|
|||
tableView?.reloadData()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
private func row(at indexPath: IndexPath) -> Row? {
|
||||
if indexPath.section < sections.count && indexPath.row < sections[indexPath.section].rows.count {
|
||||
return sections[indexPath.section].rows[indexPath.row]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: Public
|
||||
@discardableResult
|
||||
open func invoke(
|
||||
|
|
@ -102,15 +110,13 @@ open class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate {
|
|||
cell: UITableViewCell?, indexPath: IndexPath,
|
||||
userInfo: [AnyHashable: Any]? = nil) -> Any?
|
||||
{
|
||||
if indexPath.section < sections.count && indexPath.row < sections[indexPath.section].rows.count {
|
||||
return sections[indexPath.section].rows[indexPath.row].invoke(
|
||||
action: action,
|
||||
cell: cell,
|
||||
path: indexPath,
|
||||
userInfo: userInfo
|
||||
)
|
||||
}
|
||||
return nil
|
||||
guard let row = row(at: indexPath) else { return nil }
|
||||
return row.invoke(
|
||||
action: action,
|
||||
cell: cell,
|
||||
path: indexPath,
|
||||
userInfo: userInfo
|
||||
)
|
||||
}
|
||||
|
||||
open override func responds(to selector: Selector) -> Bool {
|
||||
|
|
@ -125,7 +131,8 @@ open class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate {
|
|||
|
||||
// MARK: - Internal
|
||||
func hasAction(_ action: TableRowActionType, atIndexPath indexPath: IndexPath) -> Bool {
|
||||
return sections[indexPath.section].rows[indexPath.row].has(action: action)
|
||||
guard let row = row(at: indexPath) else { return false }
|
||||
return row.has(action: action)
|
||||
}
|
||||
|
||||
@objc
|
||||
|
|
@ -172,6 +179,8 @@ open class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate {
|
|||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
guard section < sections.count else { return 0 }
|
||||
|
||||
return sections[section].numberOfRows
|
||||
}
|
||||
|
||||
|
|
@ -196,29 +205,39 @@ open class TableDirector: NSObject, UITableViewDataSource, UITableViewDelegate {
|
|||
|
||||
// MARK: UITableViewDataSource - section setup
|
||||
open func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
guard section < sections.count else { return nil }
|
||||
|
||||
return sections[section].headerTitle
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||
guard section < sections.count else { return nil }
|
||||
|
||||
return sections[section].footerTitle
|
||||
}
|
||||
|
||||
// MARK: UITableViewDelegate - section setup
|
||||
open func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
guard section < sections.count else { return nil }
|
||||
|
||||
return sections[section].headerView
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
|
||||
guard section < sections.count else { return nil }
|
||||
|
||||
return sections[section].footerView
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
guard section < sections.count else { return 0 }
|
||||
|
||||
let section = sections[section]
|
||||
return section.headerHeight ?? section.headerView?.frame.size.height ?? UITableView.automaticDimension
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
|
||||
guard section < sections.count else { return 0 }
|
||||
|
||||
let section = sections[section]
|
||||
return section.footerHeight
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ Pod::Spec.new do |s|
|
|||
s.name = 'TableKit'
|
||||
s.module_name = 'TableKit'
|
||||
|
||||
s.version = '2.8.0'
|
||||
s.version = '2.8.1'
|
||||
|
||||
s.homepage = 'https://github.com/maxsokolov/TableKit'
|
||||
s.summary = 'Type-safe declarative table views with Swift.'
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
2CBFA2F521F692F100147B56 /* ExpandableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBFA2F421F692F100147B56 /* ExpandableState.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 */; };
|
||||
|
|
@ -37,6 +38,7 @@
|
|||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
2CBFA2F421F692F100147B56 /* ExpandableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableState.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>"; };
|
||||
|
|
@ -115,6 +117,7 @@
|
|||
3201E78721BE9EB2001DF9E7 /* Expandable.swift */,
|
||||
3201E78921BE9ED4001DF9E7 /* ExpandableCellViewModel.swift */,
|
||||
32BDFE9E21C167F400D0BBB4 /* LayoutType.swift */,
|
||||
2CBFA2F421F692F100147B56 /* ExpandableState.swift */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -250,6 +253,7 @@
|
|||
DA9EA7AF1D0EC2C90021F650 /* ConfigurableCell.swift in Sources */,
|
||||
DA9EA7B31D0EC2C90021F650 /* TableDirector.swift in Sources */,
|
||||
3201E78821BE9EB2001DF9E7 /* Expandable.swift in Sources */,
|
||||
2CBFA2F521F692F100147B56 /* ExpandableState.swift in Sources */,
|
||||
DA9EA7B71D0EC2C90021F650 /* TableSection.swift in Sources */,
|
||||
DA9EA7B01D0EC2C90021F650 /* TablePrototypeCellHeightCalculator.swift in Sources */,
|
||||
3201E78421BE9DE1001DF9E7 /* ExpandableCellHeightCalculator.swift in Sources */,
|
||||
|
|
|
|||
Loading…
Reference in New Issue