SheetImagePicker/SheetImagePicker/SheetContainerViewControlle...

137 lines
4.7 KiB
Swift

//
// SheetContainerViewController.swift
// SheetImagePicker
//
// Created by Shadowfacts on 9/23/19.
// Copyright © 2019 Shadowfacts. All rights reserved.
//
import UIKit
public class SheetContainerViewController: UIViewController {
let content: UIViewController
public var detents: [Detent] = [.bottom, .middle, .top] {
didSet {
sortDetents()
}
}
var sortedDetentOffsets: [CGFloat] = []
var topConstraint: NSLayoutConstraint!
lazy var initialConstant: CGFloat = view.bounds.height / 2
public var minimumDetentJumpVelocity: CGFloat = 500
public var maximumStretchDistance: CGFloat = 15
public init(content: UIViewController) {
self.content = content
super.init(nibName: nil, bundle: nil)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func viewDidLoad() {
super.viewDidLoad()
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)
}
public override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
sortDetents()
}
private func sortDetents() {
sortedDetentOffsets = detents.map {
$0.offset(in: view)
}.sorted()
}
@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
if realOffset < sortedDetentOffsets.first! {
func clamp(_ value: CGFloat, from: CGFloat, to: CGFloat) -> CGFloat {
if value < from {
return from
} else if value > to {
return to
} else {
return value
}
}
func smoothstep(value: CGFloat, from: CGFloat, to: CGFloat) -> CGFloat {
let x = clamp((value - from) / (to - from), from: 0, to: 1)
// 3x^2 - 2x^3
return 3 * pow(x, 2) - 2 * pow(x, 3)
}
let topOffset = sortedDetentOffsets.first!
let smoothed = smoothstep(value: realOffset, from: topOffset, to: 0)
realOffset = topOffset - smoothed * maximumStretchDistance
}
topConstraint.constant = realOffset
case .ended:
let velocity = recognizer.velocity(in: view)
let springToDetent: CGFloat
if abs(velocity.y) > minimumDetentJumpVelocity,
let offsetInVelocityDirection = nearestDetentOffset(currentOffset: topConstraint.constant, direction: velocity.y) {
springToDetent = offsetInVelocityDirection
} else if let nearestOffset = nearestDetentOffset(currentOffset: topConstraint.constant) {
springToDetent = nearestOffset
} else {
return
}
let springDistance = abs(topConstraint.constant - springToDetent)
self.topConstraint.constant = springToDetent
let springVelocity = velocity.y / springDistance
UIView.animate(withDuration: 0.35, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: springVelocity, animations: {
self.view.layoutIfNeeded()
})
default:
return
}
}
func nearestDetentOffset(currentOffset: CGFloat) -> CGFloat? {
return sortedDetentOffsets.min(by: { abs($0 - currentOffset) < abs($1 - currentOffset) })
}
func nearestDetentOffset(currentOffset: CGFloat, direction: CGFloat) -> CGFloat? {
if direction < 0 {
return sortedDetentOffsets.last(where: { $0 < currentOffset })
} else {
return sortedDetentOffsets.first(where: { $0 > currentOffset })
}
}
}