4

I'm trying to snap a view to a UITabBarController's tabBar (UITabBar) to never hide it (the way Apple does it in the 'Featured' tab of the tvOS App Store).

I can make it work by setting up my constraints in a UIViewController that's directly contained in a UITabBarController. When the tab bar is hidden and shown, the view (in my case, a UICollectionView) follows perfectly with the animation. But it doesn't work as well when my UIViewController is in a UINavigationController. It eventually updates, but while the UITabBar is animating (hiding and showing), it doesn't update.

Here's how I set my constraints using NSLayoutConstraints: UIView *targetView = self.collectionView;

NSLayoutConstraint *constraint1 = [NSLayoutConstraint constraintWithItem:targetView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.tabBarController.tabBar attribute:NSLayoutAttributeBottom multiplier:1 constant:0];  
NSLayoutConstraint *constraint2 = [NSLayoutConstraint constraintWithItem:targetView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1 constant:0];  
NSLayoutConstraint *constraint3 = [NSLayoutConstraint constraintWithItem:targetView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeHeight multiplier:1 constant:0];  
NSLayoutConstraint *constraint4 = [NSLayoutConstraint constraintWithItem:targetView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeWidth multiplier:1 constant:0];  

[self.view.window addConstraint:constraint1];  
[self.view addConstraints:@[constraint2, constraint3, constraint4]];  

Here's how I set those same constraints with Masonry (both have the same result, but this is much more readable):

[self.containerView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.width.height.equalTo(self.view);
        make.top.equalTo(self.tabBarController.tabBar.mas_bottom);
    }]; 

Here's a sample project demonstrating my issue: https://www.dropbox.com/s/bxbi0gyidxhu2bz/SampleMovingWithTabBar.zip?dl=1

Is this a bug or is it expected behavior ?

I'm trying to implement this "view not hiding under the tab bar" behavior in another more complex app and I get the same odd behavior. Though in this case there are no UINavigationController involved. The view is directly in a UIViewController (which is part of the viewControllers array of the UITabBarController).

What can prevent Auto Layout constraints from being respected during animations ?

This how what my View Controller hierarchy looks like in the demo project I linked to (the View Controller that is within the UINavigationController is the one that does not animate when showing/hiding the tab bar):

<UITabBarController 0x7fca42d10bb0>, state: appeared, view: <UILayoutContainerView 0x7fca42f848b0>
   | <FirstViewController 0x7fca42d11340>, state: disappeared, view: <UIView 0x7fca42f96510>
   | <UINavigationController 0x7fca43812000>, state: appeared, view: <UILayoutContainerView 0x7fca42d46200>
   |    | <FirstViewController 0x7fca42f347f0>, state: appeared, view: <UIView 0x7fca42f8bd90>
Felix Lapalme
  • 1,058
  • 1
  • 11
  • 25
  • Can you specify your final VC hierarchy ? is it UINavigation -> UITabBarController -> UIViewController ?. Also it would be helpful to display animation video of your desired result. Not everyone knows how featured tv OS store works. – Kunal Balani Apr 10 '16 at 21:53
  • I added a print of the final VC hierarchy. View controller at 0x7fca42d11340 has no animation problems. View controller at 0x7fca42f347f0 has animation problems. – Felix Lapalme Apr 10 '16 at 22:34

1 Answers1

1

Whenever you use CoreAnimation API's for animations at that time layout constraints do not respect constraints during transient Animations.

When you use NSLayoutconstraint's property like constant for animation then auto layout does respect constraints during transient animations.

CoreAnimations API's include UIView's block based animations. If you interested in further reading then here is the wwdc video

However, looking at your code, I don't think that's your problem. You want to respect layout constraints while UITabbar is animating. You just need to set your constraints in viewDidLayoutSubviews instead of videDidAppear and you should be all set with the existing code.

EDIT ---

I got some additional details about the problem. So first of all it's not an bug by apple. UITabBar is only responsible of it's direct children view controller. It's doesn't have any information about it's sub structure. You are responsible for chaining that responsibility.

The way to fix this is to listen to transitions calls of your UIViewController and animate accordingly.

This is the magic code. Add this to your view controller.

-(void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator{

    NSString *prevFocusViewClassName = NSStringFromClass([context.previouslyFocusedView class]);
    NSString *nextFocusedView = NSStringFromClass([context.nextFocusedView class]);

    // The tabbar is going to disappear
    if ([prevFocusViewClassName isEqualToString:kUITabBarButtonClassName] &&
        ![nextFocusedView isEqualToString:kUITabBarButtonClassName]) {
        [self.view layoutIfNeeded];

        [coordinator addCoordinatedAnimations:^{
            [self.view layoutIfNeeded];
        } completion:nil];
        // The tabbar is going to appear
    } else if (![prevFocusViewClassName isEqualToString:kUITabBarButtonClassName] &&
               [nextFocusedView isEqualToString:kUITabBarButtonClassName]) {
        [self.view layoutIfNeeded];

        [coordinator addCoordinatedAnimations:^{
            [self.view layoutIfNeeded];
        } completion:nil];
    }

}

I have fixed your code with expected output https://www.dropbox.com/s/c8jdw367a3bnb42/SampleMovingWithTabBar-2.zip?dl=0

Kunal Balani
  • 4,739
  • 4
  • 36
  • 73
  • Thanks for the answer ! Yes, as you said, your first 3 paragraphs do not apply to my problem at all as I am not relying on constants or the Core Animation API. Setting my constraints in viewDidLayoutSubviews did not help. (Here's the updated sample project with where I set the constraints in viewDidLayoutSubviews: https://www.dropbox.com/s/yxon6p110ahawd5/SampleMovingWithTabBar-2.zip?dl=1 ) – Felix Lapalme Apr 10 '16 at 22:43
  • Thanks for the update ! This is the workaround I am currently using but it's really not ideal because the coordinatedAnimation doesn't have the same curve and speed as the UITabBar hiding/showing animation. You don't get the illusion that the view is glued to the tab bar. – Felix Lapalme Apr 11 '16 at 17:15
  • Also, regarding "UITabBar is only responsible of it's direct children view controller.". The view that is within the UINavigationController is a subview of the UINavigationController view which is a subview of the UITabBarController view. The fact that it DOES update (without animations) proves that the constraints are applied even if the view isn't a direct subview of the UITabController view. – Felix Lapalme Apr 11 '16 at 17:15
  • you missed the point. UITabBar animates it's direct children In first case it was your view. In second case it's the navigation controllers view not your first view. Right now the animation is not in sync because I am using Core animation API's in the delegate callback (refer my original paragraph). I have answered your original question. I am not responsible for making your code work. You are ! I have mentioned you how to do it. – Kunal Balani Apr 11 '16 at 17:22
  • I know it's possible to make it "work" that way, but it's still a hack: I could make them sync by not using the animation coordinator and trying to find the right animation curve and speed, but if Apple changes the animation curve or speed in tvOS then it's going to break the animation. But where is it documented that it only animates the direct subview and not the ones below ? I can't find that anywhere. – Felix Lapalme Apr 11 '16 at 17:50
  • It's not a hack. This is indeed the only way. The animation coordinator is responsible for communication. UITabBar know only about UINavigation it's doesn't know anything about your VC. Think this as a nesting of UIView. Every UIView is responsible for it's direct subviews not it's descendant. Instead of UIView here you have UIViewController. The delegate is there for a reason. Read the doc for UIFocusAnimationCoordinator. – Kunal Balani Apr 11 '16 at 18:10