|
|
|
@ -44,6 +44,7 @@ public class SheetContainerViewController: UIViewController {
|
|
|
|
|
var dimmingView: UIView!
|
|
|
|
|
public var minimumDimmingAlpha: CGFloat = 0
|
|
|
|
|
public var maximumDimmingAlpha: CGFloat = 0.75
|
|
|
|
|
var initialScrollViewContentOffset = CGPoint.zero
|
|
|
|
|
|
|
|
|
|
public init(content: UIViewController) {
|
|
|
|
|
self.content = content
|
|
|
|
@ -84,8 +85,12 @@ public class SheetContainerViewController: UIViewController {
|
|
|
|
|
dimmingView.bottomAnchor.constraint(equalTo: content.view.topAnchor, constant: content.view.layer.cornerRadius)
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognized(_:)))
|
|
|
|
|
content.view.addGestureRecognizer(panGesture)
|
|
|
|
|
if let scrollView = content.view as? UIScrollView {
|
|
|
|
|
scrollView.panGestureRecognizer.addTarget(self, action: #selector(scrollViewPanGestureRecognized))
|
|
|
|
|
} else {
|
|
|
|
|
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognized(_:)))
|
|
|
|
|
content.view.addGestureRecognizer(panGesture)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func panGestureRecognized(_ recognizer: UIPanGestureRecognizer) {
|
|
|
|
@ -95,44 +100,90 @@ public class SheetContainerViewController: UIViewController {
|
|
|
|
|
|
|
|
|
|
case .changed:
|
|
|
|
|
let translation = recognizer.translation(in: content.view)
|
|
|
|
|
var realOffset = initialConstant + translation.y
|
|
|
|
|
let topOffset = topDetent.offset
|
|
|
|
|
if realOffset < topOffset {
|
|
|
|
|
let smoothed = smoothstep(value: realOffset, from: topOffset, to: 0)
|
|
|
|
|
realOffset = topOffset - smoothed * maximumStretchDistance
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
topConstraint.constant = realOffset
|
|
|
|
|
dimmingView.alpha = lerp(realOffset, min: topOffset, max: bottomDetent.offset, from: maximumDimmingAlpha, to: minimumDimmingAlpha)
|
|
|
|
|
let realOffset = initialConstant + translation.y
|
|
|
|
|
setTopOffset(realOffset)
|
|
|
|
|
|
|
|
|
|
case .ended:
|
|
|
|
|
let velocity = recognizer.velocity(in: view)
|
|
|
|
|
springToNearestDetent(verticalVelocity: velocity.y)
|
|
|
|
|
|
|
|
|
|
let springToDetent: (Detent, CGFloat)
|
|
|
|
|
if abs(velocity.y) > minimumDetentJumpVelocity,
|
|
|
|
|
let detentInVelocityDirection = nearestDetentOffset(currentOffset: topConstraint.constant, direction: velocity.y) {
|
|
|
|
|
springToDetent = detentInVelocityDirection
|
|
|
|
|
} else if let nearestDetent = nearestDetentOffset(currentOffset: topConstraint.constant) {
|
|
|
|
|
springToDetent = nearestDetent
|
|
|
|
|
} else {
|
|
|
|
|
return
|
|
|
|
|
default:
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func scrollViewPanGestureRecognized(_ recognizer: UIPanGestureRecognizer) {
|
|
|
|
|
guard let scrollView = recognizer.view as? UIScrollView else { return }
|
|
|
|
|
|
|
|
|
|
let translation = recognizer.translation(in: scrollView)
|
|
|
|
|
let velocity = recognizer.velocity(in: scrollView)
|
|
|
|
|
|
|
|
|
|
let shouldMoveSheetDown = scrollView.contentOffset.y <= 0 && velocity.y > 0 // scrolled to top and dragging down
|
|
|
|
|
let shouldMoveSheetUp = topConstraint.constant > topDetent.offset && velocity.y < 0 // not fully expanded and dragging up
|
|
|
|
|
|
|
|
|
|
let shouldMoveSheet = shouldMoveSheetDown || shouldMoveSheetUp
|
|
|
|
|
if shouldMoveSheet {
|
|
|
|
|
scrollView.bounces = false
|
|
|
|
|
scrollView.setContentOffset(.zero, animated: false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch recognizer.state {
|
|
|
|
|
case .began:
|
|
|
|
|
initialScrollViewContentOffset = scrollView.contentOffset
|
|
|
|
|
|
|
|
|
|
case .changed:
|
|
|
|
|
if shouldMoveSheet {
|
|
|
|
|
setTopOffset(topConstraint.constant + translation.y)
|
|
|
|
|
recognizer.setTranslation(initialScrollViewContentOffset, in: scrollView)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.75, initialSpringVelocity: springVelocity, animations: {
|
|
|
|
|
self.view.layoutIfNeeded()
|
|
|
|
|
self.dimmingView.alpha = lerp(springToDetent.1, min: self.topDetent.offset, max: self.bottomDetent.offset, from: self.maximumDimmingAlpha, to: self.minimumDimmingAlpha)
|
|
|
|
|
}, completion: { (finished) in
|
|
|
|
|
self.delegate?.sheetContainer(self, didSnapToDetent: springToDetent.0)
|
|
|
|
|
})
|
|
|
|
|
case .ended:
|
|
|
|
|
scrollView.bounces = true
|
|
|
|
|
if shouldMoveSheet {
|
|
|
|
|
springToNearestDetent(verticalVelocity: velocity.y)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func setTopOffset(_ offset: CGFloat) {
|
|
|
|
|
var offset = offset
|
|
|
|
|
|
|
|
|
|
let topOffset = topDetent.offset
|
|
|
|
|
if offset < topOffset {
|
|
|
|
|
let smoothed = smoothstep(value: offset, from: topOffset, to: 0)
|
|
|
|
|
offset = topOffset - smoothed * maximumStretchDistance
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
topConstraint.constant = offset
|
|
|
|
|
dimmingView.alpha = lerp(offset, min: topOffset, max: bottomDetent.offset, from: maximumDimmingAlpha, to: minimumDimmingAlpha)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func springToNearestDetent(verticalVelocity velocity: CGFloat) {
|
|
|
|
|
let springToDetent: (Detent, CGFloat)
|
|
|
|
|
if abs(velocity) > minimumDetentJumpVelocity,
|
|
|
|
|
let detentInVelocityDirection = nearestDetentOffset(currentOffset: topConstraint.constant, direction: velocity) {
|
|
|
|
|
springToDetent = detentInVelocityDirection
|
|
|
|
|
} else if let nearestDetent = nearestDetentOffset(currentOffset: topConstraint.constant) {
|
|
|
|
|
springToDetent = nearestDetent
|
|
|
|
|
} else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if delegate?.sheetContainer(self, willSnapToDetent: springToDetent.0) ?? true {
|
|
|
|
|
let springDistance = abs(topConstraint.constant - springToDetent.1)
|
|
|
|
|
self.topConstraint.constant = springToDetent.1
|
|
|
|
|
let springVelocity = velocity / springDistance
|
|
|
|
|
|
|
|
|
|
UIView.animate(withDuration: 0.35, delay: 0, usingSpringWithDamping: 0.75, initialSpringVelocity: springVelocity, animations: {
|
|
|
|
|
self.view.layoutIfNeeded()
|
|
|
|
|
self.dimmingView.alpha = lerp(springToDetent.1, min: self.topDetent.offset, max: self.bottomDetent.offset, from: self.maximumDimmingAlpha, to: self.minimumDimmingAlpha)
|
|
|
|
|
}, completion: { (finished) in
|
|
|
|
|
self.delegate?.sheetContainer(self, didSnapToDetent: springToDetent.0)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func nearestDetentOffset(currentOffset: CGFloat) -> (Detent, CGFloat)? {
|
|
|
|
|