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.
Related
This question already has an answer here:
Swift 4 Odd unexpected jump back to earlier storyboard
(1 answer)
Closed 4 years ago.
Every time I tap somewhere along the top part of a view controller, it would return to the previous view. Root Controller is not affected as it doesn't have anything to return too. All of my View Controllers are affected though. (Please read update 3)
Things I've tried:
Looked for unnecessary gesture recognizer. (Result: I received nil when I check for the list gestures.)
I figured I might have accidentally added or left a IBOutlet to the view or something. (Result: There was no unnecessary IBOutlets.)
I disabled user interaction from a view to see if it would be affected. (Result: The bug seemed to go away until I enabled user interaction again.)
I've tried to do some research but I wasn't getting the results I wanted.
Tapping above the green line will make the View Controller return to the previous Controller.
I'm still relatively new to Objective-C and Xcode so I don't know if I'm making a rookie mistake or not.
UPDATE (Please read update 3)
Still nothing but here are a few more things i've tried:
Logging every user action.
Created a new project with new view controllers.
Opened a previous project to test it's view controllers.
Uninstalled and reinstalled Xcode.
All attempts still came with that weird bug.
UPDATE 2(Please read update 3)
I've decided to put a band-aid over the problem. Since the bug doesn't work when clicking over a button, I put constraints on a empty button, put it on top of the background image and removed the effect when button is tapped.
I thought this would work but there is a few spots on the screen that activates the bug. I experimented and figured that certain parts of ui elements activated the bug. (Example: Some labels in a Stackview, top part of a textfield, a switch, top part of a UICollectionView, etc.)
I've disabled user interaction for certain ui elements. Unfortunately not all ui elements could be disabled because some require user interaction.
Also I figure out that tapping with three fingers on the screen somehow activates the bug.
Here is an error code I received in the console when I was trying multiple taps on the screen:
<_UISystemGestureGateGestureRecognizer: 0x2822fc1e0>: Gesture: Failed to receive system gesture state notification before next touch
UPDATE 3
After playing around with the code again. I believe I've figured out what is causing the problem but not how to fix it.
I believe it has to do with the story board segue, or at least the transitions being used. If I set the kind to "Show(e.g. Push)" than it works fine, but if I set the kind to "Present Modally" and the transition to "Partial Curl", the bug does it's thing.
Sidenote: This is how I tested it. I created a new project (I don't think the language matters). The project has two view controllers; one button each with segue pointing at each view controllers. Segue 1 has it's kind as "Present Modally" and the transition as "Partial curl". Segue 2 is on the 2nd view controller and has it's kind as "Show(e.g. Push)". To test the bug, tap along the top part of the screen. If the bug worked, then it should show the partial curl transition but in reverse and send the user back to the 1st view controller.
maybe you are experiencing the standard swipe back gesture coming from the navigation controller. It thats the case try this code:
Objective-C:
if ([self.navigationController respondsToSelector:#selector(interactivePopGestureRecognizer)]) {
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}
Swift:
navigationController?.interactivePopGestureRecognizer?.isEnabled = false
I have noted recently that each of my app view has one particular bug/behaviour: if user taps too fast on a UI component when the view appears, the tap is simply ignored. If the user wait a bit before tapping, the tap works.
Storyboard is used for the storyboard, tap gesture recognisers are on UIImageview and using IOS 10.2.
Through different forums i have read about the following options:
nest the call of "present view controllers" in main thread
Call CFRunLoopWakeup before presentviewcontroller
Add programatically the TapGesturerecognizer
change the states of "delays touches end" and "delays touches began"
disable 3d touch option as similar symptoms was reported to happen in other apps
All above have been unsuccessful.
Anyone would have met similar issues with the first tap just after the view load?
[Update: I realise this misbehaviour is not specific to this app. Two tests to try:
create an xCode Project for iPhone and two view controllers Controller A and Controller B. Two buttons : a button on Controller A view to go to Controller B view and a button Back in Controller B view to go back to Controller A view. Tap to go from View A to B, tap back in B and try immediately to tap on button to go to B. First tap doesn't work either.
Go in Settings of the iPhone. Tap On Notifications. Press On Settings to go back to Main Settings screen, Tap immediately back on Notifications. If fast enough, first tap doesn't work. Second tap works or waiting a bit before first tap.
Question is now: this looks like a common problem across iPhone apps. Would you know if there would be a common setting somewhere? or is this a common bug for given IOS version ?
]
Stephane
This is a general problem (the moment when the view controller is changed, the first tap will be ignored), but this is not a bug that will happen only because the switch view controller's animation has not yet been completed. If you set the animation to false , then the view controller can immediately respond to your click, no matter how fast (anyway faster than your hand) :)
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'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 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.