// // 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 }) } } }