TableView and PKRevealController gesture conflicts - How to really solve them? - ios

I am trying to implement a view with sliding side menus, such as with PKRevealController in iOS 6.1. A simple demo of this issue with source code on github is here, however you might not need to grab it if you already understand gestureRecognizer delegate implementation.
The problem I see is that two gestures that my users will want to use are going to be mutually confused for each other. The UITableView in the center (main screen) of the application should be able to use the swipe-right gesture to delete, but I still want a swipe that occurs across the top navigation area to result in exposing the side menus.I also intend to show other things than just the Table view, and at runtime I plan to swap out the main view with a different view, whenever a user selects a button on one of the side menus. This is kind of like a "hidden side tray UITabBarController" that I'm going for, but I want the side bars to be revealed only when the main "front view" controller is NOT a UITableView or its subviews.
Right now, using the demo sources that comes with PKRevealController, and adding deletion support to the main view's UITableView, no slide gesture to delete a row is possible. (You have to add one table view method to enable deletion support in the UITable view, which I did add.)
This was asked here, but the answer stated is incomplete, and as seen below, does not work for me and I have no idea why, because it appears that this delegate method is not invoked at any time where I return a YES, and yet it goes ahead anyways and begins a gesture.
Update The answer in the previous question is also wrong, as compared to the WIKI/FAQ answer I placed below.
I have only modified the class PKRevealController.m by adding this:
- (BOOL) gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
UIView *view1 = otherGestureRecognizer.view;
UIView *view2;
if (view1) {
view2 = view1.superview;
};
if ([gestureRecognizer.view isKindOfClass:[UITableView class]])
{
return NO;
}
// Co-operate by not stealing gestures from UITableView.
if ([view1 isKindOfClass:[UITableView class]]) {
return NO;
}else if ([view1 isKindOfClass:[UITableViewCell class]]) {
return NO;
// UITableViewCellContentView
}
else if (view2 && [view2 isKindOfClass:[UITableViewCell class]]) {
return NO;
// UITableViewCellContentView
}
else
{
return YES; // NEVER GETS HIT. BREAKPOINT HERE!
}
}
What confuses me is that at no point does the return YES code above get hit (I have a breakpoint on it) and yet, the Gesture controller is still stealing the gesture.
Note: I have made an evil hack, but I thought that I could prevent this cleanly. Here is my evil hack:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
if (gestureRecognizer == self.revealPanGestureRecognizer)
{
CGPoint translation = [self.revealPanGestureRecognizer translationInView:self.frontViewContainer];
BOOL begin = (fabs(translation.x) >= fabs(translation.y));
// BEGIN EVIL HACK
if (_topLimitY > 0) {
CGPoint location = [gestureRecognizer locationInView:gestureRecognizer.view];
if (location.y>_topLimitY) // _topLimitY = 55 for instance.
begin = NO;
}
// END EVIL HACK.
return begin;
}
else if (gestureRecognizer == self.revealResetTapGestureRecognizer)
{
return ([self isLeftViewVisible] || [self isRightViewVisible]);
}
return YES;
}
Right now in my evil hacked demo, I have set the topLimitY property (that I added to PKRevealController's properties) to 55, which allows me to swipe on the nav bar area of the front view, but not on the table view which takes up the rest of the demo.
Note that I plan to have multiple main views, and only want to defeat the gesture recognition on the whole main area if the view is a UITableView or some sub-view thereof. That is why I call my hack above a hack. Because I thought you could tell the gesture recognizer to go away and not bother you, and yet it doesn't work, it doesn't even invoke the shouldRecognize method, it just goes ahead and does the next thing in its list of things to do.

I should really read the WIKI first shouldn't I?
This is a FAQ, it says so right here:
When instantiating the controller pass this option in your options dictionary:
NSDictionary *options = #{
PKRevealControllerRecognizesPanningOnFrontViewKey : #NO
};
This will disable pan-based reveal for the entire front view. Now, you can use the revealPanGestureRecognizer and add it to any view you desire to be panned on that doesn't interfere with your table view, to enable gesture based reveal.
I'd advise (if working with a table based environment with swipe'able cells) you, to add the revealPanGestureRecognizer to your front view controller's navigation bar (which it most likely has):
[self.navigationController.navigationBar addGestureRecognizer:self.revealController.revealPanGestureRecognizer];
And voilĂ  - panning doesn't interfere with your table view anymore.
more info at:
https://github.com/pkluz/PKRevealController/issues/76
Thank you Wiki. If only I had read it all first.
The above completely answers my question and was already there on the wiki. I'm answering my own question because it seems Google always comes to Stackoverflow first, and that might help other confused developers in the future.
Update If the above thing blows up when you try it, it's probably being done too early. Here's a slightly more robust version of the above fix:
// Additional gesture recognition linkups. The underscore variables here
// are implementation-section ivars in my app-delegate, that I have already
// checked are valid and initialized, and this is the last thing in my app delegate
// didFinishLaunch... method, before the return YES:
UIGestureRecognizer *rec = _revealController.revealPanGestureRecognizer;
if (rec) {
[_frontViewNavController.navigationBar addGestureRecognizer:rec];
}

Use This :
self.revealController.frontViewController.revealController.recognizesPanningOnFrontView = YES;

Related

Should superview's gesture cancel subview's gesture in iOS 7?

For several years now I've operated under the assumption that if a superview and its subview both have gesture recognizers, the subview would receive the touches first and cancel out the superview's gesture. Until iOS 7 this assumption never failed me, allowing me to add gestures to subviews confident the superview's gestures wouldn't interfere. But in iOS 7, the superview will randomly receive the touches first and cancel out the subview's gestures. This happens somewhat rarely, which made the problem hard to spot.
I first experienced this problem as buttons that couldn't be tapped using UITapGestureRecognizer... again, very rarely. Usually the buttons would work until they didn't. Made you kind of question your sanity. So I rolled my own TapGestureRecognizer and discovered that superview taps were canceling their subview's taps on occasion. It's never done this in any previous version of iOS, but I'm wondering if this behavior was simply never promised.
I thought the subview's gesture was supposed to cancel it's superview's gesture (unless otherwise specified by a delegate). Is this wrong or is this a bug?
Please Note: I'm not asking how to handle the situation. I'm asking if anyone knows whether my assumption is incorrect. I'm already rearranging views, dynamically adding/removing gestures and creating rather complex implementations of gestureRecognizer:shouldReceiveTouch: to remedy the situation. It's not fun, but I can work around the problem.
After a lot of searching around I found a discussion on Apple's message boards with other users that are having this problem: Issues with UITapGestureRecognizer (developer account required). I went ahead and submitted a bug report: 15331126 (does anyone know how to link to bug reports anymore?). In the meantime, I implemented this workaround. So far it seems to be working, but since the bug is so rare I may simply not have triggered it yet. I'm releasing it to my beta users and if I get no complaints from them (who have been complaining) I'll assume this fixes the issue.
UPDATE:
This solution has fixed the problem. After weeks of use by dozens of users I haven't had a single issue with gestures.
Most of my gestures are custom. I altered them to be delegates of themselves and implemented:
- (BOOL) gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
if (gestureRecognizer == self){
if ([otherGestureRecognizer isMemberOfClass:self.class]){
if ([self isGestureRecognizerInSuperviewHierarchy:otherGestureRecognizer]){
return YES;
} else if ([self isGestureRecognizerInSiblings:otherGestureRecognizer]){
return YES;
}
}
}
return NO;
}
Note that my custom gestureRecognizers implement the UIGestureRecognizerDelegate protocol now (publically, for reasons you'll see below). I also added a couple of categories to UIGestureRecognizer (used in the above code):
- (BOOL) isGestureRecognizerInSiblings:(UIGestureRecognizer *)recognizer{
UIView *superview = self.view.superview;
NSUInteger index = [superview.subviews indexOfObject:self.view];
if (index != NSNotFound){
for (int i = 0; i < index; i++){
UIView *sibling = superview.subviews[i];
for (UIGestureRecognizer *viewRecognizer in sibling.gestureRecognizers){
if (recognizer == viewRecognizer){
return YES;
}
}
}
}
return NO;
}
- (BOOL) isGestureRecognizerInSuperviewHierarchy:(UIGestureRecognizer *)recognizer{
if (!recognizer) return NO;
if (!self.view) return NO;
//Check siblings
UIView *superview = self.view;
while (YES) {
superview = superview.superview;
if (!superview) return NO;
for (UIGestureRecognizer *viewRecognizer in superview.gestureRecognizers){
if (recognizer == viewRecognizer){
return YES;
}
}
}
}
I'm not entirely sure that I need to check for siblings as I've only seen the issue occur with superview gestures. However, I didn't want to take that chance. Note that I only check for siblings "below" the current one as I don't want to cancel view gestures "above" the current view.
I had to add implementations for for those classes that set themselves as delegates of the custom recognizers, but they pretty much just call back to the gestureRecognizer:
- (BOOL) gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
if ([gestureRecognizer respondsToSelector:#selector(gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer:)]){
return [(id <UIGestureRecognizerDelegate>)gestureRecognizer gestureRecognizer:gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:otherGestureRecognizer];
}
return NO;
}
Hope this helps anyone else having the problem.

Cross Directional UIScrollViews - Can I Modify the Scrolling Behaviour?

Here's how the scroll views work: One scroll view is paging enabled in the horizontal direction. Each 'page' of this scroll view contains a vertically scrolling UITableView. Without modification, this works OK, but not perfectly.
The behaviour that's not right: When the user scrolls up and down on the table view, but then wants to flick over to the next page quickly, the horizontal flick/swipe will not work initially - it will not work until the table view is stationary (even if the swipe is very clearly horizontal).
How it should work: If the swipe is clearly horizontal, I'd like the page to change even if the table view is still scrolling/bouncing, as this is what the user will expect too.
How can I change this behaviour - what's the easiest or best way?
NOTE For various reasons, a UIPageViewController as stated in some answers will not work. How can I do this with cross directional UIScrollViews (/one is a table view, but you get the idea)? I've been banging my head against a wall for hours - if you think you can do this then I'll more than happily award a bounty.
According to my understanding of the question, it is only while the tableView is scrolling we want to change the default behaviour. All the other behaviour will be the same.
SubClass UITableView. UITableViews are subClass of UIScrollViews. On the UITableView subClass implement one UIScrollView's UIGestureRecognizer's delegate method
- (BOOL)gestureRecognizer:(UIPanGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UISwipeGestureRecognizer *)otherGestureRecognizer
{
//Edit 1
//return self.isDecelerating;
//return self.isDecelerating | self.bounces; //If we want to simultaneous gesture on bounce and scrolling
//Edit 2
return self.isDecelerating || self.contentOffset.y < 0 || self.contentOffset.y > MAX(0, self.contentSize.height - self.bounds.size.height); // #Jordan edited - we don't need to always enable simultaneous gesture for bounce enabled tableViews
}
As we only want to change the default gesture behaviour while the tableView is decelerating.
Now change all 'UITableView's class to your newly created tableViewSubClass and run the project, swipe should work while tableView is scrolling. :]
But the swipe looks a little too sensitive while tableView is scrolling. Let's make the swipe a little restrictive.
SubClass UIScrollView. On the UIScrollView subclass implement another UIGestureRecognizer's delegate method gestureRecognizerShouldBegin:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
CGPoint velocity = [(UIPanGestureRecognizer *)gestureRecognizer velocityInView:self];
if (abs(velocity.y) * 2 < abs(velocity.x)) {
return YES;
}
}
return NO;
}
We want to make the "swipe is clearly horizontal". Above code only permits gesture begin if the gesture velocity on x axis is double than on y axis. [Feel free to increase the hard coded value "2" if your like. The higher the value the swipe needs to be more horizontal.]
Now change the `UiScrollView' class (which has multiple TableViews) to your ScrollViewSubClass. Run the project. :]
I've made a project on gitHub https://github.com/rishi420/SwipeWhileScroll
Although apple doesn't like this method too much:
Important: You should not embed UIWebView or UITableView objects in UIScrollView objects. If you do so, unexpected behavior can result
because touch events for the two objects can be mixed up and wrongly
handled.
I've found a great way to accomplish this.
This is a complete solution for the problem. In order to scroll the UIScrollView while your UITableView is scrolling you'll need to disable the interaction you have it.
- (void)viewDidLoad
{
[super viewDidLoad];
_myScrollView.contentSize = CGSizeMake(2000, 0);
data = [[NSMutableArray alloc]init];
for(int i=0;i<30;i++)
{
[data addObject:[NSString stringWithFormat:#"%d",i]];
}
UITapGestureRecognizer * tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTap:)];
[self.view addGestureRecognizer:tap];
}
- (void)handleTap:(UITapGestureRecognizer *)recognizer
{
[_myTableView setContentOffset:_myTableView.contentOffset animated:NO];
}
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
{
scrollView.userInteractionEnabled = NO;
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
scrollView.userInteractionEnabled = YES;
}
To sum up the code above, if the UITableView is scrolling, set userInteractionEnabled to NO so the UIScrollView will detect the swipe. If the UITableView is scrolling and the user taps on the screen, userInteractionEnabled will be set to YES.
Instead of using UIScrollView as a container for these multiple table views, try using a UIPageViewController.
You can even integrate this into your existing view controller setup as a child view controller (directly replacing the UIScrollView).
In addition, you'll likely want to implement the required methods from UIPageViewControllerDataSource and possibly one or more of the methods from UIPageViewControllerDelegate.
Did you try the methods : directionalLockEnabled of both your table and scroll and set them up to horizontal for one and vertical for the other ?
Edit :
1)
What you want to do is very complicate since the touch wait some time (like 0.1s) to know what your movement will be. And if your table is moving, it will take your touch immediately whatever it is (because it's suppose to be reactive movement on it).
I don't see any other solution for you but to override touch movement from scratch to detect immediately the kind of mouvement you want (like if the movement will be horizontal) but it will be more than hard to do it good.
2)
Another solution I can advise you is to make your table have left and right margin, where you can touch the parent scroll (pages thing so) and then even if your table is scrolling, if you touch here, only your paging scroll will be touched. It's simpler, but could not fit with your design maybe...
Use UIPageViewController and in the -viewDidLoad method (or any other method what best suits your needs or design) get UIPageViewController's UIScrollView subview and assign a delegate to it. Keep in mind that, its delegate property won't be nil. So optionally, you can assign it to another reference, and then assign your object, which conforms to UIScrollViewDelegate, to it. For example:
id<UIScrollViewDelegate> originalPageScrollViewDelegate = ((UIScrollView *)[pageViewController.view.subviews objectAtIndex:0]).delegate;
[((UIScrollView *)[pageViewController.view.subviews objectAtIndex:0]) setDelegate:self];
So that you can implement UIScrollViewDelegate methods with ease. And your UIPageViewController will call your delegate's -scrollViewDidScroll: method.
By the way, you may be obliged to keep original delegate, and respond to delegate methods with that object. You can see an example implementation in ViewPagerController class on my UI control project here
I faced the same thing recently. My UIScrollview was on paging mode and every page contained a UITableView and like you described it worked but not as you'd expected it to work. This is how solved it.
First I disabled the scrolling of the UIScrollview
Then I added a UISwipeGestureRecognizer to the actual UITableView for left and right swipes.
The action for those swipes were:
[scroll setContentOffset:CGPointMake(currentPointX + 320, PointY) animated:YES];
//Or
[scroll setContentOffset:CGPointMake(currentPointX - 320 , PointY) animated:YES];
This works flawlessly, the only down side is that if the user drags his finger on the UITableVIew that will be considered as a swipe. He won't be able to see half of screen A and half of screen B on the same screen.
You could subclass your scroll view and your table views, and add this gesture recognizer delegate method to each of them...
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:
(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
I can't be sure this is exactly what you are after, but it may come close.

iOS Slide up menu

I am attempting to create a menu system similar to the target app for iPhone. Here is what the menu looks like:
(source: iclarified.com)
I have been searching the internet and have not found a lot of information. To the best of my knowledge, I will have to create a UIView that is the menu, offset the view and detect when it is touched to slide up. These buttons in the menu view will have to load other views just like a tab bar controller would while keeping the menu tab to slide up at the bottom. Slide menu up, tap button for where you want to go, load that view while sliding the button back down.
The animations seem straight forward, its making it function like the tab bar controller and always stay in view and also how to detect the sliding of the menu (maybe something with UIScrollView)?
Any help would be great, no idea where to start.
Steps up to 5 contain the creation of the subview itself, but if you are only interested on the dragging events read from step five, which is about the panGestureRecognizer.
Create a custom UIView, I have also created a delegate protocol to inform the viewController when an action occurs on the subview. Depending on how much complicated your subview will be, you may want to create a xib file with the same name for that. Let's call it filterView from now on.
Create two methods like "hide" and "show" on the filterView. If you do not want to have a button (namely the small button with the arrow, I ll call it dropdownButton) to close and open the filterView, you may skip this step, but I strongly recommend to implement them. What you have to do is to animate filterView.frame.origin.y to
[[UIScreen mainScreen] bounds].size.height - dropDownButton.frame.height to hide the filterView
[[UIScreen mainScreen] bounds].size.height - filterView.frame.size.height to show it.
If requested I can also send code for that animations.
3.
Open the xib file for the UIViewController and add a UIView, so that only its dropdown button will be visible. Click the view and change its class to filterView using the rightmost pane in the interface builder. At this step you should be able to see your filtersView's tip on the bottom of the page if you did everything correctly. If not, the filterView.xib file is probably not correctly connected to filterView source code files.
4
.
Import the FilterView in the ViewController and connect as IBOutlet & synthetize it. Implement the delegate protocol if you have written one. If you have buttons on the filterView, you will need it later on. The delegate protocol should include optional messages like
-(void) featuresButtonTappedOnFilterView: (FilterView *) filterView;
and in the implementation you should do what you have to do in the viewController, like opening another viewController.
5
. On the viewControllers xib file create a panGestureRecognizer and add it to your filterView. PanGestureRecognizer is a gestureRecognizer which will handle the dragging events. At first the whole filterView will be able to dragged around by clicking any point on it, we will get to that later. Connect the gestureRecognizer as IBOutlet and create an IBAction (preferably with a better name) like:
-(IBAction)_panRecogPanned:(id)sender;
In the viewDidLoad method of the ViewController dont forget to set the delegate as:
[_panRecog setDelegate:self];
6
. Implement other states for more power. but stateChanged will be sufficient at first.
- (IBAction)_panRecogPanned:(id)sender {
switch ([(UIPanGestureRecognizer*)sender state]) {
case UIGestureRecognizerStateBegan: { } break;
case UIGestureRecognizerStateChanged: {
if ( [((UIPanGestureRecognizer *)sender) locationInView:_filterView].y > dropDownButton.frame.height )
return; // Only drag if the user's finger is on the button
CGPoint translation = [_panRecog translationInView:filterView];
//Note that we are omitting translation.x, otherwise the filterView will be able to move horizontally as well. Also note that that MIN and MAX were written for a subview which slides down from the top, they wont work on your subview.
CGRect newFrame = _panRecog.view.frame;
//newFrame.origin.y = MIN (_panRecog.view.frame.origin.y + translation.y, FILTER_OPEN_ORIGIN_Y);
//newFrame.origin.y = MAX (newFrame.origin.y, FILTER_INITIAL_ORIGIN_Y);
newFrame.origin.y = _panRecog.view.frame.origin.y + translation.y;
_panRecog.view.frame = newFrame;
[_panRecog setTranslation:CGPointMake(0, 0) inView:self.view];
} break;
//Remember the optional step number 2? We will use hide/ show methods now:
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled: {
//CGPoint velocity = [_panRecog velocityInView:_panRecog.view];
//Bonus points for using velocity when deciding what to do when if the user lifts his finger
BOOL open;
/*
if (velocity.y < -600.0) {
open = NO;
}
else if (velocity.y >= 600.0) {
open = YES;
} else
*/
if ( _panRecog.view.frame.origin.y > (FILTER_OPEN_ORIGIN_Y + FILTER_INITIAL_ORIGIN_Y) / 2 ) {
open = YES;
}
else {
open = NO;
}
if (open == YES) {
[_filterView show];
}
else {
[_filterView hide];
}
} break;
default:
break;
}
}
Note: My filter view was on the top of the page instead of the bottom, as it is the subview will move out of the page borders. Comment out the MAX and MIN statements to fix that, I am to lazy to write them by myself. The code is modified after copy&paste, it may (and probably will) contain typos.
I think you're trying to make it too complicated - just make a view and position in such a way that it is mostly off the screen, with just a tip showing up. When you touch the tip, move the view to be fully visible, and put that movement within an animation block.
The menu items could be just UIButtons with custom graphics on that view.
Don't think complex, logic is very simple. set its height and width in such a way that it is off the screen. try this approaches and then give your feedback. check http://blogsbits.com this method will help. hit and try method can be use to check these variables.

How do you set the tab order in iOS?

Is there a way (either in IB or code) to set the tab order between text fields in a view?
Note that I'm not talking about the next form field after the return (or "Next") button is pressed -- many bluetooth keyboards have a tab key, which seems to cycle through the fields in completely different order. In my particular case, this order doesn't correspond to the fields' position in the view or even the order in which the fields were added. Modifying the xib file by hand to change the NSNextKeyView doesn't seem to make a difference either.
Does anyone know how to change this order?
#sprocket's answer was only somewhat helpful. Just because something works out of the box doesn't mean you should stop thinking about a better way -- or even the right way -- of doing something. As he noticed the behavior is undocumented but fits our needs most of the time.
This wasn't enough for me though. Think of a RTL language and tabs would still tab left-to-right, not to mention the behavior is entirely different from simulator to device (device doesn't focus the first input upon tab). Most importantly though, Apple's undocumented implementation seems to only consider views currently installed in the view hierarchy.
Think of a form in form of (no pun intended) a table view. Each cell holds a single control, hence not all form elements may be visible at the same time. Apple would just cycle back up once you reached the bottommost (on screen!) control, instead of scrolling further down. This behavior is most definitely not what we desire.
So here's what I've come up with. Your form should be managed by a view controller, and view controllers are part of the responder chain. So you're perfectly free to implement the following methods:
#pragma mark - Key Commands
- (NSArray *)keyCommands
{
static NSArray *commands;
static dispatch_once_t once;
dispatch_once(&once, ^{
UIKeyCommand *const forward = [UIKeyCommand keyCommandWithInput:#"\t" modifierFlags:0 action:#selector(tabForward:)];
UIKeyCommand *const backward = [UIKeyCommand keyCommandWithInput:#"\t" modifierFlags:UIKeyModifierShift action:#selector(tabBackward:)];
commands = #[forward, backward];
});
return commands;
}
- (void)tabForward:(UIKeyCommand *)command
{
NSArray *const controls = self.controls;
UIResponder *firstResponder = nil;
for (UIResponder *const responder in controls) {
if (firstResponder != nil && responder.canBecomeFirstResponder) {
[responder becomeFirstResponder]; return;
}
else if (responder.isFirstResponder) {
firstResponder = responder;
}
}
[controls.firstObject becomeFirstResponder];
}
- (void)tabBackward:(UIKeyCommand *)command
{
NSArray *const controls = self.controls;
UIResponder *firstResponder = nil;
for (UIResponder *const responder in controls.reverseObjectEnumerator) {
if (firstResponder != nil && responder.canBecomeFirstResponder) {
[responder becomeFirstResponder]; return;
}
else if (responder.isFirstResponder) {
firstResponder = responder;
}
}
[controls.lastObject becomeFirstResponder];
}
Additional logic for scrolling offscreen responders visible beforehand may apply.
Another advantage of this approach is that you don't need to subclass all kinds of controls you may want to display (like UITextFields) but can instead manage the logic at controller level, where, let's be honest, is the right place to do so.
I'm interested in solving the same problem, although so far the default order, which appears to be left to right, then top to bottom, is the one I want.
I tested the hypothesis that the cursor moves in depth-first order through the tree of subviews and superview, but that is not true. Changing the order of subviews without changing their location didn't change the order of fields traversed by tab presses.
One possibly useful feature is that the text field delegate's textFieldShouldBeginEditing method appears to be called for every text field in the application's window. If that returns NO, then the text field won't be chosen, so if you can define your desired order and make only the right one return YES, that might solve your problem.
This is how you set the tab order on iOS:
http://weaklyreferenced.wordpress.com/2012/11/13/responding-to-the-tab-and-shift-tab-keys-on-ios-5-ios-6-with-an-external-keyboard/
The Tab key behaviour in ios will be as follows:-
when u press tab on external keyboard- the control traverses across all the textfields in that screen by calling only shouldBeginEditing method where its return value is also determined by Apple which cant be override.
After scanning all the fields it calculates nearest x positioned Textfield relative to view offset from the current Textfield and then nearest Y Positioned Field.
Also can't be done anything until control comes to textFieldDidBeginEditing method.
Reason for apple's restriction might be to let devs to follow the guidelines of UI where next responder of field should be it's closest positioned Field rather than any other field .
Register a UIKeyCommand to detect the tab key pressed. I did this in my current view controller.
self.addKeyCommand(UIKeyCommand(input: "\t", modifierFlags: [], action: #selector(tabKeyPressed)))
Inside the key tabKeyPressed handler find your current active field then set your next responder. orderedTextFields is an array of UITextField in the tab order I want.
func tabKeyPressed(){
let activeField = getActiveField()
if(activeField == nil){
return
}
let nextResponder = getNextTextField(activeField!)
nextResponder?.becomeFirstResponder()
}
func getActiveField() -> UITextField? {
for textField in orderedTextFields {
if(textField.isFirstResponder()){
return textField
}
}
return nil
}
func getNextTextField(current: UITextField) -> UITextField? {
let index = orderedTextField.indexOf(current)
if(orderedTextField.count-1 <= index!){
return nil
}
return orderedTextField[index! + 1]
}
You can do this by setting the tag for each textfield and handling this in the textfieldShouldReturn method.
See this blogpost about it:
http://iphoneincubator.com/blog/windows-views/how-to-create-a-data-entry-screen
The only way I've found to uniquely detect a Tab keystroke from a physical keyboard, is implementing the UIKeyInput protocol's insertText: method on a custom object that canBecomeFirstResponder.
- (void)insertText:(NSString *)text {
NSLog(#"text is equal to tab character: %i", [text isEqualToString:#"\t"]);
}
I didn't get this to work while subclassing UITextField, unfortunately, as UITextField won't allow the insertText: protocol method to get called.
Might help you on the way, though..
I solved this by subclassing UITextField as NextableTextField. That subclass has a property of class UITextField with IBOutlet a hookup.
Build the interface in IB. Set the class of your text field to NextableTextField. Use the connections Inspector to drag a connection to the 'next' field you want to tab to.
In your text field delegate class, add this delegate method...
- (BOOL)textFieldShouldReturn:(UITextField *) textField
{
BOOL didResign = [textField resignFirstResponder];
if (!didResign) return NO;
if ([textField isKindOfClass:[NextableTextField class]])
dispatch_async(dispatch_get_current_queue(), ^{ [[(NextableTextField *)textField nextField] becomeFirstResponder]; });
return YES;
}
BTW - I didn't come up with this; just remember seeing someone else's idea.

Stop UIWebView from "bouncing" vertically?

Does anyone know how to stop a UIWebView from bouncing vertically? I mean when a user touches their iphone screen, drags their finger downwards, and the webview shows a blank spot above the web page I had loaded?
I've looked at the following possible solutions, but none of them worked for me:
http://www.iphonedevsdk.com/forum/iphone-sdk-development/996-turn-off-scrolling-bounces-uiwebview.html
http://forums.macrumors.com/showthread.php?t=619534
How do I stop a UIScrollView from bouncing horizontally?
for (id subview in webView.subviews)
if ([[subview class] isSubclassOfClass: [UIScrollView class]])
((UIScrollView *)subview).bounces = NO;
...seems to work fine.
It'll be accepted to App Store as well.
Update: in iOS 5.x+ there's an easier way - UIWebView has scrollView property, so your code can look like this:
webView.scrollView.bounces = NO;
Same goes for WKWebView.
I was looking at a project that makes it easy to create web apps as full fledged installable applications on the iPhone called QuickConnect, and found a solution that works, if you don't want your screen to be scrollable at all, which in my case I didn't.
In the above mentioned project/blog post, they mention a javascript function you can add to turn off the bouncing, which essentially boils down to this:
document.ontouchmove = function(event){
event.preventDefault();
}
If you want to see more about how they implement it, simply download QuickConnect and check it out.... But basically all it does is call that javascript on page load... I tried just putting it in the head of my document, and it seems to work fine.
Well all I did to accomplish this is :
UIView *firstView = [webView.subviews firstObject];
if ([firstView isKindOfClass:[UIScrollView class]]) {
UIScrollView *scroll = (UIScrollView*)firstView;
[scroll setScrollEnabled:NO]; //to stop scrolling completely
[scroll setBounces:NO]; //to stop bouncing
}
Works fine for me...
Also, the ticked answer for this question is one that Apple will reject if you use it in
your iphone app.
In the iOS 5 SDK you can access the scroll view associated with a web view directly rather than iterating through its subviews.
So to disable 'bouncing' in the scroll view you can use:
myWebView.scrollView.bounces = NO;
See the UIWebView Class Reference.
(However if you need to support versions of the SDK before 5.0, you should follow Mirek Rusin's advice.)
Swift 3
webView.scrollView.bounces = false
Warning. I used setAllowsRubberBanding: in my app, and Apple rejected it, stating that non-public API functions are not allowed (cite: 3.3.1)
In Swift to disable bounces
webViewObj.scrollView.bounces = false
Brad's method worked for me. If you use it you might want to make it a little safer.
id scrollView = [yourWebView.subviews objectAtIndex:0];
if( [scrollView respondsToSelector:#selector(setAllowsRubberBanding:)] )
{
[scrollView performSelector:#selector(setAllowsRubberBanding:) withObject:NO];
}
If apple changes something then the bounce will come back - but at least your app won't crash.
On iOS5 only if you plan to let the users zoom the webview contents (e.i.: double tap) the bounce setting isn't enough. You need to set also alwaysBounceHorizontal and alwaysBounceVertical properties to NO, else when they zoom-out (another double tap...) to default it will bounce again.
I traversed the collection of UIWebView's subviews and set their backgrounds to [UIColor blackColor], the same color as the webpage background. The view will still bounce but it will not show that ugly dark grey background.
It looks to me like the UIWebView has a UIScrollView. You can use documented APIs for this, but bouncing is set for both directions, not individually. This is in the API docs.
UIScrollView has a bounce property, so something like this works (don't know if there's more than one scrollview):
NSArray *subviews = myWebView.subviews;
NSObject *obj = nil;
int i = 0;
for (; i < subviews.count ; i++)
{
obj = [subviews objectAtIndex:i];
if([[obj class] isSubclassOfClass:[UIScrollView class]] == YES)
{
((UIScrollView*)obj).bounces = NO;
}
}
I was annoyed to find out that UIWebView is not a scroll view, so I made a custom subclass to get at the web view's scroll view. This suclass contains a scroll view so you can customize the behavior of your web view. The punchlines of this class are:
#class CustomWebView : UIWebview
...
- (id) initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
// WebViews are subclass of NSObject and not UIScrollView and therefore don't allow customization.
// However, a UIWebView is a UIScrollViewDelegate, so it must CONTAIN a ScrollView somewhere.
// To use a web view like a scroll view, let's traverse the view hierarchy to find the scroll view inside the web view.
for (UIView* v in self.subviews){
if ([v isKindOfClass:[UIScrollView class]]){
_scrollView = (UIScrollView*)v;
break;
}
}
return self;
}
Then, when you create a custom web view, you can disable bouncing with:
customWebView.scrollView.bounces = NO; //(or customWebView.scrollView.alwaysBounceVertically = NO)
This is a great general purpose way to make a web view with customizable scrolling behavior. There are just a few things to watch out for:
as with any view, you'll also need to override -(id)initWithCoder: if you use it in Interface Builder
when you initially create a web view, its content size is always the same as the size of the view's frame. After you scroll the web, the content size represents the size of the actual web contents inside the view. To get around this, I did something hacky - calling -setContentOffset:CGPointMake(0,1)animated:YES to force an unnoticeable change that will set the proper content size of the web view.
Came across this searching for an answer and I eventually just lucked on an answer of my own by messing about. I did
[[webview scrollView] setBounces:NO];
and it worked.
This worked for me, and beautifully too (I am using phonegap with webView)
[[webView.webView scrollView] setScrollEnabled:NO];
or
[[webView scrollView] setScrollEnabled:NO];
I tried a slightly different approach to prevent UIWebView objects from scrolling and bouncing: adding a gesture recognizer to override other gestures.
It seems, UIWebView or its scroller subview uses its own pan gesture recognizer to detect user scrolling. But according to Apple's documentation there is a legitimate way of overriding one gesture recognizer with another. UIGestureRecognizerDelegate protocol has a method gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: - which allows to control the behavior of any colliding gesture recognizers.
So, what I did was
in the view controller's viewDidLoad method:
// Install a pan gesture recognizer // We ignore all the touches except the first and try to prevent other pan gestures
// by registering this object as the recognizer's delegate
UIPanGestureRecognizer *recognizer;
recognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePanFrom:)];
recognizer.delegate = self;
recognizer.maximumNumberOfTouches = 1;
[self.view addGestureRecognizer:recognizer];
self.panGestureFixer = recognizer;
[recognizer release];
then, the gesture override method:
// Control gestures precedence
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
// Prevent all panning gestures (which do nothing but scroll webViews, something we want to disable in
// the most painless way)
if ([otherGestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]])
{
// Just disable every other pan gesture recognizer right away
otherGestureRecognizer.enabled = FALSE;
}
return NO;
}
Of course, this delegate method can me more complex in a real application - we may disable other recognizers selectively, analyzing otherGestureRecognizer.view and making decision based on what view it is.
And, finally, for the sake of completeness, the method we registered as a pan handler:
- (void)handlePanFrom:(UIPanGestureRecognizer *)recognizer
{
// do nothing as of yet
}
it can be empty if all we want is to cancel web views' scrolling and bouncing, or it can contain our own code to implement the kind of pan motions and animations we really want...
So far I'm just experimenting with all this stuff, and it seems to be working more or less as I want it. I haven't tried to submit any apps to iStore yet, though. But I believe I haven't used anything undocumented so far... If anyone finds it otherwise, please inform me.
Here's two newer potential solutions. Apparently, you can use jqtouch or pastrykit to disable scrolling. However, I haven't got these to work. You might be more competent.
turning off vertical scrolling
digging into pastrykit
fixed positioning on mobile safari
This link helped me lot.....Its easy.. There is a demo..
(Xcode 5 iOS 7 SDK example) Here is a Universal App example using the scrollview setBounces function. It is an open source project / example located here: Link to SimpleWebView (Project Zip and Source Code Example)
One of the subviews of UIWebView should be a UIScrollView. Set its scrollEnabled property to NO and the web view will have scrolling disabled entirely.
Note: this is technically using a private API and thus your app could be rejected or crash in future OS releases. Use #try and respondsToSelector
Look into the bounces property of UIScrollView. Quoth the Apple docs:
If the value of the property is YES (the default), the scroll view bounces when it encounters a boundary of the content. Bouncing visually indicates that scrolling has reached an edge of the content. If the value is NO, scrolling stops immediately at the content boundary without bouncing.
Make sure you're using the right UIScrollView. I'm not sure what the hierarchy looks like for a UIWebView, but the scroll view could be a parent, not a child, of the UIWebView.
To disable UIWebView scrolling you could use the following line of code:
[ObjWebview setUserInteractionEnabled:FALSE];
In this example, ObjWebview is of type UIWebView.
webView.scrollView.scrollEnabled=NO;
webView.scrollView.bounces=NO;

Resources