diff --git a/SheetImagePicker/Detent.swift b/SheetImagePicker/Detent.swift index 930e36a..a287198 100644 --- a/SheetImagePicker/Detent.swift +++ b/SheetImagePicker/Detent.swift @@ -8,7 +8,7 @@ import UIKit -public enum Detent { +public enum Detent: Equatable { case top case middle case bottom diff --git a/SheetImagePicker/SheetContainerViewController.swift b/SheetImagePicker/SheetContainerViewController.swift index 1543898..a523a58 100644 --- a/SheetImagePicker/SheetContainerViewController.swift +++ b/SheetImagePicker/SheetContainerViewController.swift @@ -8,16 +8,29 @@ 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] { - didSet { - sortDetents() - } + 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 sortedDetentOffsets: [CGFloat] = [] var topConstraint: NSLayoutConstraint! lazy var initialConstant: CGFloat = view.bounds.height / 2 @@ -52,19 +65,7 @@ public class SheetContainerViewController: UIViewController { 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: @@ -73,7 +74,7 @@ public class SheetContainerViewController: UIViewController { case .changed: let translation = recognizer.translation(in: content.view) var realOffset = initialConstant + translation.y - if realOffset < sortedDetentOffsets.first! { + if realOffset < topDetent.offset { func clamp(_ value: CGFloat, from: CGFloat, to: CGFloat) -> CGFloat { if value < from { return from @@ -88,7 +89,7 @@ public class SheetContainerViewController: UIViewController { // 3x^2 - 2x^3 return 3 * pow(x, 2) - 2 * pow(x, 3) } - let topOffset = sortedDetentOffsets.first! + let topOffset = topDetent.offset let smoothed = smoothstep(value: realOffset, from: topOffset, to: 0) realOffset = topOffset - smoothed * maximumStretchDistance @@ -98,37 +99,46 @@ public class SheetContainerViewController: UIViewController { case .ended: let velocity = recognizer.velocity(in: view) - let springToDetent: CGFloat + let springToDetent: (Detent, 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 + let detentInVelocityDirection = nearestDetentOffset(currentOffset: topConstraint.constant, direction: velocity.y) { + springToDetent = detentInVelocityDirection + } else if let nearestDetent = nearestDetentOffset(currentOffset: topConstraint.constant) { + springToDetent = nearestDetent } 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() - }) + 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.6, initialSpringVelocity: springVelocity, animations: { + self.view.layoutIfNeeded() + }, completion: { (finished) in + self.delegate?.sheetContainer(self, didSnapToDetent: springToDetent.0) + }) + } default: return } } - func nearestDetentOffset(currentOffset: CGFloat) -> CGFloat? { - return sortedDetentOffsets.min(by: { abs($0 - currentOffset) < abs($1 - currentOffset) }) + 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) -> CGFloat? { + 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 sortedDetentOffsets.last(where: { $0 < currentOffset }) + return sorted.last(where: { $0.1 < currentOffset }) } else { - return sortedDetentOffsets.first(where: { $0 > currentOffset }) + return sorted.first(where: { $0.1 > currentOffset }) } } diff --git a/SheetImagePickerTest/ViewController.swift b/SheetImagePickerTest/ViewController.swift index 4bf8546..50f3380 100644 --- a/SheetImagePickerTest/ViewController.swift +++ b/SheetImagePickerTest/ViewController.swift @@ -19,6 +19,8 @@ class ViewController: UIViewController { content.view.translatesAutoresizingMaskIntoConstraints = false content.view.backgroundColor = .red let sheet = SheetContainerViewController(content: content) + sheet.delegate = self + sheet.detents = [.bottom, .middle, .top] sheet.view.backgroundColor = .blue addChild(sheet) @@ -34,3 +36,18 @@ class ViewController: UIViewController { } +extension ViewController: SheetContainerViewControllerDelegate { + func sheetContainer(_ sheetContainer: SheetContainerViewController, willSnapToDetent detent: Detent) -> Bool { + if detent == .bottom { + UIView.animate(withDuration: 0.35, animations: { + sheetContainer.view.transform = CGAffineTransform(translationX: 0, y: sheetContainer.view.bounds.height) + }, completion: { (finished) in + sheetContainer.removeFromParent() + sheetContainer.didMove(toParent: nil) + sheetContainer.view.removeFromSuperview() + }) + return false + } + return true + } +}