UINavigationBar with UISegmentedControl partially covers childViews
Asked Answered
U

1

1

I have read many other threads on this and the Apple docs, but haven't found a solution yet for my particular problem.

My app uses a UITabBarController as the rootViewController, and in one of the tabs I have a UISegmentedControl in the navigationBar to switch between three child UITableViewControllers.

(In the real app two of the childVCs are a custom UIViewController, I'm just using three UITableViewControllers for the sample app).

The segmentedControl setup and the switching all works fine. The thing that goes wrong is that only the first UITableViewController is shown correctly. For the second and third one, part of the first cell is hidden under the navigationBar. When I click through all three, the first one is still ok.

I have made a little sample app to show what's going on, using very bright colors for demonstration purposes: https://www.dropbox.com/s/7pfutvn5jba6rva/SegmentedControlVC.zip?dl=0

Here is also some code (I'm not using storyboards):

// AppDelegate.m

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    FirstViewController *fvc = [[FirstViewController alloc] init];
    UINavigationController *firstNavigationController = [[UINavigationController alloc] initWithRootViewController: fvc];

    SecondViewController *svc = [[SecondViewController alloc] init];
    UINavigationController *secondNavigationController = [[UINavigationController alloc] initWithRootViewController: svc];

    // Initialize tab bar controller, add tabs controllers
    UITabBarController *tabBarController = [[UITabBarController alloc] init];
    tabBarController.viewControllers = @[firstNavigationController, secondNavigationController];

    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.window.rootViewController = tabBarController;
    [self.window makeKeyAndVisible];

    return YES;
}


// FirstViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    self.title = @"One";
    self.view.backgroundColor = [UIColor orangeColor];

    UITableViewController *vc1 = [[UITableViewController alloc] init];
    UITableViewController *vc2 = [[UITableViewController alloc] init];
    UITableViewController *vc3 = [[UITableViewController alloc] init];

    vc1.view.backgroundColor = [UIColor redColor];
    vc2.view.backgroundColor = [UIColor blueColor];
    vc3.view.backgroundColor = [UIColor greenColor];

    self.viewControllers = @[vc1, vc2, vc3];
    self.segmentTitles = @[@"Red", @"Blue", @"Green"];

    self.segmentedControl = [[UISegmentedControl alloc] initWithItems: self.segmentTitles];
    [self.segmentedControl addTarget: self
                              action: @selector(segmentClicked:)
                    forControlEvents: UIControlEventValueChanged];

    self.navigationItem.titleView = self.segmentedControl;

    self.segmentedControl.selectedSegmentIndex = 0;

 // set the first child vc:  
    UIViewController *vc = self.viewControllers[0];

    [self addChildViewController: vc];
    vc.view.frame = self.view.bounds;
    [self.view addSubview: vc.view];
    self.currentVC = vc;
}

- (void)segmentClicked:(id)sender
{
    if (sender == self.segmentedControl)
    {
        NSUInteger index = self.segmentedControl.selectedSegmentIndex;
        [self loadViewController: self.viewControllers[index]];
    }
}

- (void)loadViewController:(UIViewController *)vc
{
    [self addChildViewController: vc];

    [self transitionFromViewController: self.currentVC
                      toViewController: vc
                              duration: 1.0
                               options: UIViewAnimationOptionTransitionFlipFromBottom
                            animations: ^{
                                [self.currentVC.view removeFromSuperview];
                                vc.view.frame = self.view.bounds;
                                [self.view addSubview: vc.view];
                            } completion: ^(BOOL finished) {
                                [vc didMoveToParentViewController: self];
                                [self.currentVC removeFromParentViewController];
                                self.currentVC = vc;
                            }
     ];
}

So obviously my question is, why does this happen, and what can I do to fix it?

Edit: adding screenshots.

First VC Second VC Third VC

EDIT: Based on the answer below I changed the code in the animation block to:

[self.currentVC.view removeFromSuperview];

if ([vc.view isKindOfClass: [UIScrollView class]])
{
    UIEdgeInsets edgeInsets = UIEdgeInsetsMake(self.topLayoutGuide.length, 0, self.bottomLayoutGuide.length, 0);
    [UIView performWithoutAnimation: ^{
      vc.view.frame = self.view.bounds;
       ((UIScrollView *)vc.view).contentInset = edgeInsets;
         ((UIScrollView *)vc.view).scrollIndicatorInsets = edgeInsets;
     }];
  }
   else
   {
       vc.view.frame = self.view.bounds;
   }

   [self.view addSubview: vc.view];

Now it works. I'm going to try this with a custom UIViewController as well.

Urchin answered 1/8, 2015 at 11:39 Comment(7)
Post some screenshots of the problem.Ingeborgingelbert
I've added the screenshots, you can see the separator lines of the 2nd and 3rd table are higher.Urchin
Do not calculate the inset by hand. Instead, use the topLayoutGuide.length of the controller.Ingeborgingelbert
About animation, you can use [UIView performWithoutAnimation:] to set the insets without animations.Ingeborgingelbert
Hmm, if I use topLayoutGuide.length, then viewDidLayoutSubviews is only called the first time (for the red VC), and my problem is back.Urchin
Something is strange. viewDidLayoutSubviews should not be used to set subsequent views' insets, because indeed it should not be called later. But you say, setting them in loadViewController: does not work, which is strange. If you do set them only in loadViewController: and you see it badly, what happens if you attempt to scroll with the finger on a bad table view? Does it allow you to scroll the cell into full view? If so, you need to set the contentOffset as well to be {0, contentInset.top}.Ingeborgingelbert
Ah, I misunderstood, I moved the insets code to the animation block, and topLayoutGuide.length now works. I had to tweak the code in the animation block some more to get it to work, see my edit above.Urchin
M
1

The issue is that you do not set the correct content inset to each table view. The system attempts to do it for you, but I guess your setup is too complex for it, and it only does it for the first tableview that is loaded in viewDidLoad. In your loadViewController: method, when replacing the currently displayed view, make sure to set both the contentInset and scrollIndicatorInsets to the values of the previous view. I think the system will manage to set the correct insets later, in case you rotate to landscape. Try it. If it doesn't, you will need to do it on your own in viewDidLayoutSubviews.

Mcneill answered 1/8, 2015 at 16:44 Comment(2)
Setting the contentInset and scrollIndicatorInsets didn't work. I'm going to look into viewDidLayoutSubviews. But that would require to subclass UITableViewController, correct?Urchin
No, I meant in the container controller.Ingeborgingelbert

© 2022 - 2024 — McMap. All rights reserved.