Add sheet container delegate

This commit is contained in:
Shadowfacts 2019-09-24 11:43:46 -04:00
parent 3dc36d98c1
commit 1eea2313cd
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
3 changed files with 65 additions and 38 deletions

View File

@ -8,7 +8,7 @@
import UIKit
public enum Detent {
public enum Detent: Equatable {
case top
case middle
case bottom

View File

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

View File

@ -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
}
}