Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UISearchBar jumps if pushed controller hides the tab bar

My UI structure is as follow:

UITabBarController (TBC) -> UINavigationController (NC) -> UITableViewController (TVC)

(for the simplicity of the example lets say the TBC has only one controller on its viewControllers array - the NC)

My TVC has UISearchBar as its table header, and when the TVC appear I hide the search bar beneath the NC navigation bar by settings the table view content offset.

When user tap a cell in the TVC another view controller is pushed (VC) and hides the tab bar with VC.hidesBottomBarWhenPushed = YES;

Now there is a very annoying behavior that I dont know how to solve:
When the user tap the back button from VC back to TVC, the search bar jumps to be visible even if it was hidden (beneath the navigation bar) before the VC was pushed.

This effect happens only if the TVC doesn't have enough rows to fill the screen, its like the search bar force itself to be visible if there is a place on screen. but its really looks bad and buggy.

I uploaded a simple project that demonstrates the problem, it has the same structure as I described in my question.
I added two bar buttons for your convenience, the "hide bar" button hides the search bar for you, and the "toggle count" button toggle the table view rows count to demonstrate that the issue happens only if there are few items.

like image 450
Eyal Avatar asked Mar 06 '14 18:03

Eyal


3 Answers

Okay.. It looks to me like you've stumbled upon a bug. It should be reported through apples bugreporter (here).

I've made a fairy simple working work-around, but keep in mind that it is a work-around. This will work, but you might have to review it if you have/add other controls to the tableView. It should be safe to use(not acting randomly), and it's not the ugliest of work-arounds, so I think it's fine to use in a release. I've uploaded the same project with the fix here, and you can just go ahead and download it, and you'll probably understand what I've done. I'll explain (in extreme detail) what I've actually thought and done here, in case the download links dies in the future:

Train of thought:

As simalone also said, the problem is that when hidesBottomBarWhenPushed is set to YES, then it will call an additional viewDidLayoutSubviews which somehow resets your current state. We need to override viewDidLayoutSubviews, and check if we are laying out subviews because we are coming from ViewController, or if it's just a regular call. When we establish that the call is indeed because we are returning from ViewController, we need to hide the search bar (only if it was hidden before).

When we return from ViewController, three calls are made to viewDidLayoutSubviews in TableViewController. I'm guessing the first is for tableView, and it seems that the second call is 'for'(or rather from) the tabBar. This second one is the one moving the searchBar down. I have no idea what the third call is, but we can ignore it.

So now there are three things we need to check inside viewDidLayoutSubviews: We need to check if we are returning from ViewController, we need to check if the searchBar was hidden before we pushed(if it should hidden be now), and we need to check that it's the second call to this method.

First things first.

In TableViewController, I added a property @property BOOL backPush; to the header(.h)-file. Now I need to change this variable from ViewController.

In ViewController, I put this:

#import "TableViewController"
...
-(void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    if(self.isMovingFromParentViewController)
    {
        if([self.navigationController.topViewController isKindOfClass:[TableViewController class]])
            [((TableViewController*)self.navigationController.topViewController) setBackPush:YES];
    }
}

In the code above, when the view is disappearing (I.E pushing forward, back, closing, whatever), I'm checking if we are disappearing because it was removed from the parent. If it is(which it is when the back-button is called), I check if the now-current top view controller is of class TableViewController, which it also is if we go back. Then I set the property backPush to YES. That's the only thing we need in ViewController.

Now, to the TableViewController. I added a counter next to your row-count:

@interface TableViewController () {
    NSInteger _rows;
    int count;
}

This is to keep track of how many calls have been made to viewDidLayoutSubviews later. I set count = 0; in viewDidLoad.

Now to the magic:

-(void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];

    if((self.backPush && count == 0 && self.tableView.contentOffset.y == 
                    self.tableView.tableHeaderView.frame.size.height) || 
                    (self.backPush && count == 1 && 
                    self.tableView.contentOffset.y == 0))
    {
        if(count == 0)
            count++;
        else
        {
            count = 0;
            self.backPush = NO;
            [self hideSearchBar];
        }
    }
    else if((count == 0 || count == 1) || self.tableView.tableHeaderView.isFirstResponder)
    {
        count = 0;

        self.backPush = NO;
    }
}

The first if-statement wants either of these situations:

  1. backPush is YES, count is 0, and searchBar is already hidden.
  2. backPush is YES, count is 1, and searchBar is visible.

If 1. is true, then we increment count by 1. If 2. is true, then 1. has already happened, and we now know that we are in the second round of viewDidLayout.. when we are coming back from VC AND that the searchBar WAS hidden (because 1. happened) but now isn't hidden. It probably happens in the super-method or something. Now we can finally push the searchBar out again. I also reset count and set backPush back to NO.

The else if is also pretty important. It checks if count is 0 or 1, or if the searchBar has the keyboard showing. If count is 0 or 1 when it reaches here, it means that the first if-statement failed, e.g that the searchBar wasn't hidden, or that it was scrolled far up.

(When I think of it, the else-if should check if backPush is YES as well. Now it sets those variables repeatedly)

Let me know if you find a better way!

like image 125
Sti Avatar answered Nov 05 '22 17:11

Sti


I think this one is simple solution. Thanks to Sti

for giving some ideas to solve this bug.

Initialize variable var hideSearchBar = false

and inside viewDidLayoutSubviews add this code for maintain same content offset.

if hideSearchBar == true {
    self.tableView.contentOffset = CGPointMake(0,  self.tableView.tableHeaderView!.bounds.height - self.tableView.contentInset.top)
}

Finally implement below methods.

override func scrollViewDidScroll(scrollView: UIScrollView) {
    if self.tableView.tableHeaderView!.bounds.height - self.tableView.contentInset.top == self.tableView.contentOffset.y && self.tableView.dragging == false {
        hideSearchBar = true
    }
    else if self.tableView.dragging == true {//Reset hiding process after user dragging
        hideSearchBar = false
    }
}

func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    if self.tableView.contentOffset.y + self.tableView.contentInset.top <= self.tableView.tableHeaderView!.bounds.height
    {
        self.tableView.contentOffset = CGPointMake(0, self.tableView.tableHeaderView!.bounds.height - self.tableView.contentInset.top)
    }
}
like image 26
gksundar789 Avatar answered Nov 05 '22 16:11

gksundar789


Try to set for TVC

 self.automaticallyAdjustsScrollViewInsets = NO
like image 27
shtefane Avatar answered Nov 05 '22 16:11

shtefane