Fix detent snapping

This commit is contained in:
Shadowfacts 2019-09-24 11:19:54 -04:00
parent 481f40519c
commit 3dc36d98c1
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
1 changed files with 62 additions and 15 deletions

View File

@ -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,62 @@ 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! {
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 })
}
}