CollectionView messes with animations in touchesBegan and touchesEnded methods - ios

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.

Related

Hide one UIView and show another

When I click on an a segment of my UISegmentedControl, I want one of two UIViews to be displayed. But how do I arrange it, that I only show the new view. Currently I am calling thisview.removeFromSuperview() on the old one, and then setup the new all from scratch. I also tried setting all the HeightConstants of the views subviews to zero and then set the heightConstants of the view itself to zero but I'd rather avoid that constraints-surgery..
What better approaches are there?
Agree with #rmaddy about using UIView's hidden property, a nice simple way to cause a view to not be drawn but still occupy its place in the view hierarchy and constraint system.
You can achieve a simple animation to make it a bit less jarring as follows:
UIView.animate(withDuration:0.4, animations: {
myView.alpha = 0
}) { (result: Bool) in
myView.isHidden = true
}
This will fade the alpha on the view "myView", then upon completion set it to hidden.
The same animation concept can be used also if you've views need to re-arrange themselves, animating layout changes will be a nice touch.
Based on #rmaddy and #CSmiths answer, I built the following function:
func changeView(newView: UIView, oldView: UIView) {
newView.isHidden = false
newView.alpha = 0
UIView.animate(withDuration:0.4, animations: {
oldView.alpha = 0
newView.alpha = 1
}) { (result: Bool) in
oldView.isHidden = true
}
}
I feel dumb now for all the hours I spent on that constraint-surgery. :|

Swift: Rounded corners appear different upon first appearance and reappearing

class ApplyCorners: UIButton {
override func didMoveToWindow() {
self.layer.cornerRadius = self.frame.height / 2
}
}
I apply this class to the buttons in my application and it is working great, but when I apply it to a button used in every cell in a table view the button corners are not round upon entering the view, but if I click one of the buttons I get segued to another view. If I then segue back the corners are "fixed" / round.
The green is the button when returning and the red is upon first entering the view.
Anyone know how to fix this?
I'd suggest layoutSubviews, which captures whenever the frame of the button changes:
class ApplyCorners: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = frame.height / 2
}
}
This takes care of both the original appearance and any subsequent appearance. It also avoids all sorts of problems related to not only whether the frame was known when the view appeared, but also if you do anything that might change the size of the button (e.g. anything related to constraints, rotation events, etc.).
This sort of thing is likely to be a timing problem. Consider the phrase self.frame.height. At the time didMoveToWindow is called, we may not yet know our frame. If you are going to call a method that depends upon layout, do so when layout has actually occurred.
Gonna propose another alternative: listen to any bounds changes. This avoids the problem of wondering "is my frame set yet when this is called?"
class ApplyCorners: UIButton {
override var bounds: CGRect {
didSet {
layer.cornerRadius = bounds.height / 2
}
}
}
Edited frame to bounds because as #Rob points out, listening for frame changes will cause you to miss the initial load sometimes.
Putting your code in didMoveToWindow() does not make sense to me. I'd suggest implementing layoutSubviews() instead. That method gets called any time a view object's layout changes, so it should update if you resize your view.
(Changed my suggestion based on comments from TNguyen and and Rob.)

Issue with horizontal scrollview inside a vertical scrollview in ios swift

My layout is currently like this:
View
-- View
-- Vertical ScrollView
------ View
--------- Horizontal Paginated ScrollView
--------- View
------------- Horizontal ScrollView -- not working properly
See this image for view hierarchy screenshot from xcode:
Using Swift.
I am adding subviews dynamically to this "Size Select Scroll View"
Two Issues:
After adding views, there is no margin between the subviews. Each subview's coord. are like this: (10.0, 0.0, 44.0, 44.0), (54.0, 0.0, 44.0, 44.0), (98.0, 0.0, 44.0, 44.0), (142.0, 0.0, 44.0, 44.0) etc.
But the appearance is like this without the 10 points gap between each subview: http://i.stack.imgur.com/mCGRW.png
Scrolling horizontally is a pain. It only works on maybe 1/4 height from top of the scrollview area and very difficult to scroll. How do i layout subviews so that this scrollview is properly scrollable?
Note: I am explicitly setting content size of size scrollview to more than required so that i can see the scrolling.
I found out the issue. According to the Apple Docs the touch events will be passed to a subview only if it lies entirely in its parent.
In my case, the scrollview was going out of bounds of its parent view ( Details View), because of which touch events were weird. I increased the parent view's size to fit the scrollview and it works fine now.
From the docs (https://developer.apple.com/library/content/qa/qa2013/qa1812.html):
The most common cause of this problem is because your view is located outside the bounds of its parent view. When your application receives a touch event, a hit-testing process is initiated to determine which view should receive the event. The process starts with the root of the view hierarchy, typically the application's window, and searches through the subviews in front to back order until it finds the frontmost view under the touch. That view becomes the hit-test view and receives the touch event. Each view involved in this process first tests if the event location is within its bounds. Only after the test is successful, does the view pass the event to the subviews for further hit-testing. So if your view is under the touch but located outside its parent view's bounds, the parent view will fail to test the event location and won't pass the touch event to your view.
This is always a challenge in iOS.
There are various solutions which unfortunately depend on the exact situation.
Here's a drop-in solution which is often the right solution.
/*
So, PASS any touch to the NEXT view, BUT ALSO if any of OUR
subviews are buttons, etc, then THOSE should ALSO work normally. :/
*/
import UIKit
class Passthrough: UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return subviews.contains(where: {
!$0.isHidden
&& $0.isUserInteractionEnabled
&& $0.point(inside: self.convert(point, to: $0), with: event)
})
}
}
(Of course, you can also just drop the call in to some class, eg
class SomeListOrWhatever: UICollectionView, .. {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
print("MIGHT AS WELL TRY THIS")
return subviews.contains(where: {
!$0.isHidden
&& $0.isUserInteractionEnabled
&& $0.point(inside: self.convert(point, to: $0), with: event)
})
}
Even if you "don't totally understand what the problem is", this is "one of" the solutions!
For example, this is (usually!) the precise solution to the exact issue quoted from the doco by #kishorer747
It's definitely a real nuisance in iOS.

Using animateWithDuration when a button is pressed but it has no effect (swift 2)

I'm using the following code to have a label slide onto the screen when a button is pressed, but it's having no effect.
override func viewDidLayoutSubviews() {
summaryLabel.alpha = 0
}
#IBAction func searchButton(sender: AnyObject) {
UIView.animateWithDuration(2) { () -> Void in
self.summaryLabel.center = CGPointMake(self.summaryLabel.center.x - 400, self.summaryLabel.center.y)
self.summaryLabel.alpha = 1
self.summaryLabel.center = CGPointMake(self.summaryLabel.center.x + 400, self.summaryLabel.center.y)
}
//button code continues...
I've tested what's going on by fixing the alpha at 1, but the label just stays where it is and does not move when the button is pressed. What am I missing?
A couple of things:
First of all, your two changes to the view's center cancel each other out. The animation applies the full set of changes that are inside the animation block all in one animation. If the end result is no change, then no change is applied. As Ramy says in his comment, you either need 2 animations, timed so the second one takes place after the first one has completed, or you nee to apply the first change before the animation begins. I would suggest starting with a single change, and a single animation, and get that working first.
Second problem: View controllers use auto layout by default. With auto layout, you can't animate the position of a view directly. It doesn't work reliably. Instead, you have to put a constraint on the view, connect it to an outlet, and the animate a change to the constraint's constant value by changing the constant and calling layoutIfNeeded() inside the animation block. The call to layoutIfNeeded() inside the animation block causes the view's position to be changed, and since it's inside the animation block, the change is applied with animation.

UIScrollViewDelegate - animateWithDuration in scrollViewDidEndDragging is not working as expected

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

Resources