UIScrollViewDelegate - animateWithDuration in scrollViewDidEndDragging is not working as expected - ios

I have a UIScrollView with 2 sub views/pages side by side (horizontal content size = 2 * Screen Width + gutter space between pages). I would like to increase the completion speed of the animation, ie after the user has completed the dragging and lifted the finger. Based on the suggestions found in SO, I implemented a UIScrollViewDelegate as below.
class MyScrollViewDelegate: NSObject, UIScrollViewDelegate
{
var targetX: CGFloat = 0.0
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
{
// I have not implemented the paging logic yet as I wanted to test the replacement animation first.
// Below code simply uses the suggested tagetOffset, but limiting the same between the valid min and max values
targetX = fmax(0, fmin(scrollView.contentSize.width - scrollView.frame.size.width, targetContentOffset.memory.x))
}
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool)
{
UIView.animateWithDuration(0.25, animations: { scrollView.contentOffset = CGPointMake(self.targetX, 0) })
}
}
My understanding is that once the dragging is over, my own animation code in the scrollViewDidEndDragging will take over and complete the translation. The problem I am facing is that there seems to be an additional animation/jump when I pull the view inward when it is at the leftmost or rightmost edge.
For example, when the contentOffset.x = 0 (left edge), if I pull the view rightward by say 20 points and release (even after pausing for a second), the contentOffset.x & targetContentOffset.memory.x would be -20, and the calculated self.targetX would be 0. Hence in my animation I expect the page to move back towards left by 20 points with given speed as soon as I lift the finger. But what I observe is that, the page goes further to the right by almost the same amount (dragged distance) and then animates back from there to 0. The movement is so fast that I can't make out whether it is an animation or direct jump. Afterwards it follows my animation parameters to fall back.
As mentioned, the rightward jump seems to be proportional to the dragging distance, suggesting that my assumption about the "current position" before the start of animation is probably wrong. But I am not able to figure out the exact reason. Your help is much appreciated. The setup is ios 9.0, swift 2.0, xcode 7.0.1.
Edit 1:
I commented the animateWithDuration (but kept the scrollViewDidEndDragging). Animations stopped except in the edge region, where there is a default animation pulling the content back. Further reading pointed to the bounce property. I have a doubt that this default animation is colliding with the one I supplied.
Edit 2:
The bounce animation seems to be the culprit. While searching in this direction I came across another question in SO (Cancel UIScrollView bounce after dragging) describing the issue and possible solutions.

The issue seems to be because of queuing up of bounce animation and the custom animation. For details please read - Cancel UIScrollView bounce after dragging. I am not sure how the chosen answer solves the problem of custom duration. There is another solution involving sub-classing. That also didn't work in my case. Not sure whether it is due to different iOS versions.
Following is what worked for me. It is only the basic snippet. You can enhance the same to have better animation curves and velocity handling.
func scrollViewWillBeginDecelerating(scrollView: UIScrollView)
{
// below line seems to prevent the insertion of bounce animation
scrollView.setContentOffset(scrollView.contentOffset, animated: false)
// provide your animation with custom options
UIView.animateWithDuration(0.25, animations: { scrollView.contentOffset = CGPointMake(targetX, targetY) })
}

Related

CollectionView messes with animations in touchesBegan and touchesEnded methods

I have a collection view that holds cells with image views. I trigger an animation when the user presses down on the image view (using touchesBegan), and another animation for when the user releases (using touchesEnded).
The animations work perfectly only if I hold down on my click then release (delayed click), but when I fast click, the animation jumps as if the duration was set to 0.
I believe the issue is because collection view is subclass of scroll view, and scrollViews "temporarily intercepts a touch-down event by starting a timer and, before the timer fires, seeing if the touching finger makes any movement. If the timer fires without a significant change in position, the scroll view sends tracking events to the touched subview of the content view."
https://developer.apple.com/documentation/uikit/uiscrollview#//apple_ref/doc/uid/TP40006922
From what I can gather, I think that the touch interception from the collection view is causing problems with the animation if the click is faster than the touch timer. When I test using a regular view instead of a collection view as the superview, the animation works perfectly and doesn't require a delayed click.
If this is the case, then how is the animation triggered for a fast-click at all? Moreover, how might I be able to tigger the animation without having to use a delayed click?
If this is not the case, then what might be the reason for this issue?
Here is my code for animation and touches:
func animateClickerAndBallPoint(newXpositionForClicker: CGFloat, newXpositionForBallPoint: CGFloat, ballPoint: UIImageView) {
UIView.animate(withDuration: 0.1) {
self.frame.origin.x = newXpositionForClicker
ballPoint.frame.origin.x = newXpositionForBallPoint
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let ballPoint = self.ballPoint else {return}
self.animateClickerAndBallPoint(newXpositionForClicker: 288, newXpositionForBallPoint: 11, ballPoint: ballPoint)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let ballPoint = self.ballPoint else {return}
if isInWritingMode == true {
animateClickerAndBallPoint(newXpositionForClicker: 306, newXpositionForBallPoint: 26, ballPoint: ballPoint)
isInWritingMode = false
} else {
animateClickerAndBallPoint(newXpositionForClicker: 297, newXpositionForBallPoint: 17, ballPoint: ballPoint)
isInWritingMode = true
}
}
As an alternative, rather than doing your own touchesBegan and touchesEnded, you might consider hooking into the button's beginTracking and endTracking.
For example, you could subclass the button and provide whatever animation you wanted:
class AnimatedButton: UIButton {
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
UIView.animate(withDuration: 0.25) {
self.transform = .init(scaleX: 0.8, y: 0.8)
}
return true
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
UIView.animate(withDuration: 0.25) {
self.transform = .identity
}
}
}
That yields:
Or, if you wanted to do it based upon the section of the cell, itself, you could hook into the collection view's "highlighting" mechanism as illustrated in https://stackoverflow.com/a/45664054/1271826.
BTW, you're assuming that the problem is the container view messing with touches. Are you 100% sure that's the problem? It could be a basic animation problem. E.g.
For example, if you start a second animation while the first one is still running, it won't finish the first animation but will rather immediately start the second animation from wherever it was mid-animation (and in modern iOS versions, using whatever speed at which it was currently traveling) and transition to the new destination.
For example, here are two views that I'm animating precisely the same distance down for one second down and then back up for another second. But for the view on the right, I started the second animation 0.1 seconds into the first animation (i.e. interrupting the first animation):
As you can see, because this second example is interrupting your animations, it looks like it's just snapping back.
I don't think it's likely in this scenario, but you have to be careful if you're using autolayout. If you do anything that triggers the auto layout engine to re-apply its constraints (and this could be practically any UI action, such as something as innocuous as setting the text in some unrelated label), that will stop whatever animations you have underway and will snap the views back to where the constraints dictated they should be.
If the views were laid out using auto layout, you may want to consider animating them using auto layout, too. For example, rather than adjusting the frame (or whatever), create IBOutlet for your constraint, change the constant for that constraint, and then animate the call to layoutIfNeeded.
Bottom line, you might want to see if you can verify whether the problem is really related to touch events and not some unrelated animation problem.
Unfortunately, there's not enough in your example for us to diagnose what the source of the problem is. You might consider creating a MVCE, a minimal, verifiable, and complete example of the problem. And I'd suggest creating a MVCE without a collection view and another with, so you can confirm whether the collection view is actually the source of the problem. But, bottom line, until we can reproduce your problem, it's hard for us to help you solve the problem.
Have you tried delaysContentTouches = false?
https://developer.apple.com/documentation/uikit/uiscrollview/1619398-delayscontenttouches
Tells the scrollView/collectionView to not delay touches on the cells. Worked for me to make responsive cells immediately when I started tapping on them. Didn't make my scrolling buggy either.
I had the same kind of issues when playing with animations in scrollviews.
I fixed it by using that parameter in the animation: UIViewAnimationOptions.allowUserInteraction
So your animation block would end up like that:
UIView.animate(withDuration: duration,
delay: 0,
usingSpringWithDamping: CGFloat(0.30),
initialSpringVelocity: CGFloat(10.0),
options: UIViewAnimationOptions.allowUserInteraction,
animations: {
applyWhatEverTransform()
},completion: completion)
I'm a bit speculating on your issue, as we don't have code from you. But of course, if you use different sort of animations this could not work ;)
Also if that doesn't help. To help you debug, you could try to disable the scrolling of your collection view to see if that the event that intercept your touch events.

How to scroll UICollectionView that is underneath another UICollectionView?

So heres my issue, the 4 orange rectangles you see on the gif are a single vertical UICollectionView = orangeCollectionView.
The Green and Purple "card" views are part of another UICollectionView = overlayCollectionView.
overlayCollectionView has 3 cells, one of which is just a blank UICollectionViewCell, the other 2 are the cards.
When the overlayCollectionView is showing the blank UICollectionViewCell, I want to be able to scroll the orangeCollectionView.
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
guard let superr = superview else { return true}
for view in superr.subviews {
if view.isKind(of: OrangeCollectionView.self) {
view.point(inside: point, with: event)
return false
}
}
return true
}
This allows me to scroll the orangeCollectionView HOWEVER this doesn't actually work to fix my issue. I need to be able to scroll left and right to show the cards, however this blocks all touches becuase the point always falls on the OrangeCollectionView.
How can I check to see if they are scrolling left/right to show the cards? Otherwise if they are on the blank cell, scroll the orangeViewController up and down.
Had this issue as well... Didn't find a nice way to do it, but this works.
First, you need to access the scroll view delegate method scrollViewDidScroll(). There you call:
if scrollView == overlayScrollView {
if scrollView.contentOffset.x == self.view.frame.width { // don't know which coordinate you need
self.overlayScrollView.alpa = 0
}
}
After that, you add a blank view onto of the orange collection view. The view's alpha is 0 (I think, maybe the color is just clear; try it out if it works).
In the code, you then add a UISwipeGestureRecognizer to the view you just created and and detect whether there's a swipe to the left or to the right.
By detecting the direction of that swipe, you can simply change the contentOffset.x axis of your overlayScrollView to 0 or self.view.frame.width * 2.
Sorry I can't provide my working sample code, I'm answering from my mobile. It's not the proper solution, but when I made a food app for a big client it worked perfectly and no one ever complained :)

Zoom in/out on swiping tableView Animation

I want to achieve this animation:
Here is the Gif: http://imgur.com/4TZIbwp
Since the content is dynamic, I'm using tableView to populate the data.
I've tried scrollViewDidScroll delegate methods to change the constraints but it's not helping me. I've even tried swipe gesture, but still can't manage to achieve this.
Can anyone provide a knowledge, a bit of code for getting this animation.
I have tried to tackle your issue in this project.
The flaw with this solution is that it requires an unattractive inset at the top of the table view to translate the offset into a meaningful variable with which to shrink the table view.
The relevant code in the project is within the scroll delegate function:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let yOffset = min(0.0, max(-maximumOffset, scrollView.contentOffset.y))
let constant = -yOffset
topTableViewConstraint.constant = constant
leadingTableViewConstraint.constant = constant / 5.0
trailingTableViewConstraint.constant = constant / 5.0
view.layoutIfNeeded()
}
I am sorry that I cannot be more helpful, or provide you with a final solution.
Hopefully the project will aid you in find that answer.

iOS animation doesn't reset when replayed repeatedly

I have a very simple animation block which performs the following animation:
(tap the image if the animation doesn't play)
func didTapButton() {
// reset the animation to 0
centerYConstraint.constant = 0
superview!.layoutIfNeeded()
UIView.animate(
withDuration: 1,
animations: {
// animate the view downwards 30 points
self.centerYConstraint.constant = 30
self.superview!.layoutIfNeeded()
})
}
Everything is great when I play the animation by itself. It resets to position 0, then animates 30 points.
The problem is when the user taps the button multiple times quickly (i.e. during the middle of an ongoing animation). Each time the user taps the button, I would expect it to reset to position 0, then animate downwards 30 points. Instead, I get this behavior:
(tap the image if the animation doesn't play)
It's clearly traveling well over 30 points. (Closer to 120 points.)
Why is this happening, and how can I "reset" the animation properly so that it only at most travels 30 points?
Things that I have tried that didn't work:
Using options: UIViewAnimationOptions.beginFromCurrentState. It does the same exact behavior.
Instead of executing the animation directly, do it after a few milliseconds using dispatch_after. Same behavior.
"Canceling" the previous animation by using a 0 second animation that changes the constant to 0, and calls superview!.layoutIfNeeded.
Other notes:
If instead of changing the position, I change the height via a constraint, I get similar odd behavior. E.g. if I set it to go from 30 points to 15 points, when repeatedly pressing the button, the view will clearly grow up to around 120 points.
Even if I don't use constraints, and instead I animate the transform from CGAffineTransform.identity to CGAffineTransform(scaleX: 0.5, y: 0.5), I get the same behavior where it'll grow to 120 points.
If I try a completely different animatable property say backgroundColor = UIColor(white: 0, alpha: 0.5) to backgroundColor = UIColor(white: 0, alpha: 0), then I get correct behavior (i.e. each time I tap the button, the color resets to 0.5 gray at most. It never gets to black.
I can reproduce the behavior you describe. But if I call removeAllAnimations() on the layer of the view that is moving, the problem goes away.
#IBAction func didTapButton(_ sender: Any) {
animatedView.layer.removeAllAnimations()
centerYConstraint.constant = 0
view.layoutIfNeeded()
centerYConstraint.constant = 30
UIView.animate(withDuration: 2) {
self.view.layoutIfNeeded()
}
}
Note, I'm not removing the animation from the superview (because that still manifests the behavior you describe), but rather of the view that is moving.
So far, the only solution I found is to recreate the view rather than trying to reset the existing one.
This gives me the exact behavior I was looking for:
(tap the image if the animation doesn't play)
It's unfortunate that you need to remove the old view and create a new one, but that's the only workaround I have found.

Limiting vertical movement of UIAttachmentBehavior inside a UICollectionView

I have a horizontal UICollectionView with a custom UICollectionViewFlowLayout that has a UIAttachmentBehavior set on each cell to give it a bouncy feel when scrolling left and right. The behavior has the following properties:
attachmentBehavior.length = 1.0f;
attachmentBehavior.damping = 0.5f;
attachmentBehavior.frequency = 1.9f;
When a new cell is added to the collection view it's added at the bottom and then animated to its position also using a UIAttachmentBehavior. Naturally it bounces up and down a bit till it rests in its position. Everything is working as expected till now.
The problem I have starts appearing when the collection view is scrolled left or right before the newly added cell has come to rest. The adds left and right bounciness to the up and down one the cell already has from being added. This results in a very weird circular motion in the cell.
My question is, is it possible to stop the vertical motion of a UIAttachmentBehavior while the collection view is being scrolled? I've tried different approaches like using multiple attachment behaviors and disabling scrolling in the collection view till the newly added cell has come to rest, but non of them seem to stop this.
One way to solve this is to use the inherited .action property of the attachment behavior.
You will need to set up a couple of variables first, something like (going from memory, untested code):
BOOL limitVerticalMovement = TRUE;
CGFloat staticCenterY = CGRectGetHeight(self.collectionView.frame) / 2;
Set these as properties of your custom UICollectionViewFlowLayout
When you create your attachment behavior:
UIAttachmentBehavior *attachment = [[UIAttachmentBehavior alloc] initWithItem:item attachedToAnchor:center];
attachment.damping = 1.0f;
attachment.frequency = 1.5f;
attachment.action = ^{
if (!limitVerticalMovement) return;
CGPoint center = item.center;
center.y = staticCenterY;
item.center = center;
};
Then you can turn the limiting function on and off by setting limitVerticalMovement as appropriate.
Have you tried manually removing animations from cells with CALayer's removeAllAnimations?
You'll want to remove the behaviour when the collection view starts scrolling, or perhaps greatly reduce the springiness so that it comes to rest smoothly, but quickly. If you think about it, what you're seeing is a realistic movement for the attachment behaviour you've described.
To keep the vertical bouncing at the same rate but prevent horizontal bouncing, you'd need to add other behaviours - like a collision behaviour with boundaries to the left and right of each added cell. This is going to increase the complexity of the physics a little, and may affect scrolling performance, but it would be worth a try.
Here's how I managed to do it.
The FloatRange limits the range of the attachment, so if you want it to go all the way up and down the screen you just set really large numbers.
This goes inside func recognizePanGesture(sender: UIPanGestureRecognizer) {}
let location = sender.location(in: yourView.superview)
var direction = "Y"
var center = CGPoint(x: 0, y: 0)
if self.direction == "Y" {center.y = 1}
if self.direction == "X" {center.x = 1}
let sliding = UIAttachmentBehavior.slidingAttachment(with: youView, attachmentAnchor: location, axisOfTranslation: CGVector(dx: center.x, dy: center.y))
sliding.attachmentRange = UIFloatRange(minimum: -2000, maximum: 2000)
animator = UIDynamicAnimator(referenceView: self.superview!)
animator.addBehavior(sliding)
If you're using iOS 9 and above then sliding function within attachment class will work perfectly for that job:
class func slidingAttachmentWithItem(_ item: UIDynamicItem,
attachmentAnchor point: CGPoint,
axisOfTranslation axis: CGVector) -> Self
it can be used easily, and it's very effective for sliding Apple documentation
I've resorted to disabling scrolling in the collection view for a specific amount of time after a new cell is added, then removing the attachment behavior after that time has passed using its action property, then adding a new attachment behavior again immediately.
That way I make sure the upwards animation stops before the collection view is scrolled left or right, but also the left/right bounciness is still there when scrolling.
Certainly not the most elegant solution but it works.

Resources