192 lines
7.8 KiB
Swift
192 lines
7.8 KiB
Swift
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
|
|
}
|
|
}
|