165 lines
6.8 KiB
Swift
165 lines
6.8 KiB
Swift
//
|
|
// SheetContainerViewController.swift
|
|
// SheetImagePicker
|
|
//
|
|
// Created by Shadowfacts on 9/23/19.
|
|
// Copyright © 2019 Shadowfacts. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
public protocol SheetContainerViewControllerDelegate {
|
|
func sheetContainer(_ sheetContainer: SheetContainerViewController, willSnapToDetent detent: Detent) -> Bool
|
|
func sheetContainer(_ sheetContainer: SheetContainerViewController, didSnapToDetent detent: Detent)
|
|
}
|
|
|
|
// default no-op implementation
|
|
public extension SheetContainerViewControllerDelegate {
|
|
func sheetContainer(_ sheetContainer: SheetContainerViewController, willSnapToDetent detent: Detent) -> Bool {
|
|
return true
|
|
}
|
|
func sheetContainer(_ sheetContainer: SheetContainerViewController, didSnapToDetent detent: Detent) {}
|
|
}
|
|
|
|
public class SheetContainerViewController: UIViewController {
|
|
|
|
public var delegate: SheetContainerViewControllerDelegate?
|
|
|
|
let content: UIViewController
|
|
|
|
public var detents: [Detent] = [.bottom, .middle, .top]
|
|
var topDetent: (detent: Detent, offset: CGFloat) {
|
|
return detents.map { ($0, $0.offset(in: view)) }.min(by: { $0.1 < $1.1 })!
|
|
}
|
|
var bottomDetent: (detent: Detent, offset: CGFloat) {
|
|
return detents.map { ($0, $0.offset(in: view)) }.max(by: { $0.1 < $1.1 })!
|
|
}
|
|
|
|
public var minimumDetentJumpVelocity: CGFloat = 500
|
|
public var maximumStretchDistance: CGFloat = 15
|
|
|
|
var topConstraint: NSLayoutConstraint!
|
|
lazy var initialConstant: CGFloat = view.bounds.height / 2
|
|
|
|
var dimmingView: UIView!
|
|
public var minimumDimmingAlpha: CGFloat = 0
|
|
public var maximumDimmingAlpha: CGFloat = 0.75
|
|
|
|
public init(content: UIViewController) {
|
|
self.content = content
|
|
|
|
super.init(nibName: nil, bundle: nil)
|
|
|
|
modalPresentationStyle = .custom
|
|
transitioningDelegate = self
|
|
}
|
|
|
|
required public init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override public func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
dimmingView = UIView()
|
|
dimmingView.translatesAutoresizingMaskIntoConstraints = false
|
|
dimmingView.backgroundColor = .black
|
|
dimmingView.alpha = (maximumDimmingAlpha - minimumDimmingAlpha) / 2
|
|
view.addSubview(dimmingView)
|
|
NSLayoutConstraint.activate([
|
|
dimmingView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
dimmingView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
dimmingView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
dimmingView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
|
])
|
|
|
|
addChild(content)
|
|
content.didMove(toParent: self)
|
|
view.addSubview(content.view)
|
|
topConstraint = content.view.topAnchor.constraint(equalTo: view.topAnchor, constant: initialConstant)
|
|
NSLayoutConstraint.activate([
|
|
content.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
content.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
topConstraint,
|
|
content.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
|
])
|
|
|
|
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognized(_:)))
|
|
content.view.addGestureRecognizer(panGesture)
|
|
}
|
|
|
|
@objc func panGestureRecognized(_ recognizer: UIPanGestureRecognizer) {
|
|
switch recognizer.state {
|
|
case .began:
|
|
initialConstant = topConstraint.constant
|
|
|
|
case .changed:
|
|
let translation = recognizer.translation(in: content.view)
|
|
var realOffset = initialConstant + translation.y
|
|
let topOffset = topDetent.offset
|
|
if realOffset < topOffset {
|
|
let smoothed = smoothstep(value: realOffset, from: topOffset, to: 0)
|
|
realOffset = topOffset - smoothed * maximumStretchDistance
|
|
|
|
}
|
|
topConstraint.constant = realOffset
|
|
dimmingView.alpha = lerp(realOffset, min: topOffset, max: bottomDetent.offset, from: maximumDimmingAlpha, to: minimumDimmingAlpha)
|
|
|
|
case .ended:
|
|
let velocity = recognizer.velocity(in: view)
|
|
|
|
let springToDetent: (Detent, CGFloat)
|
|
if abs(velocity.y) > minimumDetentJumpVelocity,
|
|
let detentInVelocityDirection = nearestDetentOffset(currentOffset: topConstraint.constant, direction: velocity.y) {
|
|
springToDetent = detentInVelocityDirection
|
|
} else if let nearestDetent = nearestDetentOffset(currentOffset: topConstraint.constant) {
|
|
springToDetent = nearestDetent
|
|
} else {
|
|
return
|
|
}
|
|
|
|
if delegate?.sheetContainer(self, willSnapToDetent: springToDetent.0) ?? true {
|
|
let springDistance = abs(topConstraint.constant - springToDetent.1)
|
|
self.topConstraint.constant = springToDetent.1
|
|
let springVelocity = velocity.y / springDistance
|
|
|
|
UIView.animate(withDuration: 0.35, delay: 0, usingSpringWithDamping: 0.75, initialSpringVelocity: springVelocity, animations: {
|
|
self.view.layoutIfNeeded()
|
|
self.dimmingView.alpha = lerp(springToDetent.1, min: self.topDetent.offset, max: self.bottomDetent.offset, from: self.maximumDimmingAlpha, to: self.minimumDimmingAlpha)
|
|
}, completion: { (finished) in
|
|
self.delegate?.sheetContainer(self, didSnapToDetent: springToDetent.0)
|
|
})
|
|
}
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
|
|
func nearestDetentOffset(currentOffset: CGFloat) -> (Detent, CGFloat)? {
|
|
return detents.map { ($0, $0.offset(in: view)) }.min { (a, b) -> Bool in
|
|
return abs(a.1 - currentOffset) < abs(b.1 - currentOffset)
|
|
}
|
|
}
|
|
|
|
func nearestDetentOffset(currentOffset: CGFloat, direction: CGFloat) -> (Detent, CGFloat)? {
|
|
let sorted = detents.map { ($0, $0.offset(in: view)) }.sorted { (a, b) -> Bool in
|
|
return a.1 < b.1
|
|
}
|
|
if direction < 0 {
|
|
return sorted.last(where: { $0.1 < currentOffset })
|
|
} else {
|
|
return sorted.first(where: { $0.1 > currentOffset })
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension SheetContainerViewController: UIViewControllerTransitioningDelegate {
|
|
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
|
return SheetContainerPresentationAnimationController()
|
|
}
|
|
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
|
return SheetContainerDismissAnimationController()
|
|
}
|
|
}
|