简体   繁体   中英

How to add a view below UINavigationBar

Alright, there is a fine line between hacking too far into the deep abyss that is iOS private classes, and the crusade for the ultimate reusable code.

I want to have an arbitrary UIView, of about 40 pixels height, below the UINavigationBar. Now, normally this could tossed in the enclosed View, but I want this UIView to stay with the UINavigationBar. So when the user drills down into different views, the UIView below the nav bar will stay present.

I've tried to build a container UIViewController, which would be placed as the RootViewController in the UINavigationController, but this doesn't help, as when the user pushes on a new view, my container view is slide out, and the my persistant bar goes with it.

If I put a UINavigationController inside a UIViewController, than I can overlay the UIView bar that I want, but the inner views still don't seem to change their frame's properly, as well as when a view is pushed or popped off the Navigation Stack, the views animate poorly (changing their Y position as well as moving off the screen, due to the UINavigationController not knowing about their new Y position further down the screen.)

So, I looked into subclassing UINavigationController. I know, it says " This class is not intended for subclassing. ", but there are always exceptions. This subclass would simple add the persistant UIView below the navigation bar, resize the content section (where the normal view will appear), and everything will just work.

But after playing around with ObjC runtime classes, I can't find the magic variables to tweak to get this to work (_containerView of UINavigationController was one such variable).

What's the best way to do this? Is there a better way than copying and pasting the code to make this Bar into all the ViewControllers that will display it?

I'm guessing you are looking to put an upload progress bar or something similar that shows up below the navigation bar, and stays with you no matter which view you go to. I just implemented this a couple of weeks ago with an extra caveat - we're using IIViewDeck so we had to be able to handle changes of the whole UINavigationController as well.

What I did was I added the view as a subview of the navigation bar. My navigation bar and navigation controller are both subclassed (and apps have been submitted to App Store without incident). At first I did this because I needed to customize the nav bar without the help of UIAppearance but now it seems to be the gift that keeps on giving since it gives me quite a bit of flexibility for tasks such as this.

  1. Create customized UINavigationController and UINavigationBar
  2. If you need to handle touches, you will have to implement hitTest:withEvent - I've included that code below too.
  3. If you have an app with changing UINavigationController views (such as a tab bar app, or with a side menu that lets you change the UINavigationController that's in the center), I implemented KVO to monitor the center view controller so that the progress view can 'follow' it around. Code is below as well.

This is how I created a custom UINavigationController and UINavigationBar:

+ (ZNavigationController *)customizedNavigationController
{
 //This is just a regular UINavigationBar subclass - doesn't have to have any code but I personally override the pop/push methods to call my own flavor of viewWill/Did/Appear/Disappear methods - iOS has always been a bit finicky about what gets called when
    ZNavigationController *navController = [[ZNavigationController alloc] initWithNibName:nil bundle:nil];

    // Ensure the UINavigationBar is created so that it can be archived. If we do not access the
    // navigation bar then it will not be allocated, and thus, it will not be archived by the
    // NSKeyedArchvier.
    [navController navigationBar];

    // Archive the navigation controller.
    NSMutableData *data = [NSMutableData data];
    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
    [archiver encodeObject:navController forKey:@"root"];
    [archiver finishEncoding];

    // Unarchive the navigation controller and ensure that our UINavigationBar subclass is used.
    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
    [unarchiver setClass:[ZNavigationBar class] forClassName:@"UINavigationBar"];
    ZNavigationController *customizedNavController = [unarchiver decodeObjectForKey:@"root"];
    [unarchiver finishDecoding];

    // Modify the navigation bar to have a background image.
    ZNavigationBar *navBar = (ZNavigationBar *)[customizedNavController navigationBar];

    [navBar setTintColor:kZodioColorBlue];
    [navBar setBackgroundImage:[UIImage imageNamed:@"Home_TopBar_RoundCorner_withLogo"] forBarMetrics:UIBarMetricsDefault];

    return customizedNavController;
}

Now that you have your own custom ZNavigationBar (these are the actual names from my code - was a pain to go through and change them all) you implement touch handling - the variable names are pretty self explanatory - I want to detect touches in a 'left accessory view', 'main area', or 'right accessory view' of this progress bar:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    DDLogVerbose(@"Got a touch at point X: %f Y: %f", point.x, point.y);

 //First check if the progress view is visible and touch is within that frame
    if ([[JGUploadManager sharedManager] progressViewVisible] &&
        CGRectContainsPoint([[JGUploadManager sharedManager] uploadProgressView].frame, point))
    {
 //Modified frame - have to compensate for actual position of buttons in the view from the standpoint of the UINavigationBar. Can probably use another/smarter method to do this.
        CGRect modifiedCancelButtonFrame = [[JGUploadManager sharedManager] uploadProgressView].leftAccessoryFrame;
        modifiedCancelButtonFrame.origin.y += [[JGUploadManager sharedManager] uploadProgressView].leftAccessoryView.superview.frame.origin.y;

 //Do the same thing for the right view
        CGRect modifiedRetryButtonFrame = [[JGUploadManager sharedManager] uploadProgressView].rightAccessoryFrame;
        modifiedRetryButtonFrame.origin.y += [[JGUploadManager sharedManager] uploadProgressView].rightAccessoryView.superview.frame.origin.y;


 //Touch is on the left button
        if ([[JGUploadManager sharedManager] progressViewVisible] &&
            [[JGUploadManager sharedManager] uploadProgressView].leftAccessoryView &&
            CGRectContainsPoint(modifiedCancelButtonFrame, point))
        {
            return [[JGUploadManager sharedManager] uploadProgressView].leftAccessoryView;
        }


 //Touch is on the right button
        else if ([[JGUploadManager sharedManager] progressViewVisible] &&
                 [[JGUploadManager sharedManager] uploadProgressView].rightAccessoryView &&
                 CGRectContainsPoint(modifiedRetryButtonFrame, point))
        {
            return [[JGUploadManager sharedManager] uploadProgressView].rightAccessoryView;
        }

 //Touch is in the main/center area
        else
        {
            return [[JGUploadManager sharedManager] uploadProgressView];
        }

    }

 //Touch is not in the progress view, pass to super
       else
    {
        return [super hitTest:point withEvent:event];
    }
}

Now the KVO part that 'follows' the navigation controller. Implement KVO somewhere - I use a singleton 'upload manager' class and I put it in the init of that class, but that's up to your app's design:

ZRootViewController *rootViewController = ((JGAppDelegate*)[UIApplication sharedApplication].delegate).rootViewDeckController;
[rootViewController addObserver:sharedManager
                     forKeyPath:@"centerController"
                        options:NSKeyValueObservingOptionNew
                        context:nil];

Then of course implement this:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    DDLogVerbose(@"KVO shows something changed: %@", keyPath);

    if ([keyPath isEqualToString:@"centerController"])
    {
        DDLogVerbose(@"Center controller changed to: %@", object);
        [self centerViewControllerChanged];
    }

}

And finally the centerViewControllerChanged function:

- (void)centerViewControllerChanged
{
    ZRootViewController *rootViewController = ((JGAppDelegate*)[UIApplication sharedApplication].delegate).rootViewDeckController;

    ZNavigationController *centerController = (ZNavigationController *)rootViewController.centerController;

    //Check if the upload progress view is visible and/or active
    if (self.uploadProgressView.frame.origin.y > 0 && self.uploadProgressView.activityIndicatorView.isAnimating)
    {
        [centerController.navigationBar addSubview:self.uploadProgressView];
    }

    //Keep weak pointer to center controller for other stuff
    DDLogVerbose(@"Setting center view controller: %@", centerController);
    self.centerViewController = centerController;
}

If I misunderstood your question then I just typed the longest SO answer in the world, in vain lol. I hope this helps and let me know if any part needs clarification!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM