diff --git a/README.md b/README.md index c13a629..a4583c0 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Build Status Swift 4.2 compatible Carthage compatible - CocoaPods compatible + CocoaPods compatible Platform iOS License: MIT

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