218 lines
6.9 KiB
Swift
218 lines
6.9 KiB
Swift
//
|
|
// SheetViewController.swift
|
|
// Sheet
|
|
//
|
|
// Created by Guilherme Rambo on 05/08/19.
|
|
// Copyright © 2019 Guilherme Rambo. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
class SheetViewController: UIViewController {
|
|
|
|
let metrics: SheetMetrics
|
|
|
|
init(metrics: SheetMetrics) {
|
|
self.metrics = metrics
|
|
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError()
|
|
}
|
|
|
|
private var container: SheetContainerViewController? {
|
|
return parent as? SheetContainerViewController
|
|
}
|
|
|
|
var rubberBandingStartHandler: (() -> Void)?
|
|
var rubberBandingUpdateHandler: ((CGFloat) -> Void)?
|
|
var rubberBandingFinishedHandler: (() -> Void)?
|
|
|
|
var isScrollingEnabled = true
|
|
|
|
private let scrollViewAtTheTopDeltaThreshold: CGFloat = 3
|
|
|
|
var isScrollViewAtTheTop: Bool {
|
|
return abs(scrollView.contentOffset.y - scrollView.contentInset.top * -1) < scrollViewAtTheTopDeltaThreshold
|
|
}
|
|
|
|
private lazy var contentView: SheetContentView = {
|
|
let v = SheetContentView(metrics: self.metrics)
|
|
|
|
v.layer.cornerRadius = view.layer.cornerRadius
|
|
v.layer.maskedCorners = view.layer.maskedCorners
|
|
v.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
v.clipsToBounds = true
|
|
|
|
return v
|
|
}()
|
|
|
|
private(set) lazy var scrollView: UIScrollView = {
|
|
let v = UIScrollView()
|
|
|
|
v.translatesAutoresizingMaskIntoConstraints = false
|
|
v.delegate = self
|
|
|
|
return v
|
|
}()
|
|
|
|
override func loadView() {
|
|
view = UIView()
|
|
|
|
view.backgroundColor = #colorLiteral(red: 0.9411764706, green: 0.9411764706, blue: 0.9411764706, alpha: 1)
|
|
view.layer.cornerRadius = metrics.cornerRadius
|
|
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
view.layer.shadowColor = UIColor.black.cgColor
|
|
view.layer.shadowOpacity = Float(metrics.shadowOpacity)
|
|
view.layer.shadowRadius = metrics.shadowRadius
|
|
view.layer.shadowOffset = CGSize(width: 0, height: -1)
|
|
|
|
contentView.frame = view.bounds
|
|
view.addSubview(contentView)
|
|
|
|
contentView.addSubview(scrollView)
|
|
|
|
NSLayoutConstraint.activate([
|
|
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
|
])
|
|
}
|
|
|
|
private weak var contentController: UIViewController?
|
|
|
|
func installContent(_ content: UIViewController) {
|
|
contentController = content
|
|
|
|
addChild(content)
|
|
content.view.translatesAutoresizingMaskIntoConstraints = false
|
|
scrollView.addSubview(content.view)
|
|
|
|
content.didMove(toParent: self)
|
|
|
|
NSLayoutConstraint.activate([
|
|
content.view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
|
|
content.view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
|
|
content.view.topAnchor.constraint(equalTo: scrollView.topAnchor),
|
|
content.view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
|
|
content.view.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
|
|
content.view.heightAnchor.constraint(equalTo: scrollView.heightAnchor)
|
|
])
|
|
}
|
|
|
|
var availableHeight: CGFloat {
|
|
guard let parentView = parent?.view else { return 0 }
|
|
|
|
return parentView.bounds.intersection(view.frame).height
|
|
}
|
|
|
|
private var scrolledUpOnFirstContentInsetUpdate = false
|
|
|
|
private var initialContentOffset: CGPoint = .zero
|
|
private var previousContentOffset: CGPoint = .zero
|
|
|
|
func updateContentInsets() {
|
|
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: metrics.trueSheetHeight - availableHeight, right: 0)
|
|
|
|
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(scrollUpOnFirstContentInsetUpdateIfNeeded), object: nil)
|
|
perform(#selector(scrollUpOnFirstContentInsetUpdateIfNeeded), with: nil, afterDelay: 0)
|
|
}
|
|
|
|
@objc private func scrollUpOnFirstContentInsetUpdateIfNeeded() {
|
|
guard !scrollView.contentInset.top.isZero else { return }
|
|
guard !scrolledUpOnFirstContentInsetUpdate else { return }
|
|
scrolledUpOnFirstContentInsetUpdate = true
|
|
|
|
scrollView.setContentOffset(CGPoint(x: 0, y: -scrollView.contentInset.top), animated: false)
|
|
}
|
|
|
|
deinit {
|
|
print("\(String(describing: type(of: self))) DEINIT")
|
|
}
|
|
|
|
}
|
|
|
|
final class SheetContentView: UIView {
|
|
|
|
let metrics: SheetMetrics
|
|
|
|
init(metrics: SheetMetrics) {
|
|
self.metrics = metrics
|
|
|
|
super.init(frame: .zero)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError()
|
|
}
|
|
|
|
}
|
|
|
|
extension SheetViewController: UIScrollViewDelegate {
|
|
|
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
initialContentOffset = scrollView.contentOffset
|
|
}
|
|
|
|
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
|
|
rubberBandingStartHandler?()
|
|
}
|
|
|
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
rubberBandingFinishedHandler?()
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
defer { previousContentOffset = scrollView.contentOffset }
|
|
|
|
var isRubberBandingUp = false
|
|
var bandOffset: CGFloat = 0
|
|
|
|
if scrollView.isDecelerating {
|
|
let effectiveOffset = scrollView.contentOffset.y + scrollView.contentInset.top
|
|
|
|
if scrollView.contentOffset.y < initialContentOffset.y {
|
|
if effectiveOffset < 0 {
|
|
isRubberBandingUp = true
|
|
bandOffset = effectiveOffset
|
|
rubberBandingUpdateHandler?(effectiveOffset)
|
|
}
|
|
}
|
|
}
|
|
|
|
if isRubberBandingUp {
|
|
// Counteract rubber banding by shifting contents so that they are flush with the top.
|
|
// We can't use setContentOffset here because that kills the rubber banding.
|
|
CATransaction.begin()
|
|
CATransaction.setDisableActions(true)
|
|
CATransaction.setAnimationDuration(0)
|
|
|
|
contentController?.view.layer.transform = CATransform3DMakeTranslation(0, bandOffset, 0)
|
|
|
|
CATransaction.commit()
|
|
} else {
|
|
let currentTransform = contentController?.view.layer.transform ?? CATransform3DIdentity
|
|
|
|
if !CATransform3DIsIdentity(currentTransform) {
|
|
CATransaction.begin()
|
|
CATransaction.setDisableActions(true)
|
|
CATransaction.setAnimationDuration(0)
|
|
|
|
contentController?.view.layer.transform = CATransform3DIdentity
|
|
|
|
CATransaction.commit()
|
|
}
|
|
}
|
|
|
|
guard isScrollingEnabled else {
|
|
scrollView.setContentOffset(previousContentOffset, animated: false)
|
|
return
|
|
}
|
|
}
|
|
|
|
}
|