diff --git a/SheetImagePicker/SheetContainerViewController.swift b/SheetImagePicker/SheetContainerViewController.swift index f49d70d..54dcffd 100644 --- a/SheetImagePicker/SheetContainerViewController.swift +++ b/SheetImagePicker/SheetContainerViewController.swift @@ -14,13 +14,17 @@ public class SheetContainerViewController: 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 @@ -49,6 +53,18 @@ public class SheetContainerViewController: UIViewController { 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: @@ -57,31 +73,63 @@ public class SheetContainerViewController: UIViewController { case .changed: let translation = recognizer.translation(in: content.view) var realOffset = initialConstant + translation.y - if realOffset < view.safeAreaInsets.top { - print(realOffset) - realOffset = view.safeAreaInsets.top - realOffset / pow(CGFloat(M_E), realOffset / view.safeAreaInsets.top) + if realOffset < sortedDetentOffsets.first! { + print("should change") + 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: - if let offset = nearestDetentOffset(offset: topConstraint.constant) { - let distance = abs(topConstraint.constant - offset) - self.topConstraint.constant = offset - let velocity = recognizer.velocity(in: view) - let springVelocity = velocity.y / distance - UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: springVelocity, animations: { - self.view.layoutIfNeeded() - }) + 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(offset: CGFloat) -> CGFloat? { - return detents.map { $0.offset(in: view) }.min { (a, b) -> Bool in - return abs(offset - a) < abs(offset - b) + 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 }) } }