UIBarButtonItem appearance trouble in iOS 7, could this be an Apple bug?
Asked Answered
J

5

9

I saw an article a while back, which is here:

User Interface Customization in iOS 6

It shows customization for iOS 6. Since the article I have written Apps that use the technique, it is pretty straightforward, no magic in there.

However, I need to update one of my apps and under iOS 7 it does not work correctly. It appears that customization of UIBarButtonItems does not work the first time the view is presented. If I dismiss the view and then present it agin everything works fine. What is seen is shown here:

First time view is presented:

enter image description here

Second time:

enter image description here

I have seen this issue in his example, my code and a test app I wrote. The code is as follows:

// Customizing the Back Bar Buttons
UIImage * btBack_30 = [[UIImage imageNamed:@"btBack_30"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 13, 0, 5)];
UIImage * btBack_24 = [[UIImage imageNamed:@"btBack_24"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 12, 0, 5)];
[[UIBarButtonItem appearance] setBackButtonBackgroundImage:btBack_30 forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
[[UIBarButtonItem appearance] setBackButtonBackgroundImage:btBack_24 forState:UIControlStateNormal barMetrics:UIBarMetricsLandscapePhone];

As you can see there is no real magic, pretty standard, but I cannot find any reason or explanation why this does not work in iOS 7. The code gets executed in - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions.

I hope someone has seen this and can offer a solution. Thanks for any help!

** Note: it was proposed that this is not an apple bug, but by design. I am not saying it IS an apple issue, it could more likely be mine, but if you run either of the sample or copy and paste the code below it is apparent that the first time it does not work correctly and subsequent times it does. That would lead me to believe that the api calls are valid, but either they have a bug, or I am missing something that needs to be done.

**** UPDATE 4:**

I changed the code in MyAppDelegate's init method in my sample to the following based on the suggestion of FruityGeek, but still no luck:

- (instancetype)init
{
    self = [super init];
    if (self)
    {
        //Other UIAppearance proxy calls go here

        [[UIBarButtonItem appearance] setTitleTextAttributes:
         [NSDictionary dictionaryWithObjectsAndKeys:
          //[UIColor colorWithRed:220.0/255.0 green:104.0/255.0 blue:1.0/255.0 alpha:1.0],
          [UIColor colorWithRed:255.0/255.0 green:255.0/255.0 blue:255.0/255.0 alpha:1.0],
          UITextAttributeTextColor,
          //[UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0],
          [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.8],
          UITextAttributeTextShadowColor,
          [NSValue valueWithUIOffset:UIOffsetMake(0.5, 0.5)],
          UITextAttributeTextShadowOffset,
          [UIFont systemFontOfSize:12.0],
          UITextAttributeFont,
          nil]
                                                    forState:UIControlStateNormal];

        // Customizing the Back Bar Buttons
        //ios6 uses whole button background image
        UIImage * btBack_30 = [[UIImage imageNamed:@"btBack_30"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 13, 0, 5)];
        UIImage * btBack_24 = [[UIImage imageNamed:@"btBack_24"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 12, 0, 5)];
        [[UIBarButtonItem appearance] setBackButtonBackgroundImage:btBack_30 forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
        [[UIBarButtonItem appearance] setBackButtonBackgroundImage:btBack_24 forState:UIControlStateNormal barMetrics:UIBarMetricsLandscapePhone];

        if ([[UIDevice currentDevice].systemVersion integerValue] >= 7)
        {
            //ios7 needs additional chevron replacement image
            UIImage * chevronReplacement = chevronReplacement = [btBack_30 imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
            UIImage * chevronTransitionMaskReplacement = chevronTransitionMaskReplacement = [btBack_30 imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
            [[UINavigationBar appearance] setBackIndicatorImage:chevronReplacement];
            [[UINavigationBar appearance] setBackIndicatorTransitionMaskImage:chevronTransitionMaskReplacement];
        }
    }
    return self;
}

**** UPDATE 3:**

I have added a dropbox link to a sample project. This is in addition to the already posted link above which is a simple app that also shows the issue. Both can be built and run in the simulator with iOS 6 and iOS 7. In the iOS 6 case everything works as expected. In iOS 7, if you click the table cell and present the next view the custom back button is not shown, if you go back and present it again the button is there.

I have been fooling with this for days, so I hope someone else sees it and can tell me what I am missing.

https://www.dropbox.com/s/oi1bh3emvtbmms0/NavigationBarDemo.zip

This may be silly, but could it have to do with my images? I will try the sample with different images and post an update.

  • Tried with different images and that makes no difference, also used images from the above posted sample. It was a long shot, but since no one seems to have a better idea yet it was worth a try.

**** UPDATE 2:**

I have tried this in another test app and moved the code to the init meted of the app delegate and it still does not work. I have this posted here as well as the original authors site of the link at the top. Plus another forum, but no one seems to have a solution.

I am wondering if this could be an Apple bug?

**** UPDATE 1:**

Moved code from didFinishLaunchingWithOptions to willFinishLaunchingWithOptions and init and it still does not appear to work.

***** INIT METHOD FROM AppDelegate.m

- (id)init
{
    // Create resizable images
    UIImage *gradientImage44 = [[UIImage imageNamed:@"navBar_44"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 0, 0, 0)];
    UIImage *gradientImage32 = [[UIImage imageNamed:@"navBar_32"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 0, 0, 0)];

    // Set the background image for *all* UINavigationBars
    [[UINavigationBar appearance] setBackgroundImage:gradientImage44 forBarMetrics:UIBarMetricsDefault];
    [[UINavigationBar appearance] setBackgroundImage:gradientImage32 forBarMetrics:UIBarMetricsLandscapePhone];

    // Customize the title text for *all* UINavigationBars
    [[UINavigationBar appearance] setTitleTextAttributes:
     [NSDictionary dictionaryWithObjectsAndKeys:
      [UIColor colorWithRed:255.0/255.0 green:255.0/255.0 blue:255.0/255.0 alpha:1.0],
      UITextAttributeTextColor,
      [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.8],
      UITextAttributeTextShadowColor,
      [NSValue valueWithUIOffset:UIOffsetMake(1, 1)],
      UITextAttributeTextShadowOffset,
      [UIFont boldSystemFontOfSize:18.0],
      UITextAttributeFont,
      nil]];

    // Customizing the NavBar Buttons
    UIImage * button30 = [[UIImage imageNamed:@"btButton_30"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 5)];
    UIImage * button24 = [[UIImage imageNamed:@"btButton_24"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 5, 0, 5)];
    [[UIBarButtonItem appearance] setBackgroundImage:button30 forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
    [[UIBarButtonItem appearance] setBackgroundImage:button24 forState:UIControlStateNormal barMetrics:UIBarMetricsLandscapePhone];

    [[UIBarButtonItem appearance] setTintColor:[UIColor whiteColor]];

    [[UIBarButtonItem appearance] setTitleTextAttributes:
     [NSDictionary dictionaryWithObjectsAndKeys:
      //[UIColor colorWithRed:220.0/255.0 green:104.0/255.0 blue:1.0/255.0 alpha:1.0],
      [UIColor colorWithRed:255.0/255.0 green:255.0/255.0 blue:255.0/255.0 alpha:1.0],
      UITextAttributeTextColor,
      //[UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0],
      [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.8],
      UITextAttributeTextShadowColor,
      [NSValue valueWithUIOffset:UIOffsetMake(0.5, 0.5)],
      UITextAttributeTextShadowOffset,
      [UIFont systemFontOfSize:12.0],
      UITextAttributeFont,
      nil]
                                                forState:UIControlStateNormal];

    // Customizing the Back Bar Buttons
    UIImage * btBack_30 = [[UIImage imageNamed:@"btBack_30"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 13, 0, 5)];
    UIImage * btBack_24 = [[UIImage imageNamed:@"btBack_24"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 12, 0, 5)];
    [[UIBarButtonItem appearance] setBackButtonBackgroundImage:btBack_30 forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
    [[UIBarButtonItem appearance] setBackButtonBackgroundImage:btBack_24 forState:UIControlStateNormal barMetrics:UIBarMetricsLandscapePhone];

    return [super init];
}
Josiejosler answered 15/11, 2013 at 20:18 Comment(5)
Speculating wildly — do you go to the appearance proxy after the view has initially presented? Where does that code lie relative to establishing the root view controller?Kuth
I do this in - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions. In AppDelegate.mJosiejosler
Put your demo project on Dropbox so others can look at it (and fix the code).Easternmost
A little birdie told me you should retest with the latest 7.1 beta... :-)Easternmost
Any solutions to this? Still seem to be an issue 2019 with iOS 12!Comber
E
5

It does look like an Apple bug, and you should enter a bug on it with bug reporter. That said, I can give you a relatively painless workaround: add this code to your RecipetTableViewController:

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.title = @"Recipe Book";

    UIBarButtonItem *it = [[UIBarButtonItem alloc] initWithTitle:self.title style:UIBarButtonItemStylePlain target:nil action:NULL];
    UIImage * btBack_30 = [[UIImage imageNamed:@"btBack_30"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 13, 0, 5)];
    [it setBackButtonBackgroundImage:btBack_30 forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
    self.navigationItem.backBarButtonItem = it;
}

EDIT: You can dup this bug if interested, the more bugs referencing it the more likely apple will fix it:

BUG: 15506447

State:OpenProduct:iOS

19-Nov-2013 03:53 PM

Summary: Setting the UIBarButtonItem appearance proxy for the back bar button item does not have any affect until the second appearance of the button.

Steps to Reproduce: In appDelegate, before anything has appeared, add these statements:

UIImage * gradientImage44 = [[UIImage imageNamed:@"navBar_44"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 0, 0, 0)]; UIImage * gradientImage32 = [[UIImage imageNamed:@"navBar_32"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 0, 0, 0)]; // Set the background image for all UINavigationBars [[UINavigationBar appearance] setBackgroundImage:gradientImage44 forBarMetrics:UIBarMetricsDefault]; [[UINavigationBar appearance] setBackgroundImage:gradientImage32 forBarMetrics:UIBarMetricsLandscapePhone];

Expected Results: When a viewController is first pushed, its back button has the image in it.

Actual Results: First time it appears, there is no image. Push the view again and its there. Actually it does appear when you click on the button the very first time, but not when the button first appears.

Version: Xcode 5.0.1, iOS 7.0.3

Notes: Adding this in the root view controller of the navigation controller makes it work:

  • (void)viewDidLoad { [super viewDidLoad];

    self.title = @"Recipe Book"; UIBarButtonItem *it = [[UIBarButtonItem alloc] initWithTitle:self.title style:UIBarButtonItemStylePlain target:nil action:NULL]; UIImage * btBack_30 = [[UIImage imageNamed:@"btBack_30"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 13, 0, 5)]; [it setBackButtonBackgroundImage:btBack_30 forState:UIControlStateNormal barMetrics:UIBarMetricsDefault]; self.navigationItem.backBarButtonItem = it; }

Attached demo project shows the problem.

Configuration:

Attachments: 'DynamicsCatalog.zip' was successfully uploaded.

EDIT: I'm happy to say that again, entering bugs in bug reporter, does sometimes work!

Easternmost answered 15/11, 2013 at 20:18 Comment(6)
YEAH!!! I am glad someone at least recognized they saw it!!! Awesome and thank you!! I will try your work around, but it needs to go into my app so I will see if it works. I actually did do this much earlier, but did not want to do it this sway because I have multiple TableViewControllers in my App. I thought the problem was with the way I was using the appearance call. Anyway, I will give this a try and thanks again!!!!Josiejosler
I tried every trick in the book, and a few more, all to no avail. the root problem is that Apple dynamically creates that button when it sees a need for it, probably in a special place that they didn't think to apply the appearance stuff to. There is just no way I can think of to bottleneck the creation of the back bar button without swizzling methods. Also, if you have multiple tableviews and a tabbarController, you may be able to have to tabBarController do this when it first loads each of the tableViewControllers - or you could do a small UITableView subclass with this in it.Easternmost
OMG, I am soo glad to hear from you. I read and read and I thought i tried everything but was going crazy. I am glad you saw it. I opened a support ticket with Apple so I will see how it goes. For now, I used what you proposed and put that in each of my TableViewControllers. Hey, quick question, maybe I am having a brain freeze, but how com the title of the button shows up as Back and not the name of the presenting view. I set it as follows: TVCCMSTopLevelMenu * svc; svc.title = homeIconObj.sTitle;Josiejosler
The back title you see in some view controller is actually the backButton item in the viewController above it! yeah, I get brain freeze on this too - I've been dealing with it for years. Its like the current vc's backBarButton is "borrowed" from the viewController above it.Easternmost
My answer on iOS 7 custom back button has a patch which can fix the Apple display bug (and it is a bug).Coffelt
I have the same issue. Still seems to be an issue in iOS 12!! Any new solutions?Comber
C
0

I was able to fix this by doing the back button customization in the viewWillDisappear method of the view that is pushing to the view on which I want the back button. The reason is because it's the previous view that "owns" the back button, not the current view.

-(void) viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear: animated];
    UIImage * backButtonImage = [[UIImage imageNamed:@"back.png"] 
                      resizableImageWithCapInsets:UIEdgeInsetsMake(6, 15, 6, 7)];

    UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithTitle:self.title 
                      style:UIBarButtonItemStylePlain target:nil action:NULL];
    [buttonItem setBackButtonBackgroundImage:backButtonImage forState:UIControlStateNormal
                      barMetrics:UIBarMetricsDefault];
    self.navigationItem.backBarButtonItem = buttonItem;

}
Columba answered 15/11, 2013 at 20:18 Comment(0)
K
0

You have to handle customizing the back button item differently for iOS7. Under iOS6, the back button was a bordered button that contains the title of the previous screen with a background image that extended under the whole button.

Under iOS7, the Back control is a chevron image plus the title of the previous screen. If you want to use a custom image to replace the default chevron, you also need to create a custom mask image. iOS 7 uses the mask to make the previous screen’s title appear to emerge from—or disappear into—the chevron during navigation transitions.

Because you are doing this in a storyboard, the best place to set the appearance proxies is in your app delegate's init method.

- (instancetype)init
{
    self = [super init];
    if (self)
    {
        //Other UIAppearance proxy calls go here

        // Customizing the Back Bar Buttons
        //ios6 uses whole button background image
        UIImage * btBack_30 = [[UIImage imageNamed:@"btBack_30"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 13, 0, 5)];
        UIImage * btBack_24 = [[UIImage imageNamed:@"btBack_24"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 12, 0, 5)];
        [[UIBarButtonItem appearance] setBackButtonBackgroundImage:btBack_30 forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
        [[UIBarButtonItem appearance] setBackButtonBackgroundImage:btBack_24 forState:UIControlStateNormal barMetrics:UIBarMetricsLandscapePhone];

        if ([[UIDevice currentDevice].systemVersion integerValue] >= 7)
        {
            //ios7 needs additional chevron replacement image
            UIImage * chevronReplacement = nil;
            chevronReplacement = [chevronReplacement imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
            UIImage * chevronTransitionMaskReplacement = nil;
            chevronTransitionMaskReplacement = [chevronTransitionMaskReplacement imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
            [[UINavigationBar appearance] setBackIndicatorImage:chevronReplacement];
            [[UINavigationBar appearance] setBackIndicatorTransitionMaskImage:chevronTransitionMaskReplacement];
        }    
    }
    return self;
}
Kolnick answered 18/11, 2013 at 18:6 Comment(16)
Uhm, did you read the question? "The code gets executed in - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions."Rottweiler
The did and will methods happen at different points in app loading. Your view is already loaded prior to you making your UIAppearance calls (which is why it looks right the second time, but not the first).Kolnick
Just leave the second part of your answer.Rottweiler
Fruity Geek, I made the changes and updated my post… still does not appear to fix the problem. I posted the code in my app, but I am now also going to try it in the sample I pointed out at the beginning of my post.Josiejosler
So you are using a storyboard or nib file (and not assigning the rootViewController directly. Read the second half of the answer.Kolnick
Yes, I moved it to init, and it still does not appear to work. FYI, I am returning [super init] from the app delegate init method, is that correct?Josiejosler
@LilMoke: please post your init-code. From your comment I imagine that it is wrong.Saccharine
I assume you are referring to the init method in my AppDelegate file, so I added that to the end of the post. It is also in the sample project in the dropbox.Josiejosler
Played with your sample and changed answer.Kolnick
I put the code in your answer into the ample project init method but it still does not appear correctly the first time. I am assuming it worked for you, what am I missing?Josiejosler
The image references are nil (no effect). You have to make chevron replacement images. You can use the images in your project to see that it will work on first load before you go through the effort of making correct replacement images.Kolnick
Ahh, I see, makes sense, I will give it a try!!! Thanks so much for the info. From your first post I felt you were on to it, but I could not figure out what I was missing. If it works, I owe you a drink!!!!!Josiejosler
Ok, so I see how those calls work but it is not quite right. I can use setBackIndicator only if I comment out the setBackButtonBackgroundImage calls. But still, then I get a new Chevron replacement image, and it is over the top of the text and not stretched to the length of the text. Plus it is the iOS 7 blue.Josiejosler
You have to specify renderingMode if you don't want the image treated as a template image. Added code for you. developer.apple.com/library/ios/documentation/UserExperience/…Kolnick
Yes, I did do that. I have used that elsewhere so that was how I specified my images. As an example to show you, I did this: UIImage * chevronReplacement = chevronReplacement = [btBack_30 imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];Josiejosler
Actually, I did have the rendering mode wrong, when I changed it to UIImageRenderingModeAlwaysOriginal the button does show up, but I still need to comment out the setBackButtonBackgroundImage code and it still does not fill the text area and the text is behind it.Josiejosler
B
0

I tried to change a few things in your demo project but as you said nothing works. I thought maybe it's because of the UINavigationController subclass but using a standard one has the same behaviour.

Unfortunately, if you really have to display your button, at launch I would silently do the opening-closing behaviour ... sorry for the uggly proposal

Blowout answered 19/11, 2013 at 14:23 Comment(3)
Do you think it could be an apple issue. There is no apple doc that says the calls I am using will not work in iOS 7.Josiejosler
It's hard to say. Maybe try with the brand new 7.1 beta if you have the same behaviour.Blowout
Same behavior with 7.1, tested.Finnegan
S
-2

This is not the bug. This is default apple's iOS7 behavior.

With iOS7 appearance you don't need to set the background. Though if you want you can achieve it using custom bar button into custom navigation bar.

Enjoy Programming !!

Shrunk answered 19/11, 2013 at 13:44 Comment(1)
Ok, not much help, but in any case, if it is not a bug, with apple or me, then why does it fail to work the first time and the second time the button appears and works correctly? Have you taken a look at the two sample downloads and/or the code in init method posted above?Josiejosler

© 2022 - 2024 — McMap. All rights reserved.