import UIKit /// `UIScrollView` wrapper that allows configuring how the scrollable content is laid out. public class ScrollWrapperView: UIView { /// Create `UIScrollView` wrapper view. public init() { scrollView = UIScrollView(frame: .zero) super.init(frame: .zero) scrollView.keyboardDismissMode = .interactive addSubview(scrollView) scrollView.addSubview(contentWrapperView) setupLayout() } /// Does nothing, this class is designed to be used programmatically. required public init?(coder aDecoder: NSCoder) { nil } // MARK: - Subviews /// Wrapped `UIScrollView`. public let scrollView: UIScrollView /// Scrollable content view. public var contentView: UIView? { didSet { oldValue?.removeFromSuperview() contentViewTopEqualSuper = nil contentViewTopGreaterThanSuper = nil contentViewLeft = nil contentViewRight = nil contentViewBottom = nil if let newValue = contentView { contentWrapperView.addSubview(newValue) setupLayout(contentView: newValue) } } } let contentWrapperView = UIView() // MARK: - Layout configuration /// If `true`, `contentView` will be stretched to fill visible area. /// /// Default is `true`. public var contentViewStretching = true { didSet { contentWrapperHeight.isActive = contentViewStretching } } /// If `true` the content view will be aligned to the bottom of scrollable area. /// /// Default is `false`. /// /// If the `contentViewStretching` is set to `false` this property makes no changes to the alignemnt. public var alignContentToBottom = false { didSet { contentViewTopGreaterThanSuper?.isActive = alignContentToBottom == true contentViewTopEqualSuper?.isActive = alignContentToBottom == false } } /// Scrollable content insets. /// /// Default is `.zero` which means no insets. public var contentInsets: UIEdgeInsets = .zero { didSet { contentViewTopEqualSuper?.constant = contentInsets.top contentViewTopGreaterThanSuper?.constant = contentInsets.top contentViewLeft?.constant = contentInsets.left contentViewRight?.constant = -contentInsets.right contentViewBottom?.constant = -contentInsets.bottom } } // MARK: - Touch handling configuration /// If `true` touches outside the `contentView` will be handled and allow scrolling. /// /// Default is `true`. public var handlesTouchesOutsideContent = true public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if handlesTouchesOutsideContent { return super.hitTest(point, with: event) } if let contentView = contentView, contentView.bounds.contains(convert(point, to: contentView)) { return super.hitTest(point, with: event) } return nil } // MARK: - Internals var visibleContentInsets: UIEdgeInsets { get { UIEdgeInsets( top: visibleContentLayoutGuideTop.constant, left: visibleContentLayoutGuideLeft.constant, bottom: -visibleContentLayoutGuideBottom.constant, right: -visibleContentLayoutGuideRight.constant ) } set { visibleContentLayoutGuideTop.constant = newValue.top visibleContentLayoutGuideLeft.constant = newValue.left visibleContentLayoutGuideRight.constant = -newValue.right visibleContentLayoutGuideBottom.constant = -newValue.bottom } } private let visibleContentLayoutGuide = UILayoutGuide() private var visibleContentLayoutGuideTop: NSLayoutConstraint! private var visibleContentLayoutGuideLeft: NSLayoutConstraint! private var visibleContentLayoutGuideRight: NSLayoutConstraint! private var visibleContentLayoutGuideBottom: NSLayoutConstraint! private var contentWrapperHeight: NSLayoutConstraint! private var contentViewTopEqualSuper: NSLayoutConstraint? private var contentViewTopGreaterThanSuper: NSLayoutConstraint? private var contentViewLeft: NSLayoutConstraint? private var contentViewRight: NSLayoutConstraint? private var contentViewBottom: NSLayoutConstraint? private func setupLayout() { setupVisibleContentLayoutGuide() setupScrollViewLayout() setupContentWrapperViewLayout() } private func setupScrollViewLayout() { scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.topAnchor.constraint(equalTo: topAnchor).isActive = true scrollView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true scrollView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true scrollView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true } private func setupContentWrapperViewLayout() { contentWrapperView.translatesAutoresizingMaskIntoConstraints = false contentWrapperView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true contentWrapperView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true contentWrapperView.rightAnchor.constraint(equalTo: scrollView.rightAnchor).isActive = true contentWrapperView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true contentWrapperView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true contentWrapperHeight = contentWrapperView.heightAnchor.constraint( greaterThanOrEqualTo: visibleContentLayoutGuide.heightAnchor ) contentWrapperHeight.isActive = contentViewStretching } private func setupVisibleContentLayoutGuide() { addLayoutGuide(visibleContentLayoutGuide) visibleContentLayoutGuideTop = visibleContentLayoutGuide.topAnchor.constraint(equalTo: topAnchor) visibleContentLayoutGuideTop.isActive = true visibleContentLayoutGuideLeft = visibleContentLayoutGuide.leftAnchor.constraint(equalTo: leftAnchor) visibleContentLayoutGuideLeft.isActive = true visibleContentLayoutGuideRight = visibleContentLayoutGuide.rightAnchor.constraint(equalTo: rightAnchor) visibleContentLayoutGuideRight.priority = .defaultHigh visibleContentLayoutGuideRight.isActive = true visibleContentLayoutGuideBottom = visibleContentLayoutGuide.bottomAnchor.constraint(equalTo: bottomAnchor) visibleContentLayoutGuideBottom.priority = .defaultHigh visibleContentLayoutGuideBottom.isActive = true } private func setupLayout(contentView view: UIView) { view.translatesAutoresizingMaskIntoConstraints = false contentViewTopEqualSuper = view.topAnchor.constraint(equalTo: contentWrapperView.topAnchor) contentViewTopEqualSuper?.constant = contentInsets.top contentViewTopEqualSuper?.isActive = alignContentToBottom == false contentViewTopGreaterThanSuper = view.topAnchor.constraint(greaterThanOrEqualTo: contentWrapperView.topAnchor) contentViewTopGreaterThanSuper?.constant = contentInsets.top contentViewTopGreaterThanSuper?.isActive = alignContentToBottom == true contentViewLeft = view.leftAnchor.constraint(equalTo: contentWrapperView.leftAnchor) contentViewLeft?.constant = contentInsets.left contentViewLeft?.isActive = true contentViewRight = view.rightAnchor.constraint(equalTo: contentWrapperView.rightAnchor) contentViewRight?.constant = -contentInsets.right contentViewRight?.isActive = true contentViewBottom = view.bottomAnchor.constraint(equalTo: contentWrapperView.bottomAnchor) contentViewBottom?.constant = -contentInsets.bottom contentViewBottom?.isActive = true } }