From ff18a4d8b84e5970963e69968bc17399c8c855c4 Mon Sep 17 00:00:00 2001 From: Ivan Zinovyev Date: Tue, 22 Jan 2019 03:00:41 +0300 Subject: [PATCH] Add Expandable States --- Sources/Expandable.swift | 94 +++++++++++++++++---------- Sources/ExpandableCellViewModel.swift | 16 ++++- Sources/ExpandableState.swift | 41 ++++++++++++ TableKit.xcodeproj/project.pbxproj | 4 ++ 4 files changed, 118 insertions(+), 37 deletions(-) create mode 100644 Sources/ExpandableState.swift diff --git a/Sources/Expandable.swift b/Sources/Expandable.swift index 847965f..c2b58b2 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) - }, completion: { _ in - viewModel.isCollapsed.toggle() + self?.applyChanges(expandableState: state) + }, completion: { _ in + 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) + let tableDirector = (tableView?.delegate as? TableDirector), + let cellHeightCalculator = tableDirector.rowHeightCalculator as? ExpandableCellHeightCalculator { + 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/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 */,