diff --git a/README.md b/README.md
index c13a629..a4583c0 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-
+
diff --git a/Sources/Expandable.swift b/Sources/Expandable.swift
index 847965f..6553e9c 100644
--- a/Sources/Expandable.swift
+++ b/Sources/Expandable.swift
@@ -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)
}
}
-
+
}
diff --git a/Sources/ExpandableCellViewModel.swift b/Sources/ExpandableCellViewModel.swift
index f3dd8ea..40c893b 100644
--- a/Sources/ExpandableCellViewModel.swift
+++ b/Sources/ExpandableCellViewModel.swift
@@ -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]
+ }
+
}
diff --git a/Sources/ExpandableState.swift b/Sources/ExpandableState.swift
new file mode 100644
index 0000000..e7db170
--- /dev/null
+++ b/Sources/ExpandableState.swift
@@ -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
+ }
+
+}
diff --git a/Sources/TableDirector.swift b/Sources/TableDirector.swift
index c17ce2c..aa6473b 100644
--- a/Sources/TableDirector.swift
+++ b/Sources/TableDirector.swift
@@ -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
diff --git a/TableKit.podspec b/TableKit.podspec
index 5d90f09..0a92cd7 100644
--- a/TableKit.podspec
+++ b/TableKit.podspec
@@ -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.'
diff --git a/TableKit.xcodeproj/project.pbxproj b/TableKit.xcodeproj/project.pbxproj
index ff64415..43a4aa8 100644
--- a/TableKit.xcodeproj/project.pbxproj
+++ b/TableKit.xcodeproj/project.pbxproj
@@ -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 = ""; };
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 = ""; };
@@ -115,6 +117,7 @@
3201E78721BE9EB2001DF9E7 /* Expandable.swift */,
3201E78921BE9ED4001DF9E7 /* ExpandableCellViewModel.swift */,
32BDFE9E21C167F400D0BBB4 /* LayoutType.swift */,
+ 2CBFA2F421F692F100147B56 /* ExpandableState.swift */,
);
path = Sources;
sourceTree = "";
@@ -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 */,