Navigationbar coloring in ViewWillAppear happens too late in iOS 10
Asked Answered
M

4

19

I am facing a weird bug, that happens only on iOS 10.

I have a application with several screens, and each screen colors the navigationBar in viewWillAppear. So when you go to the next screen, it will be properly colored.

However, when testing on iOS 10 I suddenly see the following behaviour when going back to a previous screen: When the previous screen appears the navigationBar still has the color of the previous screen and then flashes to the proper color. It almost looks like viewWillAppear somehow behaves as viewDidAppear.

Relevant code:

ViewController:

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [ViewControllerPainter paint:self withBackground:[UIColor whiteColor] andForeground:[UIColor blackColor] andIsLight:true];

}

Painter:

+ (void)paint:(UIViewController *)controller withBackground:(UIColor *)backgroundColor andForeground:(UIColor *)foregroundColor andIsLight:(bool)isLight
{
    controller.navigationController.navigationBar.opaque = true;
    controller.navigationController.navigationBar.translucent = false;
    controller.navigationController.navigationBar.tintColor = foregroundColor;
    controller.navigationController.navigationBar.barTintColor = backgroundColor;
    controller.navigationController.navigationBar.backgroundColor = backgroundColor;
    controller.navigationController.navigationBar.barStyle = isLight ? UIBarStyleDefault : UIBarStyleBlack;
    controller.navigationController.navigationBar.titleTextAttributes = @{NSForegroundColorAttributeName: foregroundColor};
}

Is this a bug? Is there something I can do about to fix this? It's very frustrating.

Mucoviscidosis answered 15/9, 2016 at 12:30 Comment(1)
Same problem here!Hanyang
P
23

Here's what changed according to the iOS 10 SDK Release Notes:

In iOS 10, UIKit has updated and unified background management for UINavigationBar, UITabBar, and UIToolbar. In particular, changes to background properties of these views (such as background or shadow images, or setting the bar style) may kick off a layout pass for the bar to resolve the new background appearance.
In particular, this means that attempts to change the background appearance of these bars inside of -[UIView layoutSubviews], -[UIView updateConstraints], -[UIViewController willLayoutSubviews], -[UIViewController didLayoutSubviews], - [UIViewController updateViewConstraints], or any other method that is called in response to layout may result in a layout loop.

So the problem seems to be that viewWillAppear is triggering the mentioned layout loop, since it's called as a result of a layout change: viewWillAppear stack trace

The quick fix for me was overriding popViewControllerAnimated and pushViewController and updating the navigationBar background on my subclass of UINavigationController. Here's how it looks like:

override func popViewControllerAnimated(animated: Bool) -> UIViewController? {
    let poppedViewController = super.popViewControllerAnimated(animated)

    // Updates the navigation bar appearance
    updateAppearanceForViewController(nextViewController)

    return poppedViewController
}

override func pushViewController(viewController: UIViewController, animated: Bool) {
    super.pushViewController(viewController, animated: animated)

    // Updates the navigation bar appearance
    updateAppearanceForViewController(viewController)
}

My guess is that it works because popViewControllerAnimated and pushViewController are not called by the OS as a result of a layout change, but by a touch event. So keep that in mind if you want to find another place to update your navigationBar background.

Plasmo answered 15/9, 2016 at 18:37 Comment(4)
Interesting. Will look into this. Still it's quite messed up to change something like that in a new iOS release. I would consider it a bug, but apparently it's a feature.Mucoviscidosis
This works indeed properly. Will still file a bugreport for this behaviour, since this is not a real solution ofcourse for cases where you want your statusbar to have a different color then your 2nd viewcontrollers statusbar.Mucoviscidosis
what do you suggest for objective c?Zasuwa
please update a solution for objective c...how can this be done for tab bar controller with navigation controller?Mangum
B
16

I had to fix this with:

self.navigationController.navigationBarHidden = YES;
self.navigationController.navigationBarHidden = NO;

This way you don't have to override popviewcontroller or pushviewcontroller. It's basically triggering the navigationbar to redraw.

It's still annoying how they can just push out a new version of OS that breaks something this significant.

Bortman answered 3/12, 2016 at 14:36 Comment(2)
Thank you for this!! I put it in viewWillDisappear of poppedViewController and it works great. Thanks so much.Shoshonean
Works. Thanks a lot. By the way, with iOS 11 the changes to UiNavigationBar is even more ridiculous.Platinocyanide
D
8

Try using willMoveToParentViewController, gives the same effect as overriding UINavigationController methods but without the hassle.

Drennan answered 2/11, 2016 at 10:10 Comment(2)
This is the only viable solution that doesn't lead straight into crazy town!Taking
That's only called by the view controller that is being pushed and popped: it's fine when pushing, but when popping, you want to have access to the viewcontroller that is going to be displayed, not the one that is going to be removed, if you have colors that change for each controller.Noe
V
-1

I am posting the solution for Objective-C (subclass of UINavigationController):

#import "FUINavigationController.h"

@interface FUINavigationController ()

@end

@implementation FUINavigationController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"current: %@",[self.topViewController class]);

    if ([NSStringFromClass([self.topViewController class]) isEqualToString:@"LoginVC"]) {
        [self setNavBarHidden];
    }

    // Do any additional setup after loading the view.
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
-(UIViewController*)popViewControllerAnimated:(BOOL)animated {

    UIViewController *popedVC = [super popViewControllerAnimated:animated];

    if ([NSStringFromClass([popedVC class]) isEqualToString:@"SignUpVC"] && [NSStringFromClass([[super topViewController] class]) isEqualToString:@"LoginVC"]) {
        [self setNavBarHidden];
    }

    return popedVC;
}

-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {

    [super pushViewController:viewController animated:animated];
    [self setNavBarVisible];
}

-(void)setNavBarHidden {

    [self.navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
    [self.navigationBar setShadowImage:[UIImage new]];
    [self.navigationBar setTranslucent:YES];
    [self.navigationBar setBackgroundColor:[UIColor clearColor]];

}

-(void)setNavBarVisible {

    [self.navigationBar setBackgroundColor:[UIColor grayColor]];
    [self.navigationBar setBarTintColor:[UIColor grayColor]];
    [self.navigationBar setTintColor:[UIColor whiteColor]];
    [self.navigationBar setTranslucent:NO];
    [self.navigationBar setTitleTextAttributes:@{NSForegroundColorAttributeName :[UIColor whiteColor]}];
    [self.navigationBar setTitleTextAttributes:[NSDictionary dictionaryWithObjectsAndKeys: [UIColor whiteColor], NSForegroundColorAttributeName, [UIFont fontWithName:@"Roboto-Reqular" size:18], NSFontAttributeName,nil]];
    [self.navigationBar.topItem setBackBarButtonItem:[[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil]];

}

@end

OR using method swizzling:

#import <objc/runtime.h>
#import "UINavigationController+FadeOutNavigationBar.h"

@implementation UINavigationController (FadeOutNavigationBar)

+(void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        //Swizzling view will appear
        SEL originalSelectorVWA = @selector(viewWillAppear:);
        SEL swizzledSelectorVWA = @selector(swizzled_viewWillAppear:);

        Method originalMethodVWA = class_getInstanceMethod(class, originalSelectorVWA);
        Method swizzledMethodVWA = class_getInstanceMethod(class, swizzledSelectorVWA);

        BOOL didAddMethodVWA =
        class_addMethod(class,
                        originalSelectorVWA,
                        method_getImplementation(swizzledMethodVWA),
                        method_getTypeEncoding(swizzledMethodVWA));

        if (didAddMethodVWA) {
            class_replaceMethod(class,
                                swizzledSelectorVWA,
                                method_getImplementation(originalMethodVWA),
                                method_getTypeEncoding(originalMethodVWA));
        } else {
            method_exchangeImplementations(originalMethodVWA, swizzledMethodVWA);
        }

        //Swizzling popViewControllerAnimated
        SEL originalSelectorPVCA = @selector(popViewControllerAnimated:);
        SEL swizzledSelectorPVCA = @selector(swizzled_popViewControllerAnimated:);

        Method originalMethodPVCA = class_getInstanceMethod(class, originalSelectorPVCA);
        Method swizzledMethodPVCA = class_getInstanceMethod(class, swizzledSelectorPVCA);

        BOOL didAddMethodPVCA =
        class_addMethod(class,
                        originalSelectorPVCA,
                        method_getImplementation(swizzledMethodPVCA),
                        method_getTypeEncoding(swizzledMethodPVCA));

        if (didAddMethodPVCA) {
            class_replaceMethod(class,
                                swizzledSelectorVWA,
                                method_getImplementation(originalMethodPVCA),
                                method_getTypeEncoding(originalMethodPVCA));
        } else {
            method_exchangeImplementations(originalMethodPVCA, swizzledMethodPVCA);
        }


        //Swizzling pushViewController
        SEL originalSelectorPVC = @selector(pushViewController:animated:);
        SEL swizzledSelectorPVC = @selector(swizzled_pushViewController:animated:);

        Method originalMethodPVC = class_getInstanceMethod(class, originalSelectorPVC);
        Method swizzledMethodPVC = class_getInstanceMethod(class, swizzledSelectorPVC);

        BOOL didAddMethodPVC =
        class_addMethod(class,
                        originalSelectorPVC,
                        method_getImplementation(swizzledMethodPVC),
                        method_getTypeEncoding(swizzledMethodPVC));

        if (didAddMethodPVC) {
            class_replaceMethod(class,
                                swizzledSelectorPVC,
                                method_getImplementation(originalMethodPVC),
                                method_getTypeEncoding(originalMethodPVC));
        } else {
            method_exchangeImplementations(originalMethodPVC, swizzledMethodPVC);
        }


    });
}

#pragma mark - Method Swizzling

- (void)swizzled_viewWillAppear:(BOOL)animated {
    [self swizzled_viewWillAppear:animated];

    NSLog(@"current: %@",[self.topViewController class]);

    if ([NSStringFromClass([self.topViewController class]) isEqualToString:@"LoginVC"]) {
        [self setNavBarHidden];
    }

}

-(void)setNavBarHidden {

    [self.navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
    [self.navigationBar setShadowImage:[UIImage new]];
    [self.navigationBar setTranslucent:YES];
    [self.navigationBar setBackgroundColor:[UIColor clearColor]];

}

-(void)setNavBarVisible {

    [self.navigationBar setBackgroundColor:[UIColor grayColor]];
    [self.navigationBar setBarTintColor:[UIColor grayColor]];
    [self.navigationBar setTintColor:[UIColor whiteColor]];
    [self.navigationBar setTranslucent:NO];
    [self.navigationBar setTitleTextAttributes:@{NSForegroundColorAttributeName :[UIColor whiteColor]}];
    [self.navigationBar setTitleTextAttributes:[NSDictionary dictionaryWithObjectsAndKeys: [UIColor whiteColor], NSForegroundColorAttributeName, [UIFont fontWithName:@"Roboto-Reqular" size:18], NSFontAttributeName,nil]];
    [self.navigationBar.topItem setBackBarButtonItem:[[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil]];

}

-(UIViewController*)swizzled_popViewControllerAnimated:(BOOL)animated {

    UIViewController *popedVC = [self swizzled_popViewControllerAnimated:animated];

    if ([NSStringFromClass([popedVC class]) isEqualToString:@"SignUpVC"] && [NSStringFromClass([[self topViewController] class]) isEqualToString:@"LoginVC"]) {
        [self setNavBarHidden];
    }

    return popedVC;
}

-(void)swizzled_pushViewController:(UIViewController *)viewController animated:(BOOL)animated {

    [self swizzled_pushViewController:viewController animated:animated];
    [self setNavBarVisible];
}
@end
Viola answered 1/11, 2016 at 7:29 Comment(6)
what is inside [self setNavBarVisible] method?Mangum
-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated method is not getting called in my view controller...Mangum
Did you subclass UINavigationController?Viola
do you mean view controller as subclass of UINavigationController?Mangum
NO. Find you UINavigationController and use a subclass of it (eg "FUINavigationController.h" like the answer)Viola
Let us continue this discussion in chat.Mangum

© 2022 - 2024 — McMap. All rights reserved.