diff --git a/PanModal.podspec b/PanModal.podspec index fff41959..67be67cf 100644 --- a/PanModal.podspec +++ b/PanModal.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'PanModal' - s.version = '1.4.9' + s.version = '1.5.0' s.summary = 'PanModal is an elegant and highly customizable presentation API for constructing bottom sheet modals on iOS.' # This description is used to generate tags and improve search results. diff --git a/PanModal/Animator/PanModalPresentationAnimator.swift b/PanModal/Animator/PanModalPresentationAnimator.swift index 04f1f3fc..18feed0e 100644 --- a/PanModal/Animator/PanModalPresentationAnimator.swift +++ b/PanModal/Animator/PanModalPresentationAnimator.swift @@ -60,13 +60,25 @@ public class PanModalPresentationAnimator: NSObject { // Use panView as presentingView if it already exists within the containerView let panView: UIView = transitionContext.containerView.panContainerView ?? toVC.view - + let topView = transitionContext.containerView.panCustomTopView + let topViewHeight: CGFloat = { + if let topViewHeight = topView?.frame.height { + return topViewHeight + PanModalPresentationController.Constants.customTopViewOffset + } else { + return 0 + } + }() + // Move presented view offscreen (from the bottom) panView.frame = transitionContext.finalFrame(for: toVC) - panView.frame.origin.y = transitionContext.containerView.frame.height + panView.frame.origin.y = transitionContext.containerView.frame.height + topViewHeight + topView?.alpha = 0 + topView?.frame.origin.y = transitionContext.containerView.frame.height PanModalAnimator.animate({ panView.frame.origin.y = yPos + topView?.frame.origin.y = yPos - topViewHeight + topView?.alpha = 1 }, config: presentable) { didComplete in transitionContext.completeTransition(didComplete) } @@ -82,9 +94,19 @@ public class PanModalPresentationAnimator: NSObject { let presentable = fromVC as? PanModalPresentable.LayoutType let panView: UIView = transitionContext.containerView.panContainerView ?? fromVC.view - + let topView = transitionContext.containerView.panCustomTopView + let topViewHeight: CGFloat = { + if let topViewHeight = topView?.frame.height { + return topViewHeight + PanModalPresentationController.Constants.customTopViewOffset + } else { + return 0 + } + }() + PanModalAnimator.animate({ - panView.frame.origin.y = transitionContext.containerView.frame.height + PanModalPresentationController.Constants.dragIndicatorHeight + panView.frame.origin.y = transitionContext.containerView.frame.height + PanModalPresentationController.Constants.dragIndicatorHeight + topViewHeight + topView?.frame.origin.y = transitionContext.containerView.frame.height + topView?.alpha = 0.0 }, config: presentable) { didComplete in fromVC.view.removeFromSuperview() transitionContext.completeTransition(didComplete) diff --git a/PanModal/Controller/PanModalPresentationController.swift b/PanModal/Controller/PanModalPresentationController.swift index 116b81c2..a74d8f27 100644 --- a/PanModal/Controller/PanModalPresentationController.swift +++ b/PanModal/Controller/PanModalPresentationController.swift @@ -38,6 +38,7 @@ public class PanModalPresentationController: UIPresentationController { struct Constants { static let snapMovementSensitivity = CGFloat(0.7) static let dragIndicatorHeight = CGFloat(16) + static let customTopViewOffset = CGFloat(10) } // MARK: - Properties @@ -204,6 +205,7 @@ public class PanModalPresentationController: UIPresentationController { override public func presentationTransitionDidEnd(_ completed: Bool) { if completed { return } + presentable?.panCustomTopView?.removeFromSuperview() backgroundView.removeFromSuperview() } @@ -339,6 +341,8 @@ private extension PanModalPresentationController { if presentable.showDragIndicator { addDragIndicatorView(to: presentedView) } + + addCustomTopViewIfExisted(in: containerView) setNeedsLayoutUpdate() adjustPanContainerBackgroundColor() @@ -379,7 +383,18 @@ private extension PanModalPresentationController { backgroundView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true backgroundView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true } - + + + func addCustomTopViewIfExisted(in containerView: UIView) { + guard let customTopView = presentable?.panCustomTopView else { return } + containerView.addSubview(customTopView) + customTopView.translatesAutoresizingMaskIntoConstraints = false + customTopView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true + customTopView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true + customTopView.bottomAnchor.constraint(equalTo: dragIndicatorView.topAnchor, constant: -Constants.customTopViewOffset).isActive = true + customTopView.heightAnchor.constraint(equalToConstant: customTopView.frame.height).isActive = true + } + /** Adds the drag indicator view to the view hierarchy & configures its layout constraints. @@ -629,7 +644,16 @@ private extension PanModalPresentationController { Sets the y position of the presentedView & adjusts the backgroundView. */ func adjust(toYPosition yPos: CGFloat) { + let topViewHeight: CGFloat = { + if let topViewHeight = presentable?.panCustomTopView?.frame.height { + return topViewHeight + Constants.customTopViewOffset + } else { + return 0 + } + }() + presentedView.frame.origin.y = max(yPos, anchoredYPosition) + presentable?.panCustomTopView?.frame.origin.y = max(yPos, anchoredYPosition) - topViewHeight - PanModalPresentationController.Constants.dragIndicatorHeight guard presentedView.frame.origin.y > shortFormYPosition else { backgroundView.dimState = .max diff --git a/PanModal/Controller/PanModalWrappedViewController.swift b/PanModal/Controller/PanModalWrappedViewController.swift index 7523635c..f9ae542a 100644 --- a/PanModal/Controller/PanModalWrappedViewController.swift +++ b/PanModal/Controller/PanModalWrappedViewController.swift @@ -12,6 +12,7 @@ public class PanModalWrappedViewController: UIViewController { struct Constants { static let snapMovementSensitivity = CGFloat(0.7) static let dragIndicatorHeight = CGFloat(16) + static let customTopViewOffset = CGFloat(10) } // MARK: - Properties @@ -607,8 +608,17 @@ private extension PanModalWrappedViewController { Sets the y position of the presentedView & adjusts the backgroundView. */ func adjust(toYPosition yPos: CGFloat) { + let topViewHeight: CGFloat = { + if let topViewHeight = presentable?.panCustomTopView?.frame.height { + return topViewHeight + Constants.customTopViewOffset + } else { + return 0 + } + }() + presentedView.frame.origin.y = max(yPos, anchoredYPosition) - + presentable?.panCustomTopView?.frame.origin.y = max(yPos, anchoredYPosition) - topViewHeight - PanModalPresentationController.Constants.dragIndicatorHeight + guard presentedView.frame.origin.y > shortFormYPosition else { backgroundView.dimState = .max return diff --git a/PanModal/Presentable/PanModalPresentable+Defaults.swift b/PanModal/Presentable/PanModalPresentable+Defaults.swift index b4bdb886..7ca5d029 100644 --- a/PanModal/Presentable/PanModalPresentable+Defaults.swift +++ b/PanModal/Presentable/PanModalPresentable+Defaults.swift @@ -77,6 +77,10 @@ public extension PanModalPresentable where Self: UIViewController { var showDragIndicator: Bool { return true } + + var panCustomTopView: PanCustomTopView? { + return nil + } func shouldRespond(to panModalGestureRecognizer: UIPanGestureRecognizer) -> Bool { return true diff --git a/PanModal/Presentable/PanModalPresentable+LayoutHelpers.swift b/PanModal/Presentable/PanModalPresentable+LayoutHelpers.swift index 88569609..51ecbf4b 100644 --- a/PanModal/Presentable/PanModalPresentable+LayoutHelpers.swift +++ b/PanModal/Presentable/PanModalPresentable+LayoutHelpers.swift @@ -92,7 +92,9 @@ extension PanModalPresentable where Self: UIViewController { } - return container.bounds.size.height - topOffset + let customTopViewHeight = panCustomTopView?.frame.height ?? 0 + + return container.bounds.size.height - topOffset - customTopViewHeight } /** diff --git a/PanModal/Presentable/PanModalPresentable.swift b/PanModal/Presentable/PanModalPresentable.swift index 00cae2ec..eb4fef53 100644 --- a/PanModal/Presentable/PanModalPresentable.swift +++ b/PanModal/Presentable/PanModalPresentable.swift @@ -134,6 +134,8 @@ public protocol PanModalPresentable { */ var showDragIndicator: Bool { get } + var panCustomTopView: PanCustomTopView? { get } + /** Asks the delegate if the pan modal should respond to the pan modal gesture recognizer. diff --git a/PanModal/View/CustomTopView.swift b/PanModal/View/CustomTopView.swift new file mode 100644 index 00000000..e0b5f83b --- /dev/null +++ b/PanModal/View/CustomTopView.swift @@ -0,0 +1,11 @@ +import UIKit + +open class PanCustomTopView: UIView { + +} + +extension UIView { + var panCustomTopView: PanCustomTopView? { + return subviews.compactMap({ $0 as? PanCustomTopView }).first + } +} diff --git a/PanModalDemo.xcodeproj/project.pbxproj b/PanModalDemo.xcodeproj/project.pbxproj index 8e493a2e..e6a7e6e2 100644 --- a/PanModalDemo.xcodeproj/project.pbxproj +++ b/PanModalDemo.xcodeproj/project.pbxproj @@ -26,6 +26,8 @@ 638A9380256B4DC500A5E00B /* PanModalWrappedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 638A937F256B4DC500A5E00B /* PanModalWrappedViewController.swift */; }; 638A9383256B4E3F00A5E00B /* PanModalWrappedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 638A937F256B4DC500A5E00B /* PanModalWrappedViewController.swift */; }; 638A938D256BB26E00A5E00B /* EmbedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 638A9387256BB21800A5E00B /* EmbedViewController.swift */; }; + 6E763B4C2A2E2313002A77E8 /* CustomTopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E763B4B2A2E2313002A77E8 /* CustomTopView.swift */; }; + 6E763B4D2A2E25A2002A77E8 /* CustomTopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E763B4B2A2E2313002A77E8 /* CustomTopView.swift */; }; 743CABB02225FC9F00634A5A /* UserGroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 743CABAF2225FC9F00634A5A /* UserGroupViewController.swift */; }; 743CABB22225FD1100634A5A /* UserGroupHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 743CABB12225FD1100634A5A /* UserGroupHeaderView.swift */; }; 743CABB42225FE7700634A5A /* UserGroupMemberPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 743CABB32225FE7700634A5A /* UserGroupMemberPresentable.swift */; }; @@ -99,6 +101,7 @@ 0F2A2C542239C119003BDB2F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 638A937F256B4DC500A5E00B /* PanModalWrappedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PanModalWrappedViewController.swift; sourceTree = ""; }; 638A9387256BB21800A5E00B /* EmbedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbedViewController.swift; sourceTree = ""; }; + 6E763B4B2A2E2313002A77E8 /* CustomTopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTopView.swift; sourceTree = ""; }; 743CABAF2225FC9F00634A5A /* UserGroupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserGroupViewController.swift; sourceTree = ""; }; 743CABB12225FD1100634A5A /* UserGroupHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserGroupHeaderView.swift; sourceTree = ""; }; 743CABB32225FE7700634A5A /* UserGroupMemberPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserGroupMemberPresentable.swift; sourceTree = ""; }; @@ -321,6 +324,7 @@ DC13906E216D9458007A3E64 /* DimmedView.swift */, 94795C9C21F03368008045A0 /* PanContainerView.swift */, D134CC7022641B350022AA29 /* IndicatorView.swift */, + 6E763B4B2A2E2313002A77E8 /* CustomTopView.swift */, ); path = View; sourceTree = ""; @@ -541,6 +545,7 @@ 0F2A2C622239C148003BDB2F /* PanModalHeight.swift in Sources */, 0F2A2C632239C14B003BDB2F /* PanModalPresentable.swift in Sources */, 0F2A2C642239C14E003BDB2F /* PanModalPresentable+Defaults.swift in Sources */, + 6E763B4C2A2E2313002A77E8 /* CustomTopView.swift in Sources */, 0F2A2C652239C151003BDB2F /* PanModalPresentable+UIViewController.swift in Sources */, 0F2A2C662239C153003BDB2F /* PanModalPresentable+LayoutHelpers.swift in Sources */, 0F2A2C672239C157003BDB2F /* PanModalPresenter.swift in Sources */, @@ -587,6 +592,7 @@ DC139061216D93ED007A3E64 /* SampleViewController.swift in Sources */, 743CABB02225FC9F00634A5A /* UserGroupViewController.swift in Sources */, 638A938D256BB26E00A5E00B /* EmbedViewController.swift in Sources */, + 6E763B4D2A2E25A2002A77E8 /* CustomTopView.swift in Sources */, DCC0EE7C21917F2500208DBC /* PanModalPresentable+Defaults.swift in Sources */, 94795C9D21F03368008045A0 /* PanContainerView.swift in Sources */, 74C072A7220BA78800124CE1 /* PanModalPresentable+LayoutHelpers.swift in Sources */, diff --git a/Sample/View Controllers/BasicViewController.swift b/Sample/View Controllers/BasicViewController.swift index 2056651f..13bdb257 100644 --- a/Sample/View Controllers/BasicViewController.swift +++ b/Sample/View Controllers/BasicViewController.swift @@ -9,6 +9,7 @@ import UIKit class BasicViewController: UIViewController { + private let topView = TopView() override func viewDidLoad() { super.viewDidLoad() @@ -33,4 +34,63 @@ extension BasicViewController: PanModalPresentable { var anchorModalToLongForm: Bool { return false } + + var panCustomTopView: PanCustomTopView? { + return topView + } +} + +extension BasicViewController { + final class TopView: PanCustomTopView { + enum Layout { + static let size = CGSize(width: UIScreen.main.bounds.width, height: 50) + } + + private let containerView: UIView = { + let containerView = UIView() + containerView.backgroundColor = .white + containerView.layer.cornerRadius = 12 + containerView.layer.masksToBounds = true + + return containerView + }() + + private let affiliateSwitch = UISwitch() + + private let titleLabel: UILabel = { + let label = UILabel() + label.text = "CustomTopView Example" + label.textColor = .black + return label + }() + + override init(frame: CGRect) { + super.init(frame: .init(origin: .zero, size: Layout.size)) + + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + containerView.addSubview(titleLabel) + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16).isActive = true + titleLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor).isActive = true + + containerView.addSubview(affiliateSwitch) + affiliateSwitch.translatesAutoresizingMaskIntoConstraints = false + affiliateSwitch.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 10).isActive = true + affiliateSwitch.centerYAnchor.constraint(equalTo: containerView.centerYAnchor).isActive = true + + addSubview(containerView) + containerView.translatesAutoresizingMaskIntoConstraints = false + containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16).isActive = true + containerView.topAnchor.constraint(equalTo: topAnchor).isActive = true + containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16).isActive = true + containerView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + } + } }