Sheet/Sheet/Bottom Sheet/Chrome/SheetViewController.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
}
}
}