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.
Related
In my app I have a camera and another view controller that I present when the photo is taken. It seems that when I move the camera too fast when I take the photo, the animations stop working (even the segues back to the original UITabBar, that precedes the camera). I am not sure if this is related to CPU usage and there not seems to be a pattern here. Sometimes, only taking pictures make it behave this way. My animations are mainly this:
UIView.animateWithDuration(0.8, delay: 0, options: UIViewAnimationOptions.CurveEaseOut, animations: { () -> Void in
//buttons.alpha = 1 or 0
//buttonA.center.x += 100
}, completion: nil)
in ViewDidLoad there are only a few alpha change values inside animation. Most of the other animations blocks are when buttons are clicked. The most interesting thing though is that if I dismiss the view controller and come back to the camera, sometimes the behavior persists and sometimes not, like as if there is no pattern at all. When the behavior persists, it affects even the segues out of the camera view controller itself ( and the other parts of the tabBar that are the main view of the app - like in Instagram, for instance). Thus, it affects the whole app. What do you guys think would be the cause of this odd behavior?
Objective-C code is welcome as well.
You shouldn't call animate code in the ViewDidLoad: portion of the view controller lifecycle. At this point the view has finished loading but it may or may not have appeared on screen yet. There's viewWillAppear: which you can insert your animation in and at that point, when the view has appeared, it will perform the animation.
Did you try viewDidApear? I'm not sure that the view has actually been added to the view hierarchy in either viewDidLoad or viewWillAppear. The only downside is you can't assume it will only be called once in the viewController's life cycle.
I am wondering what is the best practice to keep some of the UI elements in place when going forward/backward between UIViewController for example if I am using UINavigationController.
To be specific. I am making an app that has several similar view controllers (they can be instances of one main view controller). Then user clicks the next button and goes to the next page; or swipe back to go to the previous page. I have a progress bar on top and one or more buttons on bottom that I wish to keep static in place while the rest of the content are changing with an animation (a simple push might work).
Now my question is, if is it better put the content inside a container view? or to implement custom transition to keep those items in place while moving the rest?
Here is an image of the concept:
In the navigation controller delegate you can implement navigationController(_ navigationController:, animationControllerFor:, from:, to:) to return a custom UIViewControllerAnimatedTransitioning object. If you go this route you will have to implement the whole animation yourself though.
If you want to keep the basic animations from UINavigationController and just keep your elements steady you can go another route. In your view controller implement viewWillAppear: and/or viewWillDisappear:. In there you can get the transitionCoordinator and call animate(alongsideTransition:, completion:) on that. With that you can run your custom animations in parallel to the system provided animations.
To keep fixed elements you add another copy of the fixed elements to the container view you can get from the context object that is passed to your block. In the completion block you then can remove it again.
Sounds complicated, but it actually is rather easy if you look at the code:
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
let fixedViewCopy = UIView(...)
fixedViewCopy.frame = self.fixedView.frame
transitionCoordinator?.animate(
alongsideTransition: { context in
context.containerView.addSubview(fixedViewCopy)
},
completion: { _ in
fixedViewCopy.removeFromSuperview()
}
)
}
I'm trying to implement the following behavior:
Long press on a collection view brings a full-window view (call it LetterView) to the front
Subsequent gestures/touches are only processed by the LetterView.
(edit: I should mention that I want a transparency effect of seeing the collectionview items beneath LetterView)
I seem to be running into behavior that everyone else is trying to implement, though - my touches get processed by both the LetterView and the collection view. I.e. I can scroll the collection view AND have hits processed by my topmost view. Showing the view hierarchy in XCode clearly shows LetterView at the front, and both the UICollectionView and the LetterView are subviews of UICollectionWrapperView.
LetterView is a UIView subclass with a UIViewController subclass. It's added to the view hierarchy programmatically, inside my UICollectionViewController subclasses's viewDidLoad method, like so:
super.viewDidLoad()
letterDrawingViewController = LetterDrawingViewController()
let viewFrame : CGRect = self.collectionView!.frame
letterDrawingViewController.view = LetterDrawingView.init(frame:viewFrame)
letterDrawingView = letterDrawingViewController.view
self.addChildViewController(letterDrawingViewController)
letterDrawingViewController.didMoveToParentViewController(self)
collectionView?.addSubview(letterDrawingView)
It doesn't appear to be a first responder issue, as I tried overriding canBecomeFirstResponder in LetterView and assigning it first responder status when I move it to the front
I tried setting userInteractionEnabled=FALSE on the CollectionView, but keeping it true on the LetterView after I moved LetterView to the front. This disabled all touch events for both views
I tried setting exclusiveTouch=True for LetterView when I moved it to the front. This didn't appear to do anything.
Aside from any specific tips, are there any general techniques for debugging hit-testing like this? According to the docs on hit-testing in iOS, iOS should prefer the "deepest" subview that returns yes for hitTest:withEvent:, which, since LetterView is a subview of collectionview, and in front of all it's cells, should be the front? Is there any logging I can enable to see a hit test over the view hierarchy in action?
Thank you!
Nate.
If letterView is full screen, you probably don't want to add it as a subview of the collection view like you are. Maybe try adding it to the application's window instead and see how that does. At least in that instance it should intercept all the touch events.
Another method, although admittedly a more fragile feeling one, would be to enable and disable user interaction on the collectionView as you present and dismiss letterView.
So, when letterView is about to be presented, you can call
self.collectionView.userInteractionEnabled = NO;
and if you also know when that view is about to be dismissed you can call
self.collectionView.userInteractionEnabled = YES;
The only thing here to worry about is that you don't get into a bad state where your letterView is not presenting and your collectionView is also ignoring a user's touch. That will feel totally broken.
Whilst I think you can deal with your issue somewhat easily I think you are making a design mistake. It feels like you are trying to code this thinking like a web developper by adding a child view to your view and trying to intercept the touches there like one would do in a modern JavaScript single page app. In iOS I think this is bad design. You should segue or present the new viewController using the methods provided by apple.
So your code should look soothing like:
letterDrawingViewController = LetterDrawingViewController()
self.presentViewController(letterDrawingViewController, animated: true, completion: nil)
iOS8 has the added benefit of allowing you to have awesome custom transitions. Take a look at this : http://www.appcoda.com/custom-segue-animations/
I'm trying to load a UIView and then right away detect touches on that new view. Currently, overriding touchesBegan gives a delay of around a second.
So, I load up the UIView and immediately keep tapping on the screen. It takes around a second for touchesBegan to be called. From that point on, all is well. However, I can't afford the ~second wait initially.
I have stripped back all the code in the UIView to just the barebones incase anything was clogging up the main thread, but the delay still persists.
How can I go about getting immediate feedback of touches from a newly presented UIView? Thanks.
-- EDIT BELOW --
I've been playing around with this for the past few hours. Even when creating a custom UIWindow and overriding sendEvent, the UITouchPhase gets halted when the new view is displayed. To begin receiving events again I have to take my finger off the screen and place it back on the screen. (I don't want to have to do this).
The problem seems to lie with the segue to the new view controller. When it segues, the touch phase is ended. If I simply add a subview to the current view controller, I see the desired functionality (i.e. instant responding to touch).
Given my newly presented view contains a lot of logic, I wanted to wrap it all up in it's own view controller rather than add it to the presenter view controller. Is there a way for me to do this and use 'addSubview` to present it? This should hopefully achieve the desired effect.
In the end, I created a custom view controller with it's own xib. Where I would have segued, I now instantiate that custom view controller and append it's view. This has eliminated the touch lag.
Have you disabled multi-touch? There's an inherent delay while the controller waits to see if there's a follow up touch (on all single touches). The initial sluggishness might be from loading up the multi-touch code and deciding what to do about it.
myViewController.view.multipleTouchEnabled=NO;
As to your final question, look into view controller containment. Since iOS 5 Apple has provided the hooks officially and safely to present one view controller as a sub view of another.
Sadly I've no insight as to the greater issue.
I found an answer that worked for me from a similar question asked here:
iOS: Why touchesBegan has some delay in some specific area in UIView
This solution isn't check-marked on that thread, so I'll copy it here to make it easier to find.
override func viewDidAppear(animated: Bool) {
let window = view.window!
let gr0 = window.gestureRecognizers![0] as UIGestureRecognizer
let gr1 = window.gestureRecognizers![1] as UIGestureRecognizer
gr0.delaysTouchesBegan = false
gr1.delaysTouchesBegan = false
}
I have this custom back button in the bottom left corner of my app. The button transitions out downwards when there is no more view controllers on the navigation stack then just the root one. Now i introduced the "swipe to pop" functionality that is standard with iOS7 and i would like to make the button transition based on how far the swipe gesture has moved the view it's popping.
I have gotten so far as to add a my own navigation controller as a target to the the interactivePopGestureRecognizer to receive the the swipe actions. So I know when the swipe starts, moves (velocity, direction and point) and when it stops. So i could make the position of the back button dependent on where the finger are on the screen without a problem.
The problem is that when the user lifts her fingers from the screen the view either pops or return to its original position. The only way i found to detect this is in the UINavigationControllerDelegate method
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated this method is called when the view that the app is returning to is fully visible (the animation has finished popping). So the way to know if the view did not pop is to wait some time after touches ended and se if the above method is called, this is of course not a good solution.
The absolute best situation would be if i could get notified about the position of the animation and the completion/reversal of the animation. Another thing to mention is that approximating will be hard i think since the decision to pop or not is not only made by the screen location of the finger upon release but also velocity etc.
Any tips would be greatly appreciated.
I did manage to solve this kind of anyway. Managing to at least get notified if animation completes or not. I posted my answer in another question similar to this, here hope it helps.