I've been working to implement a custom modal transition that uses a UIPresentationController subclass to create and manipulate an additional view during the presentation and dismissal. Apple helpfully provides an example of how to do this in the documentation, but I've hit a snag.
When presenting the modal, my custom view animations work perfectly, but when I dismiss the modal, the animations applied to the custom views in dismissalTransitionWillBegin play out of sync with the animations specified by the transition animator object I'm returning from animationControllerForDismissedController:. Specifically, the custom view's animations are ignoring the duration of the transition animation and are always playing very quickly (the duration appears to be around 0.2 seconds).
What could cause animateAlongsideTransition:completion: to ignore the duration of the base animation?
The source of the trouble appears to be a bug in iOS.
No matter how I refactored or simplified my animation code, I always ended up with the same result, so I started to wonder if there might be something in the way my project was set up that was causing the problem. I dropped my custom modal transition code into a clean project and, lo and behold, it worked perfectly on the first try.
Bit by bit I customized my test app to more closely match my real app and I was eventually able to get the problem to reappear. Through trial and error I found the combination of factors that was triggering the problem:
The presenting view controller is within a UINavigationController
The presenting view controller's bar button items include an image-based UIBarButtonItem
The window has a tint color set
When those three conditions are met, the animation block of the animateAlongsideTransition: call in dismissalTransitionWillBegin will be performed before the animation block of the animateWithDuration: call in animateTransition. This seems to prevent the custom view's animations from getting the transition animation's duration. In my testing, the animateAlongsideTransition: animations ran with a duration of 0.215 seconds, which I believe is the default duration.
I have been unable to find any way prevent the issue from occurring other than removing one of the three factors triggering it. The workaround I ultimately settled on was removing the window's tint color and instead setting a global tint color using UIView's appearance proxy. There are some side-effects--like UIAlertViews' buttons getting tinted--, but for my purposes this was an acceptable trade-off.
Related
I am trying to implement swipe up/down to dismiss view controller and everything is working great. If I start swiping up, it will finish the animation in the upwards direction and vice versa.
The problem appears when the user first swipes up but then decides to swipe the view controller down - how do I change the value in the animation to dismiss view controller to the bottom rather than to the top (set by the user's first swipe).
In my UIViewControllerAnimatedTransitioning controller I define the animation like this:
func animateTransition(transitionContext:UIViewControllerContextTransitioning) {
UIView.animateWithDuration(duration, animations: {
guard let transitionDelegate = self.transitionDelegate else {return}
snapshot.frame.origin.y = transitionDelegate.shouldAnimateUp ? -snapshot.frame.height : snapshot.frame.height
TransitionDelegate points to UIPercentDrivenInteractiveTransition controller (where the swipe gesture is defined). ShouldAnimateUp is defined in UIPanGestureRecognizer function like this:
shouldAnimateUp = translatedView.center.y < translatedView.frame.height / 2
That is if the view is in upper half, shouldAnimateUp = true and the other way around.
But unfortunately, when I call finishInteractiveTransition() func it uses the value which was initially set in UIView.animateWithDuration in UIViewControllerAnimatedTransitioning controller when dismissViewControllerAnimated(true, completion: nil) was called.
So, is there any way to change values for animation in UIViewControllerAnimatedTransitioning controller after dismissViewControllerAnimated(true, completion: nil) is called?
PS: I kind of struggled to define my problem in words (words are hard 🙄 ), so please tell me if you need additional info or if I should try to rewrite my explanation. Also, an additional hint: I would like the animation to work like image dismissal works in official Twitter app.
OK, I see what they're doing. It's actually more complicated that "if you swipe down and then up, it animates scene off upward". If you swipe down and back up, the Twitter app will cancel if you don't get much past where you started, but will go up if you pass where you started by some considerable portion (and are still swiping in that upward).
I must confess that I'm not crazy about this UX, because two very similar gestures can result in very different behavior. If you swipe down, keep your finger down, and then flick back upward, there is a seemingly arbitrary nature as to whether it's interpreted as a cancelation of the downward swipe or as an upward swipe. I personally think that if the user initiates a demonstrable downward gesture, that dragging back up should merely be a cancelation of the transition. But that's not the question here.
Anyway, if this is what you want to do, there are a couple of ways to achieve it:
You could consider using view property animator-based animations, which handle mid-flight changes to the animation more elegantly than older animation techniques. So, you theoretically could just addAnimations to your UIViewPropertyAnimator. But it seems messy to me.
See Advances in UIKit Animations and Transitions for more information about view property animators and how to use them with interruptible custom transitions.
When dismissing, you might not use UIViewControllerAnimatedTransitioning at all. Just animate the dismissal yourself.
For example, when you present, the UIPresentationController subclass can keep the presenting view (by returning false from should​Remove​Presenters​View). Then, when the gesture recognizer starts, it might not initiate a custom, interactive custom transition with dismiss at all, but instead merely adjust the frame of the presented scene (and modify the opacity of the dimming chrome). Then, at the end of the gesture, complete the animation manually and then the completion block can dismiss the presented view controller with no animation at all (because you've animated it yourself).
IMHO, this is architecturally inelegant. A view controller has no business mucking about with chrome that should be owned by the presentation controller. But, it works.
We should recognize that they might not have "presented" the full screen image at all. For example, they could have done simple view controller containment, but just added the child view controller scene to the existing scene. Then, the gesture could do whatever frame and dimming layer opacity changes it wanted, and when its done, just do the final animation and in the completion block, just remove the child view controller (e.g. willMove and removeFromParentViewController).
If I were to do this, I'd probably lean towards option 3, though I'd wager that it might not feel very satisfying for you, having invested time in custom interactive transitions already. Regardless, these are a couple of approaches you could consider.
I have a situation in my app where a navigation controller pushViewController:animated:YES is triggered by a user. The user can also trigger a popViewController:animated:YES by tapping another control. The intent is that the popViewController is the undo or inverse of the pushViewController.
However, if the user triggers the popViewController:animated:YES while the pushViewController animation is still happening, I get a message logged to the console:
2014-08-22 08:26:36.601 MyApp[22380:60b] nested pop animation can result in corrupted navigation bar
2014-08-22 08:26:36.960 MyApp[22380:60b] Finishing up a navigation transition in an unexpected state. Navigation Bar subview tree might get corrupted.
And indeed, the navigation bar does get corrupted: the back button is no longer visible. I have to go do something else then come back to the affected page in my app to get it working properly again.
What can I do to avoid this race condition? My first thought was protect the popViewController call with a check to see if a current navigation animation is already occurring, and waiting for that to finish (or even canceling the popViewController call completely). However, I haven't been able to find a way of detecting that an action is already occurring.
There are two solutions to this problem. I suggest you to implement first one.
1 . Avoid user interaction on multiple controls at a time by setting exclusive touch to them.
Set exclusiveTouch property to YES for those controls if they shares superView(parent view) else you will have to set this property YES to their parent views.
2 . Implement UINavigationControllerDelegate protocol in that view controller where user is tapping multiple controls at a time.
– navigationController:willShowViewController:animated:
– navigationController:didShowViewController:animated:
Set a flag when first delegate method gets called & reset it in second. Use this flag on every push/pop operation.
I have a UIView animation such as :
[UIView animateWithDuration:0.5 animations:^{
//properties to animate
}];
Is it possible to have a gesture connected to this animation so you can have an interactive transition?
For example, I have a square, when I touch it, it transitions to double it's size. However, when I pinch it, I like it to become interactively bigger or smaller depending on the pinch scale. When a user let's go and the square is 150% larger it will finish the transition else it will cancel the transition and animates back to it's previous state. Hope this makes sense.
I think percent-driven interactive transition is only enabled for view controller transitions right now. That's not to say that the technology is not there, it obviously is (see here, namely startInteractiveTransition:containerViews:animation:), but Apple has chosen not to expose this method for the time being. Right now, it is called only when performing view controller transitions (push/pop and presentation).
There is an update since iOS 10, everything is now possible with UIViewPropertyAnimator: you can make the animation interactive or automatic, or even change the parameters on the fly.
I'm using CABasicAnimation and CATransaction to animate some layers in my custom UIView.
However, when after that returning to the rest of my app using a navigation controller's back button, the navigation controller does not animate anymore. Not even when then going to any other view.
I am using an iPad with iOS5.
I was able to workaround this by calling
[UIView setAnimationsEnabled:YES];
in my viewDidDissapear and viewWillDisappear methods.
Really strange since I am never disabling view animations anywhere in my code.
How do I make a curled-up view update live as the user interacts with view being presented with presentModalViewController: under it?
The behaviour I want:
User taps a view settings button.
User taps controls in the view settings screen. Rather than dismissing view settings, the view automatically updates in the background.
User taps something to dismiss view settings.
Imagine if in Maps tapping Map, Satellite, Hybrid didn't uncurl automatically but just updated the display.
I'm able to get notification that something's changed from the settings controller back to the main view controller. The main view controller can update and signal that it should redraw parts of itself, but actually updating the screen is intermittent. It will work 5 times in a row, then not work a couple times, then work another 5 times in a row. Dismissing the modal view always catches up the view underneath, however, so I assume the rendered image of my view is sometimes being cached or not being redrawn despite my request. But I can't think of a way to verify this.
This happens on both the device and the simulator.
While there might be multiple root causes of this behavior, here's a common issue I've seen that causes 'delayed' or 'intermittent' updates to UIKit views.
If the statements that update your presenting view (or the presented view) are running in a dispatch queue other than the main queue, UIKit may produce inconsistent results and timing in the actual UI update. You can usually tell by putting a breakpoint on the statements that update the UI and examining the name of the 'queue' displayed in Xcode's left-side debugger view. It should be 'com.apple.main-thread'. If it's not, that could be your issue. This can happen in quite a few delegate methods such as the network APIs.
Try wrapping your UI updates in:
dispatch_async(dispatch_get_main_queue(), ^() { ... }); and see if that helps! You should only do this when needed and take care to use block-safe techniques as always.
I tested this in a brand new Universal app for iOS 7.0.3 using the iPad simulator with a view controller presented using the partial curl transition. I was able to replicate the original issue, including the intermittent update and the 'snap' update when dismissing the presented view by using a background queue in the code I provided above. Once I switched to the main queue, everything worked A-OK.
Let me know if this helps or if there was some other issue :)