UIPageViewController prevent hiding keyboard on scroll - ios

I have a UIPageViewController with multiple UIViewController, each one containing a UITextField.
Whenever I slide the UIPageViewController, the keyboard automatically dismisses. Is there a way to prevent this behavior?
Invoking becomeFirstResponder on the new UITextfield but the animation the won't fix the problem.

You can try to embed the PageViewController as a ChildViewController of an other viewController.
I believe this way the navigation in the PageViewController will not effect the keyboard.
I am not sure if that is needed, but if it is still not working you can set your ParentViewController as the firstResponder when transition occurs.

unfortunately this seems to come from the animation of the transition showing as done, before it is actually done,
the workarounds I can think of are
1. made the animating false
2. set textFieldShouldEndEditing in the next VC to return NO or handle it with a bool
3. add a delay in the animation, or in the next VC viewWillAppear

When using setViewControllers on UIPageViewController (setting a controller without the scroll), it seems that the completion block is called just before the scrollView has reached its final position. When it does, it dismisses the first responder.
The solution we found was to first, grab the scroll view:
// In UIPageViewController subclass
for subview in self.view.subviews {
if let scrollV = subview as? UIScrollView {
scrollV.delegate = self
self.scrollView = scrollV // Optional, we don't really need it
}
}
The scroll view position is done when its offset is the x position of the middle controller. That is, the offset will equal the view's length.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.x == self.view.width {
// Transition is 'really' done.
}
}
At that point, you can send a notification that transition is completed.
What we did, is hold a completion block and call it when the transition is done.
Create a function, so that a controller can pass a block in:
func transitionCompleted(completed:(()->())?) {
self.transitionCompletedBlock = completed
}
When transition is completed:
self.transitionCompletedBlock?()
The controller with the keyboard will look like:
pagerController.transitionCompleted(completed: {
self.textfield.becomeFirstResponder()
})

Related

Add subView immediately after view presented

I have 2 view A and B.
At view A
presentViewController(viewB, animation: true) {
NSNotificationCenter.defautCenter.postNotificationName("addButton")
}
At view B:
var masterView:UIView!
func addButton(notification: NSNotification){
var button:UIButton!
button.frame = masterView.caculator // set frame for button
self.addSubview(button)
}
When view B finished animation on simulator( go from Bottom). Button added after delay 0.3s.
Following some document, when animation finish, method viewDidAppear called. i tried this way. But as I saw, button added after animation finish 0.3s
How to addButton immediately when animation finish?
Thank you!
I have solved it. Because in viewDidAppear, this method was called when all Layout is loaded.
So we need implement add my button in LayoutSubview Method.

Two UIViewControllers inside UIScrollView - resigning first responder

In the app I am working on I have two view controllers inside a scroll view. The second view controller (VC2) contains a text view. You can see the setup on the image below:
!
When I scroll from VC2 to VC1, the keyboard persists and covers the content of VC1. I managed to solve the problem by making the scroll view the first responder on scrollViewDidScroll event. This works, but it results in the keyboard disappearing even on a partial scroll, which can be annoying to the users. I can solve this problem by also checking the content offset but it strikes me as overcomplicated and not elegant at all. Is there a better way to do this?
Edit:
As Chonch and latenitecoder suggested, I detected the page change. I adapted the code from: Detecting UIScrollView page change to swift. Here it is:
var previousPage = 0
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let pageWidth = scrollView.frame.size.width
let fractionalPage = scrollView.contentOffset.x / pageWidth
let page = Int(round(fractionalPage))
if (previousPage != page) {
// Page has changed, do your thing!
self.becomeFirstResponder()
// Finally, update previous page
previousPage = page
}
}
You can set the UIScrollView's pagingEnabled property to YES and only call resignFirstResponder for the UITextView when the paging ended and the resulting page is VC1. As long as the current page remains VC2, you don't call resignFirstResponder, and the keyboard remains shown.
However, notice that it may actually be a good idea to hide the keyboard as soon as the user starts scrolling (as you're describing is your current state). Maybe you should leave it like this and when the paging ends, check if the current page is VC2, and if it is, call becomeFirstResponder on the UITextView in order to display the keyboard again.
I'd suggested to use UIPageViewController to horizontal scrolling controllers. Then you can do the same action in it's delegate method
- (void)pageViewController: didFinishAnimating: previousViewControllers: transitionCompleted: but it will save you memory a lot.

UIPageViewController crashes when holding down a button while it's animating

I have UIPageViewController that animates programatically. The problem is that the view controllers inside it has UIButtons inside them. When I hold down a button and wait until the UIPageViewController animates, the app crashes with the error:
'Failed to determine navigation direction for scroll'
What I think I need to do is to somehow fake that the user releases the button before the UIPageviewController animates.
However, [self.button sendActionsForControlEvents:UIControlEventTouchCancel]; doesn't seem to do the trick. Neither do UIControlEventTouchUpInside.
Is there a better way do to it or am I using sendActionsForControlEvents wrong?
All sendActionsForControlEvents: does is call any methods you've assigned to the control events passed in for the button. It doesn't call any internal methods to programmatically lift up touches or anything like that.
Right before you programmatically animate your page view controller, try using this method to effectively cancel any touches on the pan gesture recognizer of the page view controller's internal scroll view:
- (void)cancelPanGestureTouchesOfPageViewController:(UIPageViewController *)pageVC
{
// Since UIPageViewController doesn't provide any API to access its scroll view,
// we have to find it ourselves by manually looping through its view's subviews.
for (UIScrollView *scrollView in pageVC.view.subviews) {
if ([scrollView isKindOfClass:[UIScrollView class]]) {
// We've found the scroll view, so use this little trick to
// effectively cancel any touches on its pan gesture recognizer
BOOL enabled = scrollView.panGestureRecognizer.enabled;
scrollView.panGestureRecognizer.enabled = !enabled;
scrollView.panGestureRecognizer.enabled = enabled;
}
}
}
(Note that this is messing with the internal view hierarchy of UIPageViewController, so this method is kind of ugly and may break in the future. I generally don't recommend doing stuff like this, but I think in this instance it should be okay.)

Combine UIPageViewController swipes with iOS 7 UINavigationController back-swipe gesture

I have a navigation controller that pushes a view-controller (PARENT) that contains an UIPageViewController (PAGES). Now I used pan/swipe gestures to switch between the children of the page-view-controller. However, I can no longer pop the PARENT-view controller using the swipe gesture from the left border of the screen, because it is interpreted as a gesture in PAGES.
Is it possible to accomplish that swipe-to-pop when the left-most view-controller is shown?
Two ideas:
Return nil in pageViewController:viewControllerBeforeViewController -> doesn't work.
Restrict the touch area, as described here.
Or is there a more straightforward way?
I had the same situation as #smallwisdom, but handled it differently.
I have a view controller A that I push on top of my navigation controller's stack.
This view controller A contains a horizontal scroll view that stretches all the way from left side of the screen to the right.
In this scenario, when I wanted to swipe the screen to pop view controller A from navigation controller's stack, all I ended up doing was scrolling this horizontal scroll view.
The solution is pretty simple.
Inside my view controller A, I have code like this:
_contentScrollView = [[UIScrollView alloc] init];
[self.view addSubview:_contentScrollView];
for (UIGestureRecognizer *gestureRecognizer in _contentScrollView.gestureRecognizers) {
[gestureRecognizer requireGestureRecognizerToFail:self.navigationController.interactivePopGestureRecognizer];
}
It works great. What this does?
It is telling the scrollView's gesture recognizers that they have to wait to see if some other gesture recognizer will recognize the current gesture.
If that other fails to recognize, then they will no longer have to wait and they can try to recognize the current gesture.
If that other recognizer succeeds and recognizes the current gesture, then all of the gesture recognizers that have been waiting will automatically fail.
This other gesture recognizer they have to wait is set to be the navigation controller's interactivePopGestureRecognizer. He is in charge for the swipe-to-go-back gestures.
I mostly agree with #ancajic's answer. I would like to provide an extra-case when you set UIPageViewController's transitionStyle to 'Scroll', in which you'll not get gestureRecognizers to be set, the workaround is:
if (self.navigationController?.interactivePopGestureRecognizer != nil)
{
for view in self.pageViewController!.view.subviews
{
if let scrollView = view as? UIScrollView
{
scrollView.panGestureRecognizer.requireGestureRecognizerToFail(self.navigationController!.interactivePopGestureRecognizer!);
}
}
}
I had a similar issue in one of my projects and used the following method. In my case, it was one of those left-side menus that were really popular before iOS 7.
My solution was to set the UINavigationControllerDelegate and then implemented the following:
- (void)navigationController:(UINavigationController *)navigationController
didShowViewController:(UIViewController *)viewController
animated:(BOOL)animated {
// enable the interactive menu gesture only if at root, otherwise enable the pop gesture
BOOL isRoot = (navigationController.viewControllers.firstObject == viewController);
self.panGestureRecognizer.enabled = isRoot;
navigationController.interactivePopGestureRecognizer.enabled = !self.panGestureRecognizer.enabled;
}
EDIT:
Additionally, you need a hook into the UIPageViewController's gesture recognizers. (They aren't returned by the gestureRecognizers property for a scroll view style page view controller.) It's annoying, but the only way I've found to access this is to iterate through the scrollview's gesture recognizers and look for the pan gesture. Then set a pointer to it and enable/disable based on whether or not you are currently displaying the left-most view controller.
If you want to keep the right swipe enabled, then replace the pan gesture with a subclassed pan gesture recognizer of your own that can conditionally recognize based on the direction of the pan gesture.
First, find UIPageViewController's scrollView
extension UIPageViewController {
var bk_scrollView: UIScrollView? {
if let v = view as? UIScrollView {
return v
}
// view.subviews 只有一个元素,其类型是 _UIQueuingScrollView,是 UIScrollView 的子类
// view.subviews have only one item of which the type is _UIQueuingScrollView, which is kind of UIScrollView's subclass.
for v in view.subviews where v is UIScrollView {
return v as? UIScrollView
}
return nil
}
}
Second, set gestureRecognizer's dependence.
override func viewDidLoad() {
super.viewDidLoad()
if let ges = navigationController?.interactivePopGestureRecognizer {
pageViewController.bk_scrollView?.panGestureRecognizer.require(toFail: ges)
}
}
Swift version of #Lcsky's answer:
if let interactivePopGesture = self.navigationController?.interactivePopGestureRecognizer, let pageViewController = self.swipeVC?.pageViewController {
let subView = pageViewController.view.subviews
for view in subView {
if let scrollView = view as? UIScrollView{
scrollView.panGestureRecognizer.require(toFail: interactivePopGesture)
}
}
}

touches methods not getting called on UIView placed inside a UIScrollView

I have a Custom Scroll View, subclassing UIScrollView. I have added a scroll view in my viewcontroller nib file and changed its class to CustomScrollView. Now, this custom scroll view (made from xib) is added as a subview on self.view.
In this scroll view, I have 3 text fields and 1 UIImageView(named signImageView) added from xib. On clicking UIImageView (added a TapGestureRecogniser), a UIView named signView is added on the custom scroll view. I want to allow User to sign on this view, So I have created a class Signature.m and .h, subclassing UIView and implemented the touches methods (touchesBegan, touchesMoved and touchesEnded) and initialised the signView as follows:
signView = [[Signature alloc]initWithFrame:signImageView.frame];
[customScrollView addSubview:signView];
But when I start signing on the signView, the view gets scrolled and hence the touches methods don't get called.
I have tried adding signView on self.view instead of custom scroll view, but in that case the view remains glued to a fixed position when I start scrolling. (Its frame remains fixed in this case)
Try setting canCancelContentTouches of the scrollView to NO and delaysContentTouches to YES.
EDIT:
I see that similiar question was answered here Drag & sweep with Cocoa on iPhone (the answer is exactly the same).
If the user tap-n-holds the signView (for about 0.3-0.5 seconds) then view's touchesBegan: method gets fired and all events from that moment on go to the signView until touchesEnded: is called.
If user quickly swipes trough the signView then UIScrollView takes over.
Since you already have UIView subclassed with touchesBegan: method implemented maybe you could somehow indicate to user that your app is prepared for him to sign ('green light' equivalent).
You could also use touchesEnded: to turn off this green light.
It might be better if you add signImageView as as subView of signView (instead of to customScrollView) and hide it when touchesBegan: is fired). You would add signView to customScrollview at the same place where you add signImageView in existing code instead.
With this you achieve that there is effectively only one subView on that place (for better touch-passing efficiency. And you could achieve that green light effect by un-hiding signImageView in touchesBegan:/touchesEnded:
If this app-behaviour (0.3-0.5s delay) is unacceptable then you'd also need to subclass UIScrollView. There Vignesh's method of overriding UIScrollView's touchesShouldBegin: could come to the rescue. There you could possibly detect if the touch accoured in signView and pass it to that view immediately.
When ever you add a scrollview in your view hierarchy it swallows all touches.Hence you are not getting the touches began. So to get the touches in your signon view you will have to pass the touches to signon view. This is how you do it.
We achieved this with a UIScrollView subclass that disables the pan gesture recogniser for a list of views that you provide.
class PanGestureSelectiveScrollView: UIScrollView {
var disablePanOnViews: [UIView]?
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let disablePanOnViews = disablePanOnViews else {
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
let touchPoint = gestureRecognizer.location(in: self)
let isTouchingAnyDisablingView = disablePanOnViews.first { $0.frame.contains(touchPoint) } != nil
if gestureRecognizer === panGestureRecognizer && isTouchingAnyDisablingView {
return false
}
return true
}
}

Resources