Pan Gesture completion handler getting called immediately - ios

I'm animating a view to move as the user pans the screen. I have kept a threshold after which the view will animate to a default position.
The problem currently is that the completion handler of the animate method which resets the view to a position is called before the duration. The animation seems to be happening abruptly instead of over a duration of time.
// Pan gesture selector method
#objc func panAction(for panGesture: UIPanGestureRecognizer) {
switch panGesture.state {
case .began:
//Began code
case changed:
if (condition) {
print("IF")
//Change constraint constant of customView
animate(view: customView)
} else if (condition) {
print("ELSE IF")
//Change constraint constant of customView
animate(view: customView)
} else {
//Change constraint constant of customView
print("ELSE")
view.layoutIfNeeded()
}
case .ended:
//Ended code
default:
break
}
}
The animate method:
func animate(view: UIView) {
UIView.animate(withDuration: 3, delay: 0, options: .curveEaseOut, animations: {
view.layoutIfNeeded()
}, completion: { (finished) in
if finished {
flag = false
}
})
}
The flag is being set immediately rather than after 3 seconds.
The o/p i get while panning and crossing threshold.
ELSE
ELSE
ELSE
ELSE
IF
Edit: I am an idiot. I did not call layoutIfNeeded() on the superView.

Your gesture sends several events while the gesture is happening, and as you call your UIView.animate() code multiple times the new value supersedes the previous one.
Try adding the animation option .beginFromCurrentState:
UIView.animate(withDuration: 0.5, delay: 0, options: [.beginFromCurrentState,.allowAnimatedContent,.allowUserInteraction], animations: {
view.layoutIfNeeded()
}) { (completed) in
...
}
And expect your completed to be called multiple times with the completed == false as the gesture is progressing.
Edit: Your issue may also be related to calling layoutIfNeeded() on the wrong view, possibly try to call this on the viewController.view ?

I solved this. I was not calling layoutIfNeeded on the superview.

Related

removeAllAnimations() does not work for me in Swift 5

Hi I checked all related question but I cannot solve my problem , I have an animate function like this;
func startScrollSlideShow(sliderValue: Float) {
if (scrollView.contentOffset.y <= (scrollView.contentSize.height - scrollView.frame.size.height)){
//reach bottom
UIScrollView.animate(withDuration: TimeInterval(sliderValue), delay: 0.5, options: .allowUserInteraction, animations: {
self.scrollView.contentOffset.y += 3.5
}) { (completion) in
self.startScrollSlideShow(sliderValue: Float(UserDefaults.standard.string(forKey: "sliderValue")!)!)
}
}else{
return
}
}
and I have a button for stop this animate
#objc func stopButton(sender: UIButton){
self.scrollView.layer.removeAllAnimations()
self.scrollView.layer.layoutIfNeeded()
}
my stop button does not stop my animate function ☹️
You need UIViewPropertyAnimator class.
See this Documentation.
https://developer.apple.com/documentation/uikit/uiviewpropertyanimator
A property animator gives you programmatic control over the timing and execution of the animations. Specifically, you can start, pause, resume, and stop animations.
-Apple Documentation-
Try this:
UIView.setAnimationsEnabled(false)

move to completion instantly if no animations affected

I'm using
UIView.animated(withDuration:animations:completion:) function and there are sometimes that there is no animations affected in the animations block
For example:
Let's assume that I have a view, and it's frame.origin.y is already equals to 0.
Now the animation that I wan't to make is that:
UIView.animate(
withDuration: 1,
animations: {
self.view.frame.origin.y = 0
}
completion: { completed in
guard completed else { return }
// do something
}
)
The completion block called after 1 second instead of instantly.
How can I make that the completion block will called instantly if there are no animations affected in the animations block without any duration.
This is a something that you should handle yourself , the animations won't know that , you can make a compare like
if self.view.frame.origin.y != someValue {
// do animation
}
else {
// run some other code
}
Replace
withDuration: 1,
With
withDuration: 0.01,
(Or even less)

Problems pausing UIViewPropertyAnimation

I am using UIViewPropertyAnimator to make small contentOffset animations.
To keep my view controller lean I have the animation code in the UIScrollView subclass which is to be animated. I originally did my animations in a UIView.animate block, but I noticed that when the view disappears (i.e. another view is pushed on top of the view) the animation jumps to the end, so I am trying to implement a pause in the animation. In addition I wanted to reduce the CPU load through unnecessary animation calls.
var animator: UIViewPropertyAnimator?
...
func showAnimation() {
...
if animator == nil {
animator = UIViewPropertyAnimator(duration: duration, curve: .linear, animations: { [unowned self] in
self.contentOffset = endPoint
})
animator?.addCompletion { [unowned self] (position) in
if position == .end {
self.afterAnimation()
print("completion called")
}
}
}
}
func pauseAnimation() {
if animator?.state == .active {
animator?.pauseAnimation()
}
print("paused animation")
}
The method afterAnimation() just determines if the showAnimation() is to be called again or not.
In my view controller I basically have the following:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
myScrollView.showAnimation()
}
override func viewWillDisappear(_ animated: Bool) {
viewWillDisappear(animated)
myScrollView.pauseAnimation()
}
Now this works well as long as the user does not stay on another screen too long.
For example if the user calls up the settings screen (which gets pushed onto the
navigation stack) the pause method is called - just as expected. If the user stays
in that screen too long, then the animator's completion method is called, even though
the animation is not completed. Once the user dismisses the settings screen again the
animation is frozen and does not continue.
I also tried working with
animator = UIViewPropertyAnimator.runningPropertyAnimator(withDuration: duration, delay: 0, options: .curveLinear, animations: { [unowned self] in
self.contentOffset = endPoint
print("starting animation")
}) { [unowned self] _ in
self.afterAnimation()
print("completion called")
}
but the results were the same. After a while the completion block gets called.
How can I best solve this issue? Thanks!
EDIT: Through different print statements I am slowly having the idea that even when the animator gets paused, the animation's internal counter continues and calls the completion block when the animation should be finished.

UIViewPropertyAnimator issue with Autolayout

Here is the code of what I tried to repeat according to Apple WWDC but with autolayout:
extension AugmentedReallityViewController {
#objc func handlePan(recognizer: UIPanGestureRecognizer) {
// // hide languages and units anyway
// moveUnitView(show: false)
// moveLanguageView(show: false)
//
// let isNowExpanded = settingsPanelState == SettingsPanelState.expanded
// let newState = isNowExpanded ? SettingsPanelState.collapsed : SettingsPanelState.expanded
//
// switch recognizer.state {
// case .began:
// startInteractiveTransition(state: newState, duration: 1)
// isLastPanelUpdateToReachTheNewState = true // just in case, but we should change this property later
// case .changed:
// let translation = recognizer.translation(in: viewSettings)
// let fractionComplete = translation.y / viewSettings.frame.size.height
//
// // we will use this property when interaction ends
// if fractionComplete != 0 { // if it's == 0 , we need to use prev data
// isLastPanelUpdateToReachTheNewState = (newState == SettingsPanelState.expanded && fractionComplete < 0) || (newState == SettingsPanelState.collapsed && fractionComplete > 0)
// }
//
// updateInteractiveTransition(fractionComplete: fractionComplete)
// case .ended:
// continueInteractiveTransition(cancel: !isLastPanelUpdateToReachTheNewState)
// default:
// break
// }
}
#objc func handleSettingsTap() {
// hide languages and units anyway
moveUnitView(show: false)
moveLanguageView(show: false)
let isNowExpanded = settingsPanelState == SettingsPanelState.expanded
let newState = isNowExpanded ? SettingsPanelState.collapsed : SettingsPanelState.expanded
animateOrReverseRunningTransition(state: newState, duration: 10)
}
// perform all animations with animators if not already running
private func animateTransitionIfNeeded(state: SettingsPanelState, duration: TimeInterval) {
if runningAnimators.isEmpty {
// // define constraint for frame animation
// // update constraints
// switch state {
// case .expanded:
// constraint_settingsView_bottom.constant = 0
// case .collapsed:
// constraint_settingsView_bottom.constant = -constraint_height_settingViewWhitePart.constant
// }
// animate that
let frameAnimator = UIViewPropertyAnimator(duration: duration, curve: .linear, animations: { [weak self] in
if let strongSelf = self {
// define constraint for frame animation
// update constraints
switch state {
case .expanded:
strongSelf.constraint_settingsView_bottom.constant = 0
case .collapsed:
strongSelf.constraint_settingsView_bottom.constant = -(strongSelf.constraint_height_settingViewWhitePart.constant)
}
}
self?.view.layoutIfNeeded()
})
frameAnimator.startAnimation()
runningAnimators.append(frameAnimator)
frameAnimator.addCompletion({ [weak self] (position) in
if position == UIViewAnimatingPosition.end { // need to remove this animator from array
if let index = self?.runningAnimators.index(of: frameAnimator) {
print("removed animator because of completion")
self?.runningAnimators.remove(at: index)
// we can change state to a new one
self?.settingsPanelState = state
}
else {
print("animator completion with state = \(position)")
}
}
})
}
}
// starts transition if neccessary or reverses it on tap
private func animateOrReverseRunningTransition(state: SettingsPanelState, duration: TimeInterval) {
if runningAnimators.isEmpty { // start transition from start to end
animateTransitionIfNeeded(state: state, duration: duration)
}
else { // reverse all animators
for animator in runningAnimators {
animator.stopAnimation(true)
animator.isReversed = !animator.isReversed
// test
print("tried to reverse")
}
}
}
// called only on pan .begin
// starts transition if neccessary and pauses (on pan .begin)
private func startInteractiveTransition(state: SettingsPanelState, duration: TimeInterval) {
animateTransitionIfNeeded(state: state, duration: duration)
for animator in runningAnimators {
animator.pauseAnimation()
// save progress of any item
progressWhenInterrupted = animator.fractionComplete
}
}
// scrubs transition on pan .changed
private func updateInteractiveTransition(fractionComplete: CGFloat) {
for animator in runningAnimators {
animator.fractionComplete = fractionComplete + progressWhenInterrupted
}
}
// continue or reverse transition on pan .ended
private func continueInteractiveTransition(cancel: Bool) {
for animator in runningAnimators {
// need to continue or reverse
if !cancel {
let timing = UICubicTimingParameters(animationCurve: .easeOut)
animator.continueAnimation(withTimingParameters: timing, durationFactor: 0)
}
else {
animator.isReversed = true
}
}
}
private func addPanGustureRecognizerToSettings() {
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(AugmentedReallityViewController.handlePan(recognizer:)))
// panGestureRecognizer.cancelsTouchesInView = false
viewSettings.addGestureRecognizer(panGestureRecognizer)
}
private func addTapGestureRecognizerToSettings() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(AugmentedReallityViewController.handleSettingsTap))
tapGestureRecognizer.cancelsTouchesInView = false
viewSettingsTopTriangle.addGestureRecognizer(tapGestureRecognizer)
}
}
Right now I'm just testing tap gestures. And there are 2 main issues:
1) Tap recognizer doesn't work properly during animation. But in apple WWDC they changed frames (not constraints like in my case) and tap recognizers worked perfectly
2) If I change reverse property it changes constraints really very bad. I have extra strips and so on
3) I tried both ways to put changing constraint before animation block and inside. It doesn't really matter, works the same
Any help how to do that with autolayout? Or at least how to do it with frames but my view controller is based on autolayout, so anyway I will have constraints to this bottom view.
When you are using autolayout for animations, you do it as follows:
Make sure autolayout is done:
self.view.layoutIfNeeded()
Then you change the constraints BEFORE the animation block. So for example:
someConstraint.constant = 0
Then after changing the constraint, you tell the autolayout that constraints have been changed:
self.view.setNeedsLayout()
And then you add an animation block with simply calling layoutIfNeeded():
UIView.animate(withDuration: 1, animations: {
self.view.layoutIfNeeded()
})
The same applies when you use UIViewPropertyAnimator - change the constraints in the animation block. E.g.:
self.view.layoutIfNeeded()
someConstraint.constant = 0
self.view.setNeedsLayout()
let animator = UIViewPropertyAnimator(duration: 1, curve: .easeInOut) {
self.view.layoutIfNeeded()
}
animator.startAnimation()
This happens because layoutIfNeeded() does the actual layout - it calculates the frames of the affected views. So if you are setting frames directly, you set them in the animation block. However, Autolayout sets the frames for you - therefore what you need is to tell the autolayout to set them in the animation block (as you would do, if you would set them directly). The layoutIfNeeded() call does exactly that - it tells the autolayout engine to calculate and set the new frames.
About reversal:
While I don't have enough experience to be 100% sure, I would expect that simply setting the animator to reverse would not suffice. Since you apply the constraints before starting the animation, and then you just tell the autolayout to update frames according to the constraints - I would assume that when you reverse the animator, you would also need to reverse also the constraints that are driving the animation.
Animator just animates views into new frames. However, reversed or not, the new constraints still hold regardless of whether you reversed the animator or not. Therefore after the animator finishes, if later autolayout again lays out views, I would expect the views to go into places set by currently active constraints. Simply said: The animator animates frame changes, but not constraints themselves. That means reversing animator reverses frames, but it does not reverse constraints - as soon as autolayout does another layout cycle, they will be again applied.
The important thing to set self.view.layoutIfNeeded() animation to happen
private func animateCard(with topOffset: CGFloat) {
let animator = UIViewPropertyAnimator(duration: 1, curve: .easeOut)
animator.addAnimations {
self.topCardConstraint?.constant = topOffset
self.view.layoutIfNeeded()
}
animator.startAnimation()
}

UIView.animateWithDuration Not Animating Swift (again)

Note: I’ve already checked the following stack overflow issues:
27907570, 32229252, 26118141, 31604300
All I am trying to do is fade animate in a view (by alpha) when called by an IBAction attached to a button. Then reverse when a button on the view is hit.
My wrinkle may be that I'm using a secondary view that is on the ViewDock in the storyboard View. The view is added to the subview at the time of viewDidLoad where the frame/bounds are set to the same as the superview (for a full layover)
The reason this is done as an overlay view since it is a tutorial indicator.
The result (like many others who've listed this problem) is that the view (and contained controls) simply appears instantly and disappears as instantly. No fade.
I have tried animationWithDuration with delay, with and without completion, with transition, and even started with the old UIView.beginAnimations.
Nothing is working. Suggestions warmly welcomed.
The code is about as straight forward as I can make it:
Edit: Expanded the code to everything relevant
Edit2: TL;DR Everything works with the exception of UIViewAnimateWithDuration which seems to ignore the block and duration and just run the code inline as an immediate UI change. Solving this gets the bounty
#IBOutlet var infoDetailView: UIView! // Connected to the view in the SceneDock
override func viewDidLoad() {
super.viewDidLoad()
// Cut other vDL code that isn't relevant
setupInfoView()
}
func setupInfoView() {
infoDetailView.alpha = 0.0
view.addSubview(infoDetailView)
updateInfoViewRect(infoDetailView.superview!.bounds.size)
}
func updateInfoViewRect(size:CGSize) {
let viewRect = CGRect(origin: CGPointZero, size: size)
infoDetailView.frame = viewRect
infoDetailView.bounds = viewRect
infoDetailView.layoutIfNeeded()
infoDetailView.setNeedsDisplay()
}
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
updateInfoViewRect(size)
}
func hideInfoView() {
AFLog.enter(thisClass)
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.0
},
completion:
{ (finished) in
return true
}
)
AFLog.exit(thisClass)
}
func showInfoView() {
AFLog.enter(thisClass)
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.75
},
completion:
{ (finished) in
return true
}
)
AFLog.exit(thisClass)
}
// MARK: - IBActions
#IBAction func openInfoView(sender: UIButton) {
showInfoView()
}
#IBAction func closeInfoView(sender: UIButton) {
hideInfoView()
}
Please note, I started with the following:
func showInfoView() {
UIView.animateWithDuration(2.0, animations: { () -> Void in
self.infoDetailView.alpha = 0.75
})
}
func hideInfoView() {
UIView.animateWithDuration(2.0, animations: { () -> Void in
self.infoDetailView.alpha = 0.00
})
}
If you infoDetailView is under auto layout constraints you need to call layoutIfNeeded on the parent view inside animateWithDuration:
func showInfoView() {
self.view.layoutIfNeeded() // call it also here to finish pending layout operations
UIView.animate(withDuration: 2.0, animations: {
self.infoDetailView.alpha = 0.75
self.view.layoutIfNeeded()
})
}
Theoretically this should not be needed if you just change the .alpha value, but maybe this could be the problem in this case.
There are several strange things I can see,
first, remove:
infoDetailView.layoutIfNeeded()
infoDetailView.setNeedsDisplay()
Usually you don't need to call those methods manually unless you know exactly what you are doing.
Also, when you are changing the size:
infoDetailView.frame = viewRect
infoDetailView.bounds = viewRect
You never need to set both bounds and frame. Just set frame.
Also, you should probably make sure that the view actually doesn't ignore the frame by setting:
infoDetailView.translatesAutoresizingMaskIntoConstraints = true
Instead of resetting the frame, just set autoresize mask:
infoDetailView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
Resulting in:
override func viewDidLoad() {
super.viewDidLoad()
// Cut other vDL code that isn't relevant
setupInfoView()
}
func setupInfoView() {
infoDetailView.alpha = 0.0
infoDetailView.translatesAutoresizingMaskIntoConstraints = true
infoDetailView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
infoDetailView.frame = view.bounds
view.addSubview(infoDetailView)
}
func hideInfoView() {
...
}
I think this should actually help because immediate animations are often connected to size problems.
If the problem persists, you should check whether the infoDetailView in your animation is the same object as the infoDetailView you are adding to the controller.
For others looking to start an animation immediately when a view loads...
The animation won't work if you call UIView.animate(...) inside viewDidLoad. Instead it must be called from the viewDidAppear function.
override func viewDidAppear(_ animated: Bool) {
UIView.animate(withDuration: 3) {
self.otherView.frame.origin.x += 500
}
}
If the animation does not seem to execute then consider examining the state of each of your views, before you enter the animation block. For example, if the alpha is already set to 0.4 then the animation that adjusts your view alpha, will complete almost instantly, with no apparent effect.
Consider using a keyframe animation instead. This is what a shake animation in objective c looks like.
+(CAKeyframeAnimation*)shakeAnimation {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
animation.values = #[[NSValue valueWithCATransform3D:CATransform3DMakeTranslation(-10.0, 0.0, 0.0)],
[NSValue valueWithCATransform3D:CATransform3DMakeTranslation(10.0, 0.0, 0.0)]];
animation.autoreverses = YES;
animation.repeatCount = 2;
animation.duration = 0.07;
return animation;
}
Here is a post that shows you how to adjust alpha with keyframes https://stackoverflow.com/a/18658081/1951992
Make sure infoDetailView's opaque is false.
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/#//apple_ref/occ/instp/UIView/opaque
This property provides a hint to the drawing system as to how it should treat the view. If set to true, the drawing system treats the view as fully opaque, which allows the drawing system to optimize some drawing operations and improve performance. If set to false, the drawing system composites the view normally with other content. The default value of this property is true.
Try Below code. Just play with alpha and duration time to perfect it.
Hide func
func hideInfoView() {
AFLog.enter(thisClass)
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.8
},
completion:
{ (finished) in
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.4
},
completion:
{ (finished) in
self.infoDetailView.alpha = 0.0
}
)
}
)
AFLog.exit(thisClass)
}
Show func
func showInfoView() {
AFLog.enter(thisClass)
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.3
},
completion:
{ (finished) in
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.7
},
completion:
{ (finished) in
self.infoDetailView.alpha = 1.0
}
)
}
)
AFLog.exit(thisClass)
}
I've replicated your code and it work well, it's all ok.
Probably you must control constraints, IBOutlet and IBActions connections. Try to isolate this code into a new project if it's necessary.
Update: my code
and my storyboard and project folder photo:
Every object (view and buttons) are with default settings.
I've commented all AFLog lines (probably it's only any more "verbose mode" to help you) , the rest of your code is ok and it do what do you aspected from it, if you press open button the view fade in, and when you tap close button the view fade out.
PS Not relevant but i'm using xCode 7.3 , a new swift 2.2 project.
Use this code:
Swift 2
UIView.animateWithDuration(0.3, animations: { () -> Void in
self.infoDetailView.alpha = 0.0
})
Swift 3, 4, 5
UIView.animate(withDuration: 0.3, animations: { () -> Void in
self.infoDetailView.alpha = 0.0
})
Have you tried changing your showInfoView() to something more like toggleInfoView?
func toggleInfoView() {
let alpha = CGFloat(infoDetailView.alpha == 0 ? 1 : 0)
infoDetailView.alpha = alpha //this is where the toggle happens
}
It says that if your view's alpha is 0, then change it to 1. Else, make it 0.
If you need that to happen in an animation, try
#IBAction func openInfoView(sender: UIButton) {
UIView.animate(withDuration: 2.0, animations: {
self.toggleInfoView() //fade in/out infoDetailView when animating
})
}
You'll still want to keep that infoDetailView.alpha = 0.0 where you have it, coming from the viewDidLoad.
For UILabel component try to changes layer's background color instead.
Try this (Tested on Swift 4):
UIView.animate(withDuration: 0.2, animations: {
self.dateLabel.layer.backgroundColor = UIColor.red.cgColor;
})
Had a similar issue with animation not being performed.
Changed the function call use perform(aSelector: Selector, with: Any?, afterDelay: TimeInterval) in the form of perform(#selector(functionThatDoesAnimationOfAlphaValue), with: nil, afterDelay: 0) and it worked. Even with a TimeInterval set to 0.
In case someone else comes here wondering for a solution.

Resources