Reputation: 9590
I am setting a new value to targetContentOffset
in scrollViewWillEndDragging(_:withVelocity:targetContentOffset:)
to create a custom paging solution in a UITableView
. It works as expected when setting a coordinate for targetContentOffset
that is in the same scroll direction as the velocity is pointing.
However, when snapping "backward" in the opposite direction of the velocity it does immediatelly "snap back" without animation. This looks quite bad. Any thoughts on how to solve this.
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// a lot of caluculations to detemine where to "snap scroll" to
if shouldSnapScrollUpFromSpeed || shouldSnapScrollUpFromDistance {
targetContentOffset.pointee.y = -scrollView.frame.height
} else if shouldSnapScrollDownFromSpeed || shouldSnapScrollDownFromDistance {
targetContentOffset.pointee.y = -detailViewHeaderHeight
}
}
I could potentionally calculate when this "bug" will appear and perhaps use another way of "snap scrolling". Any suggestions on how to do this or solve it using targetContentOffset.pointee.y
as normal?
Upvotes: 8
Views: 2397
Reputation: 1160
In the following code threshold
is regarded as the trigger point for moving content in the same direction as the drag. If this threshold is not met, then the content returns to its original position (in the opposite direction).
The target content offset is expecting a value in the direction of its momentum, so you need to stop the momentum first. Then just move the view using a standard animation block.
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let distance = distanceTravelled(scrollView)
let isForwardDirection = (velocity.x > 0 || distance < 0)
if threshold(velocity, distance) {
let rect = makeRect(scrollView)
if let attribute = attribute(for: rect, isForwardDirection) {
currentIndexPath = attribute.indexPath
targetContentOffset.pointee = point(for: attribute)
}
} else if let attribute = flowLayout.layoutAttributesForItem(at: currentIndexPath) {
targetContentOffset.pointee = scrollView.contentOffset
DispatchQueue.main.async {
let point = self.point(for: attribute)
UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseOut, .allowUserInteraction]) {
scrollView.contentOffset = point
}
}
}
}
Upvotes: 0
Reputation: 12287
func scrollViewWillEndDragging(_ sv: UIScrollView,
withVelocity vel: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>) {
print("Apple wants to end at \(targetContentOffset.pointee)")
...
Let's say UIKit wants the throw to end at "355.69".
...
... your calculations and logic
solve = 280
You do YOUR calculation and logic. You want the throw to end at 280.
... your calculations and logic
solve = 280
targetContentOffset.pointee.y = solve
UIView.animate(withDuration: secs, delay: 0, options: .curveEaseOut, animations: {
theScroll.contentOffset.y = solve
})
That's all there is to it.
However there are MANY complications for a perfect result that matches UIKit behavior and feel.
it is a huge chore figuring out your "solve", where you want it to land next.
you need a lot of code to decide whether the user actually threw it (to your next stop position), or, whether they just "moved their finger around a little, and then let go"
if they did just "move their finger around a little, and then let go" you have to decide whether your next solve is the one they started on, the "next" one or the "previous" one of your clickstops.
a difficult issue is, in reality you'll wanna animate it using spring physics so as to match the normal stop that UIKit would do
even trickier, when you do the spring animation, you need to figure out and match the throw speed of the finger or it looks wrong
It's a real chore.
Upvotes: 1
Reputation: 9590
I found an okey solution (or workaround). Let me know if anyone have a better solution.
I just detected when the undesired "non-animated snap scroll" would appear and then instead of setting a new value to targetContentOffset.pointee.y
I set it to the current offset value (stopped it) and set the desired offset target value with scrollViews setContentOffset(_:animated:)
instead
if willSnapScrollBackWithoutAnimation {
targetContentOffset.pointee.y = -scrollView.frame.height+yOffset //Stop the scrolling
shouldSetTargetYOffsetDirectly = false
}
if let newTargetYOffset = newTargetYOffset {
if shouldSetTargetYOffsetDirectly {
targetContentOffset.pointee.y = newTargetYOffset
} else {
var newContentOffset = scrollView.contentOffset
newContentOffset.y = newTargetYOffset
scrollView.setContentOffset(newContentOffset, animated: true)
}
}
Upvotes: 1