UIViewPropertyAnimator issue with Autolayout - ios

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

Related

Height constraint animation 'jumping'

I've been creating my own UIControl subclass for use in my tweak iDunnoU. I've finished the UIControl, with the exception of the expand/collapse animation. The issue with this animation is that it 'jumps' down/up when expanding/collapsing, instead of fanning out smoothly like my original mockup (see below).
I have uploaded the code to a GitHub repository, found here. The code for adding the control to the superview can be found here, the code for setting up the height constraint can be found here, and the code for animating the height constraint can be found here.
UIView.animate() can be a little tricky -- you need to call .layoutIfNeeded() on the correct view.
Replace your isExpanded / didSet in iDUMenuButton class with this:
var isExpanded = false {
didSet {
if isExpanded != oldValue {
if isExpanded {
becomeFirstResponder()
let haptics = UIImpactFeedbackGenerator(style: .rigid)
haptics.impactOccurred()
}
guard let sv = self.superview else {
// shouldn't happen, but let's be thorough
fatalError("Self must have a superview!!!")
}
// not needed
//self.layoutIfNeeded()
UIView.animate(withDuration: 0.3) {
self.heightConstraint.isActive = !self.isExpanded
// call .layoutIfNeeded() on self's superview
//self.layoutIfNeeded()
sv.layoutIfNeeded()
self.layer.shadowOpacity = self.isExpanded ? 1 : 0
self.buttons.forEach { $0.setBadgeHidden(hidden: !self.isExpanded, animated: true) }
}
delegate?.menuButton(self, isExpandedDidUpdate: isExpanded)
}
}
}

Interactive View Controller Transition - Weird behavior (stuck in the transition)

I'm trying to implement a custom pan gesture to interactively transition to a new view controller. The way it works is that I have a button (labeled "Template Editor", see below) on which you can start a pan to move the current view controller to the right, revealing the new view controller next to it (I've recorded my problem, see below).
Everything is working but there is a bug that I don't understand at all:
Sometimes, when I just swipe over the button (triggering a pan gesture) then lift my finger again (touch down -> fast, short swipe to the right -> touch up) the interactive transition glitches out. It starts to very slowly complete the transition and afterwards, I cannot dismiss the presented view controller, nor can I present anything on that presented view controller.
I have no idea why. Here's my code:
First, the UIViewControllerAnimatedTransitioning class. It's implemented using UIViewPropertyAnimator and just adds the animation using transform:
class MovingTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
enum Direction {
case left, right
}
// MARK: - Properties
// ========== PROPERTIES ==========
private var animator: UIViewImplicitlyAnimating?
var duration = 0.6
var presenting = true
var shouldAnimateInteractively: Bool = false
public var direction: Direction = .left
private var movingMultiplicator: CGFloat {
return direction == .left ? -1 : 1
}
// ====================
// MARK: - Initializers
// ========== INITIALIZERS ==========
// ====================
// MARK: - Overrides
// ========== OVERRIDES ==========
// ====================
// MARK: - Functions
// ========== FUNCTIONS ==========
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let animator = interruptibleAnimator(using: transitionContext)
animator.startAnimation()
}
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
// If the animator already exists, return it (important, see documentation!)
if let animator = self.animator {
return animator
}
// Otherwise, create the animator
let containerView = transitionContext.containerView
let fromView = transitionContext.view(forKey: .from)!
let toView = transitionContext.view(forKey: .to)!
if presenting {
toView.frame = containerView.frame
toView.transform = CGAffineTransform(translationX: movingMultiplicator * toView.frame.width, y: 0)
} else {
toView.frame = containerView.frame
toView.transform = CGAffineTransform(translationX: -movingMultiplicator * toView.frame.width, y: 0)
}
containerView.addSubview(toView)
let animator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.9, animations: nil)
animator.addAnimations {
if self.presenting {
toView.transform = .identity
fromView.transform = CGAffineTransform(translationX: -self.movingMultiplicator * toView.frame.width, y: 0)
} else {
toView.transform = .identity
fromView.transform = CGAffineTransform(translationX: self.movingMultiplicator * toView.frame.width, y: 0)
}
}
animator.addCompletion { (position) in
// Important to set frame above (device rotation will otherwise mess things up)
toView.transform = .identity
fromView.transform = .identity
if !transitionContext.transitionWasCancelled {
self.shouldAnimateInteractively = false
}
self.animator = nil
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
self.animator = animator
return animator
}
// ====================
}
Here's the part that adds the interactivity. It's a method that's being called by a UIPanGestureRecognizer I added to the button.
public lazy var transitionAnimator: MovingTransitionAnimator = MovingTransitionAnimator()
public lazy var interactionController = UIPercentDrivenInteractiveTransition()
...
#objc private func handlePan(pan: UIPanGestureRecognizer) {
let translation = pan.translation(in: utilityView)
var progress = (translation.x / utilityView.frame.width)
progress = CGFloat(fminf(fmaxf(Float(progress), 0.0), 1.0))
switch pan.state {
case .began:
// This is a flag that helps me distinguish between when a user taps on the button and when he starts a pan
transitionAnimator.shouldAnimateInteractively = true
// Just a dummy view controller that's dismissing as soon as its been presented (the problem occurs with every view controller I use here)
let vc = UIViewController()
vc.view.backgroundColor = .red
vc.transitioningDelegate = self
present(vc, animated: true, completion: {
self.transitionAnimator.shouldAnimateInteractively = false
vc.dismiss(animated: true, completion: nil)
})
case .changed:
interactionController.update(progress)
case .cancelled:
interactionController.cancel()
case .ended:
if progress > 0.55 || pan.velocity(in: utilityView).x > 600
interactionController.completionSpeed = 0.8
interactionController.finish()
} else {
interactionController.completionSpeed = 0.8
interactionController.cancel()
}
default:
break
}
}
I also implemented all the necessary delegate methods:
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transitionAnimator.presenting = true
return transitionAnimator
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transitionAnimator.presenting = false
return transitionAnimator
}
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
guard let animator = animator as? MovingTransitionAnimator, animator.shouldAnimateInteractively else { return nil }
return interactionController
}
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
guard let animator = animator as? MovingTransitionAnimator, animator.shouldAnimateInteractively else { return nil }
return interactionController
}
That's it. There's no more logic behind it (I think; if you need more information, please tell me), but it still has this bug. Here's a recording of the bug. You can't really see my touch but all I'm doing is touching down -> fast, shortly swiping to the right -> touching up. And after this really slow transition has finished, I can't dismiss the red view controller. It's stuck there:
Here's what's even stranger:
Neither interactionController.finish() nor interactionController.cancel() is being called when this occurs (at least not from within my handlePan(_:)method).
I checked the view hierarchy in Xcode after this bug occurred and I got this:
First, it's seemingly stuck in the transition (everything is still inside UITransitionView).
Second, on the left hand side you see the views of the first view controller(the one I start the transition from). However, on the image there only is the red view controller visible, the one that was about to be presented.
Do you have any idea what's going on? I've been trying to figure this out for the past 3 hours but I can't get get it to work properly. I'd appreciate any help
Thank you!
EDIT
Okay, I found a way to reproduce it 100% of the time. I also created an isolated project demonstrating the problem (it's a little differently structured because I tried many things but the result is still exactly the same)
Here's the project: https://github.com/d3mueller/InteractiveTransitionDemo2
How to reproduce the problem:
Swipe from right to left and then quickly from left to right. This will trigger the bug.
Also, a similar bug will appear, when you swipe from right to left very fast multiple times. Then it will actually run the transition and finish it correctly (but it shouldn't even start because moving from right to left keeps the progress at 0.0)
You might try setting:
/// Set this to NO in order to start an interruptible transition non
/// interactively. By default this is YES, which is consistent with the behavior
/// before 10.0.
#property (nonatomic) BOOL wantsInteractiveStart NS_AVAILABLE_IOS(10_0);
to NO on your interactionController
Good luck and curious to hear if you figure it out.

Pan Gesture completion handler getting called immediately

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.

Swift UIView appearing in different places

Im trying to make a small game. And there is some problem with the animation. Im new to Swift. So lets take a look. I create a UIImageView picture and want to do animation of this picture appear in a different places on a screen. I believe that the algorithm will look like this:
Infinite loop{
1-GetRandomPlace
2-change opacity from 0 to 1 and back(with smooth transition)
}
Looks simple, but I can't understand how to do it correctly in Xcode.
Here is my test code but it looks useless
Thank you for help and sorry if there was already this question, I can't find it.
class ViewController: UIViewController {
#IBOutlet weak var BackgroundMainMenu:UIImageView!
#IBOutlet weak var AnimationinMenu: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// MoveBackgroundObject(AnimationinMenu)
// AnimationBackgroundDots(AnimationinMenu, delay: 0.0)
// self.AnimationinMenu.alpha = 0
//
// UIImageView.animateWithDuration(3.0,
// delay: 0.0,
// options: UIViewAnimationOptions([.Repeat, .CurveEaseInOut]),
// animations: {
// self.MoveBackgroundObject(self.AnimationinMenu)
// self.AnimationinMenu.alpha = 1
// self.AnimationinMenu.alpha = 0
// },
// completion: nil)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillAppear(animated: Bool) {
AnimationBackgroundDots(AnimationinMenu, delay: 0.0)
}
func ChangeOpacityto1(element: UIImageView){
element.alpha = 0
UIView.animateWithDuration(3.0) {
element.alpha = 1
}
}
func ChangeOpacityto0(element: UIImageView){
element.alpha = 1
UIView.animateWithDuration(3.0){
element.alpha = 0
}
}
func AnimationBackgroundDots(element: UIImageView, delay: Double){
element.alpha = 0
var z = 0
while (z<4){
MoveBackgroundObject(AnimationinMenu)
UIImageView.animateWithDuration(3.0,
animations: {
element.alpha = 0
element.alpha = 1
element.alpha = 0
},
completion: nil)
z++
}
}
func MoveBackgroundObject(element: UIImageView) {
// Find the button's width and height
let elementWidth = element.frame.width
let elementHeight = element.frame.height
// Find the width and height of the enclosing view
let viewWidth = BackgroundMainMenu.superview!.bounds.width
let viewHeight = BackgroundMainMenu.superview!.bounds.height
// Compute width and height of the area to contain the button's center
let xwidth = viewWidth - elementWidth
let yheight = viewHeight - elementHeight
// Generate a random x and y offset
let xoffset = CGFloat(arc4random_uniform(UInt32(xwidth)))
let yoffset = CGFloat(arc4random_uniform(UInt32(yheight)))
// Offset the button's center by the random offsets.
element.center.x = xoffset + elementWidth / 2
element.center.y = yoffset + elementHeight / 2
}
}
The problem is in AnimationBackgroundDots.
You are immediately creating 4 animations on the same view but only one can run at a time. What you need to do is wait until one animation is finished (fade in or fade out) before starting a new one.
Also, the animations closure is for setting the state you want your view to animate to. It looks at how your view is at the start, runs animations, then looks at the view again and figures out how to animate between the two. In your case, the alpha of the UIImageView starts at 0, then when animations runs, the alpha ends up being 0 so nothing would animate. You can't create all the steps an animation should take that way.
Want you need to do it move your view and start the fade in animation. The completion closure of fading in should start the fade out animation. The completion closure of the fading out should then start the process all over again. It could look something like this.
func AnimationBackgroundDots(element: UIImageView, times: Int) {
guard times > 0 else {
return
}
MoveBackgroundObject(element)
element.alpha = 1
// Fade in
UIView.animateWithDuration(3.0, animations: {
element.alpha = 1
}, completion: { finished in
// Fade Out
UIView.animateWithDuration(3.0, animations: {
element.alpha = 0
}, completion: { finished in
// Start over again
self.AnimationBackgroundDots(element, times: times-1)
})
})
}
You called also look at using keyframe animations but this case is simple enough that theres no benefit using them.
Also as a side note. The function naming convention in swift is to start with a lowercase letter, so AnimationBackgroundDots should be animationBackgroundDots.

UIPercentDrivenInteractiveTransition doesn't get to animation's completion on fast gesture

I have created an interactive transition. My func animateTransition(transitionContext: UIViewControllerContextTransitioning) is quite normal, I get the container UIView, I add the two UIViewControllers and then I do the animation changes in a UIView.animateWithDuration(duration, animations, completion).
I add a UIScreenEdgePanGestureRecognizer to my from UIViewController. It works well except when I do a very quick pan.
In that last scenario, the app is not responsive, still on the same UIViewController (the transition seems not to have worked) but the background tasks run. When I run the Debug View Hierarchy, I see the new UIViewController instead of the previous one, and the previous one (at least its UIView) stands where it is supposed to stand at the end of the transition.
I did some print out and check points and from that I can say that when the problem occurs, the animation's completion (the one in my animateTransition method) is not reached, so I cannot call the transitionContext.completeTransition method to complete or not the transition.
I could see as well that the pan goes sometimes from UIGestureRecognizerState.Began straight to UIGestureRecognizerState.Ended without going through UIGestureRecognizerState.Changed.
When it goes through UIGestureRecognizerState.Changed, both the translation and the velocity stay the same for every UIGestureRecognizerState.Changed states.
EDIT :
Here is the code:
animateTransition method
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
self.transitionContext = transitionContext
let containerView = transitionContext.containerView()
let screens: (from: UIViewController, to: UIViewController) = (transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!, transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!)
let parentViewController = presenting ? screens.from : screens.to
let childViewController = presenting ? screens.to : screens.from
let parentView = parentViewController.view
let childView = childViewController.view
// positionning the "to" viewController's view for the animation
if presenting {
offStageChildViewController(childView)
}
containerView.addSubview(parentView)
containerView.addSubview(childView)
let duration = transitionDuration(transitionContext)
UIView.animateWithDuration(duration, animations: {
if self.presenting {
self.onStageViewController(childView)
self.offStageParentViewController(parentView)
} else {
self.onStageViewController(parentView)
self.offStageChildViewController(childView)
}}, completion: { finished in
if transitionContext.transitionWasCancelled() {
transitionContext.completeTransition(false)
} else {
transitionContext.completeTransition(true)
}
})
}
Gesture and gesture handler:
weak var fromViewController: UIViewController! {
didSet {
let screenEdgePanRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: "presentingViewController:")
screenEdgePanRecognizer.edges = edge
fromViewController.view.addGestureRecognizer(screenEdgePanRecognizer)
}
}
func presentingViewController(pan: UIPanGestureRecognizer) {
let percentage = getPercentage(pan)
switch pan.state {
case UIGestureRecognizerState.Began:
interactive = true
presentViewController(pan)
case UIGestureRecognizerState.Changed:
updateInteractiveTransition(percentage)
case UIGestureRecognizerState.Ended:
interactive = false
if finishPresenting(pan, percentage: percentage) {
finishInteractiveTransition()
} else {
cancelInteractiveTransition()
}
default:
break
}
}
Any idea what might happen?
EDIT 2:
Here are the undisclosed methods:
override func getPercentage(pan: UIPanGestureRecognizer) -> CGFloat {
let translation = pan.translationInView(pan.view!)
return abs(translation.x / pan.view!.bounds.width)
}
override func onStageViewController(view: UIView) {
view.transform = CGAffineTransformIdentity
}
override func offStageParentViewController(view: UIView) {
view.transform = CGAffineTransformMakeTranslation(-view.bounds.width / 2, 0)
}
override func offStageChildViewController(view: UIView) {
view.transform = CGAffineTransformMakeTranslation(view.bounds.width, 0)
}
override func presentViewController(pan: UIPanGestureRecognizer) {
let location = pan.locationInView((fromViewController as! MainViewController).tableView)
let indexPath = (fromViewController as! MainViewController).tableView.indexPathForRowAtPoint(location)
if indexPath == nil {
pan.state = .Failed
return
}
fromViewController.performSegueWithIdentifier("chartSegue", sender: pan)
}
I remove the "over" adding lines => didn't fix it
I added updateInteractiveTransition in .Began, in .Ended, in both => didn't fix it
I turned on shouldRasterize on the layer of the view of my toViewController and let it on all the time => didn't fix it
But the question is why, when doing a fast interactive gesture, is it not responding quickly enough
It actually works with a fast interactive gesture as long as I leave my finger long enough. For example, if I pan very fast on more than (let say) 1cm, it's ok. It's not ok if I pan very fast on a small surface (let say again) less than 1cm
Possible candidates include the views being animated are too complicated (or have complicated effects like shading)
I thought about a complicated view as well but I don't think my view is really complicated. There are a bunch of buttons and labels, a custom UIControl acting as a segmented segment, a chart (that is loaded once the controller appeared) and a xib is loaded inside the viewController.
Ok I just created a project with the MINIMUM classes and objects in order to trigger the problem. So to trigger it, you just do a fast and brief swipe from the right to the left.
What I noticed is that it works pretty easily the first time but if you drag the view controller normally the first time, then it get much harder to trigger it (even impossible?). While in my full project, it doesn't really matter.
When I was diagnosing this problem, I noticed that the gesture's change and ended state events were taking place before animateTransition even ran. So the animation was canceled/finished before it even started!
I tried using GCD animation synchronization queue to ensure that the updating of the UIPercentDrivenInterativeTransition doesn't happen until after `animate:
private let animationSynchronizationQueue = dispatch_queue_create("com.domain.app.animationsynchronization", DISPATCH_QUEUE_SERIAL)
I then had a utility method to use this queue:
func dispatchToMainFromSynchronizationQueue(block: dispatch_block_t) {
dispatch_async(animationSynchronizationQueue) {
dispatch_sync(dispatch_get_main_queue(), block)
}
}
And then my gesture handler made sure that changes and ended states were routed through that queue:
func handlePan(gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .Began:
dispatch_suspend(animationSynchronizationQueue)
fromViewController.performSegueWithIdentifier("segueID", sender: gesture)
case .Changed:
dispatchToMainFromSynchronizationQueue() {
self.updateInteractiveTransition(percentage)
}
case .Ended:
dispatchToMainFromSynchronizationQueue() {
if isOkToFinish {
self.finishInteractiveTransition()
} else {
self.cancelInteractiveTransition()
}
}
default:
break
}
}
So, I have the gesture recognizer's .Began state suspend that queue, and I have the animation controller resume that queue in animationTransition (ensuring that the queue starts again only after that method runs before the gesture proceeds to try to update the UIPercentDrivenInteractiveTransition object.
Have the same issue, tried to use serialQueue.suspend()/resume(), does not work.
This issue is because when pan gesture is too fast, end state is earlier than animateTransition starts, then context.completeTransition can not get run, the whole animation is messed up.
My solution is forcing to run context.completeTransition when this situation happened.
For example, I have two classes:
class SwipeInteractor: UIPercentDrivenInteractiveTransition {
var interactionInProgress = false
...
}
class AnimationController: UIViewControllerAnimatedTransitioning {
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
if !swipeInteractor.interactionInProgress {
DispatchQueue.main.asyncAfter(deadline: .now()+transitionDuration) {
if context.transitionWasCancelled {
toView?.removeFromSuperview()
} else {
fromView?.removeFromSuperview()
}
context.completeTransition(!context.transitionWasCancelled)
}
}
...
}
...
}
interactionInProgress is set to true when gesture began, set to false when gesture ends.
I had a similar problem, but with programmatic animation triggers not triggering the animation completion block. My solution was like Sam's, except instead of dispatching after a small delay, manually call finish on the UIPercentDrivenInteractiveTransition instance.
class SwipeInteractor: UIPercentDrivenInteractiveTransition {
var interactionInProgress = false
...
}
class AnimationController: UIViewControllerAnimatedTransitioning {
private var swipeInteractor: SwipeInteractor
..
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
...
if !swipeInteractor.interactionInProgress {
swipeInteractor.finish()
}
...
UIView.animateWithDuration(...)
}
}

Resources