Smooth transition (advanced version) of iOS transparent navigation bar

lead

As I am Portal: hide and display when iOS navigation bar switches the interface As mentioned in, many personal center modules of apps do not retain the navigation bar, which will directly make the navigation bar transparent, such as the well-done QQ personal information interface:

Why does QQ do well? Since there are transparent navigation bars and opaque navigation bars, there must be a transition process between interface switching, and QQ does a very good job in this process. When returning from the transparent navigation bar interface to the opaque navigation bar interface, the transparency of the navigation bar is a gradual transition effect, and even a ground glass effect, Interested can open the mobile phone QQ to the personal interface to have a look. The effect is very good.

The practices of many apps are actually rough, similar to what I did in Portal: hide and display when iOS navigation bar switches the interface When the navigation bar needs to be transparent, the navigation bar is directly hidden. Directly hiding means that the entire navigation bar is useless, that is, the title and return button need to be done by yourself, which is a troublesome place. In addition, when switching between interfaces with or without navigation bar, the process is relatively rigid, and the navigation bar does not appear gradually. If all these are acceptable, the biggest problem I mentioned in that article is that if the UI tabbarcontroller is used to switch the interface, the navigation bar will have a fast animation that retracts upward, which is actually very unsightly. Of course, we can achieve the effect of being friendly to Tabbar switching by removing the animation that hides the navigation bar:

[self.navigationController setNavigationBarHidden:NO animated:NO];

But in this way, when you switch the interface under the UINavigationController system, the effect here will become very poor because there is no animation. These two contradictions have never thought of a means to reconcile, unless Tabbar is not displayed in business, but it is never a long-term plan.

At the same time, although QQ is doing well, there are still some deficiencies. If we play more with the navigation bar transition process, we will find that if we decide not to go back when we are going to return from the transparent navigation bar, we will still stay in the transparent interface of the navigation bar. At this time, although the navigation bar will return to transparency, there will be a small flaw flashing in the navigation bar.

Now that the problem has been solved, based on these problems, we will try to achieve a better smooth transition effect. Instead of customizing the navigation bar, we can directly use the system's native navigation bar and use Category and Runtime technology to achieve this effect:

The code can be downloaded from the sample project (Please add Star ~) https://github.com/Cloudox/SmoothNavDemo

Implementation process

In fact, we have three purposes:

1. Instead of customizing the navigation bar, use the system's native title and return button. This means that the navigation bar is not hidden, but the navigation bar background should be made transparent separately; 2. When switching between the transparent interface of the navigation bar or not, the transparency has a gradient effect; 3. The switching interface under UINavigationController system and UITabarController system is perfect.

For the third purpose, when we switch under UITabarController, there will be a small animation hidden in the navigation bar, but if we meet the first purpose, there will be no hidden navigation bar, so the third problem will not exist.

Let's first look at the first purpose.

Set navigation bar background transparency

There should be many views on the navigation bar. What we need to do is to make the background transparent and keep the title and return button. iOS does not directly provide us with access to the navigation bar background view, so we can only find it ourselves.

First, we traverse and print out all subviews of the UINavigationBar, including one level of subviews, to see what the navigation bar contains:

The above figure shows all the sub view s contained in the navigation bar UINavigationBar. The serial number and indentation represent the hierarchical ownership relationship. The printing method can be seen in this article: Portal: iOS traverses and prints all sub views

From the class names of these sub views, we can roughly guess what they are on the navigation bar. Let's make a bold guess_ UIBarBackground is the background view, and the subordinate UIImageView is the background picture_ UINavigationBarBackIndicatorView is a return arrow. UINavigationItemView is some navigation bar buttons added, including the return button. Because I didn't add any other buttons to the navigation bar, this must be a return button. The subordinate UILabel is the word "return".

Based on the information obtained above, we will try to_ Set the alpha values of UIBarBackground, UIImageView and UIVisualEffectView to 1 or 0 to change the transparency of the navigation bar background.

We can create a category for UINavigationController to add a method to this class to set the transparency of the navigation bar:

// UIViewController+Cloudox.m

- (void)setNeedsNavigationBackground:(CGFloat)alpha {
    // Navigation bar background transparency settings
    UIView *barBackgroundView = [[self.navigationBar subviews] objectAtIndex:0];// _UIBarBackground
    UIImageView *backgroundImageView = [[barBackgroundView subviews] objectAtIndex:0];// UIImageView
    if (self.navigationBar.isTranslucent) {
        if (backgroundImageView != nil && backgroundImageView.image != nil) {
            barBackgroundView.alpha = alpha;
        } else {
            UIView *backgroundEffectView = [[barBackgroundView subviews] objectAtIndex:1];// UIVisualEffectView
            if (backgroundEffectView != nil) {
                backgroundEffectView.alpha = alpha;
            }
        }
    } else {
        barBackgroundView.alpha = alpha;
    }
}

So far, what effect will we get? to glance at:

We successfully set the navigation bar background to transparent! But what about that thin line?! It's useless to use the above method. This line is not in the sub view we found. There are many methods to hide the thin line by checking the data, but it doesn't conflict with our setting of the navigation bar background, and it can only be hidden when the navigation bar background is set to transparent. The following method is a better method:

// Deal with the line under the navigation bar
self.navigationBar.clipsToBounds = alpha == 0.0;

When we set the transparency of the navigation bar to 0, the thin lines will be hidden, otherwise they will not be hidden. In this way, when we switch to other interfaces, the thin lines will come out again.

Now the transparency of the navigation bar is perfect:

When the background of the navigation bar is directly set to transparent, the small animation of the navigation bar stowed will not appear when the Tabbar switch interface:

Add navigation bar transparency attribute for UIViewController

For convenience, we create a Category of UIViewController and add an attribute navBarBgAlpha to it. Generally, attributes cannot be added to Category, but we can do it through the associated object of Runtime. For details, see my article: Portal: add attribute to Category in iOS , because we can only associate objects, we can't directly add properties of CGFloat type. We just add properties of NSString type directly, and then use the [NSString floatValue] method when using. In this way, each ViewController can manage its own navigation bar transparency. In the setter method of this new attribute, we call the method added in the Category of UINavigationController to set the navigation bar transparency, so as to get through.

The setting method of UIViewController is as follows:

// UIViewController+Cloudox.h

@interface UIViewController (Cloudox)
@property (copy, nonatomic) NSString *navBarBgAlpha;
@end

// UIViewController+Cloudox.m
#import "UIViewController+Cloudox.h"
// Only when the runtime is imported can the associated object be used
#import <objc/runtime.h>
// Only by importing our Category can we call the method we added
#import "UINavigationController+Cloudox.h"

@implementation UIViewController (Cloudox)

//Definition constants must be C language strings
static char *CloudoxKey = "CloudoxKey";

-(void)setNavBarBgAlpha:(NSString *)navBarBgAlpha{
    /*
     OBJC_ASSOCIATION_ASSIGN;            //assign strategy
     OBJC_ASSOCIATION_COPY_NONATOMIC;    //copy strategy
     OBJC_ASSOCIATION_RETAIN_NONATOMIC;  // retain strategy
     
     OBJC_ASSOCIATION_RETAIN;
     OBJC_ASSOCIATION_COPY;
     */
    /*
     * id object Which object's property is assigned a value
     const void *key key corresponding to attribute
     id value  Set the attribute value to value
     objc_AssociationPolicy policy  The strategy used is an enumeration value, which is the same as copy, retain and assign. NONATOMIC is generally selected for mobile phone development
     objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
     */
    
    objc_setAssociatedObject(self, CloudoxKey, navBarBgAlpha, OBJC_ASSOCIATION_COPY_NONATOMIC);
    
    // Set the navigation bar transparency (using the method added by Category)
    [self.navigationController setNeedsNavigationBackground:[navBarBgAlpha floatValue]];
}

-(NSString *)navBarBgAlpha{
    return objc_getAssociatedObject(self, CloudoxKey);
}

@end

When using, we only need:

// Make the navigation bar transparent
self.navBarBgAlpha = @"0.0";

// Make the navigation bar opaque
self.navBarBgAlpha = @"1.0";

Realize the gradual transition when switching the interface

Now a better transparent navigation bar effect is achieved, but when the transparent navigation bar interface is directly switched with the opaque navigation bar interface, the transparency of the navigation bar directly jumps:

What we want is a transparency gradient effect that changes with the sliding gesture between completely transparent and opaque like QQ. This is the best transition effect.

We need to change the transparency of the navigation bar in real time with the progress of the gesture sliding back to the interface. For example, when sliding to half of the interface, the transparency of the navigation bar should be 0.5. For this requirement, the first thought is that we should monitor the sliding progress of the sliding event.

As it happens, UINavigationController has a method_ Updateinteractive transition: to monitor this gesture and its progress, we can use the Runtime Dark Magic - Method exchange to meet our requirements.

How? Obtain the corresponding method implementation through the name of the method to be exchanged and the method we define, and then use method_ The exchangeimplementation method exchanges implementations of two methods:

+ (void)initialize {
    if (self == [UINavigationController self]) {
        // Exchange method
        SEL originalSelector = NSSelectorFromString(@"_updateInteractiveTransition:");
        SEL swizzledSelector = NSSelectorFromString(@"et__updateInteractiveTransition:");
        Method originalMethod = class_getInstanceMethod([self class], originalSelector);
        Method swizzledMethod = class_getInstanceMethod([self class], swizzledSelector);
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

This step is done in the initialize method, so that it will take effect when called. For initialization, see this article: Portal: similarities and differences between load method and initialize method in OC.

We create a method for exchange. In this method, in addition to calling the original method (note that since the implementation corresponding to the method name has been exchanged, our purpose here is to call the original implementation, but the name used is the name of the method itself), we also add a processing_ updateInteractiveTransition: one parameter is the percentage of the interface sliding process. Then we get the transparency of the navigation bar of the previous interface, the transparency of the navigation bar of the next interface, and the sliding progress. Through very simple mathematical calculation, we can get the transparency corresponding to the current progress, Here we can also see how meaningful it is for us to add a navigation bar transparency attribute to the ViewController. We can call it directly here. Of course, remember to import our Category:

// Exchange method to monitor sliding gesture
- (void)et__updateInteractiveTransition:(CGFloat)percentComplete {
    [self et__updateInteractiveTransition:(percentComplete)];
    UIViewController *topVC = self.topViewController;
    if (topVC != nil) {
        id<UIViewControllerTransitionCoordinator> coor = topVC.transitionCoordinator;
        if (coor != nil) {
            // Set the navigation bar transparency gradient as you slide
            CGFloat fromAlpha = [[coor viewControllerForKey:UITransitionContextFromViewControllerKey].navBarBgAlpha floatValue];
            CGFloat toAlpha = [[coor viewControllerForKey:UITransitionContextToViewControllerKey].navBarBgAlpha floatValue];
            CGFloat nowAlpha = fromAlpha + (toAlpha - fromAlpha) * percentComplete;
            NSLog(@"from:%f, to:%f, now:%f",fromAlpha, toAlpha, nowAlpha);
            [self setNeedsNavigationBackground:nowAlpha];
        }
    }
}

We printed the transparency gradient process. You can see:

As expected, the transparency gradually changes with the progress of the sliding interface, and the actual effect is the same:

Repair of some minor defects

The current effect is actually good, but there are also some small defects. For example, there will be a small jump when you slide to half and let go. For this, we can add a process in the Delegate of UINavigationController to monitor whether to automatically complete the return or cancel the return operation when you let go, and use UIView animation at the same time (for UIView animation, see my article: Portal: iOS basic animation tutorial ), change the transparency to the navigation bar transparency of the corresponding interface during the time of automatic operation, so that it does not jump so much:

#pragma mark - UINavigationController Delegate
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    UIViewController *topVC = self.topViewController;
    if (topVC != nil) {
        id<UIViewControllerTransitionCoordinator> coor = topVC.transitionCoordinator;
        if (coor != nil) {
            [coor notifyWhenInteractionChangesUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context){
                [self dealInteractionChanges:context];
            }];
        }
    }
}

- (void)dealInteractionChanges:(id<UIViewControllerTransitionCoordinatorContext>)context {
    if ([context isCancelled]) {// The return gesture is automatically cancelled
        NSTimeInterval cancelDuration = [context transitionDuration] * (double)[context percentComplete];
        [UIView animateWithDuration:cancelDuration animations:^{
            CGFloat nowAlpha = [[context viewControllerForKey:UITransitionContextFromViewControllerKey].navBarBgAlpha floatValue];
            NSLog(@"Auto cancel return to alpha: %f", nowAlpha);
            [self setNeedsNavigationBackground:nowAlpha];
        }];
    } else {// The return gesture is completed automatically
        NSTimeInterval finishDuration = [context transitionDuration] * (double)(1 - [context percentComplete]);
        [UIView animateWithDuration:finishDuration animations:^{
            CGFloat nowAlpha = [[context viewControllerForKey:
                                 UITransitionContextToViewControllerKey].navBarBgAlpha floatValue];
            NSLog(@"Autocomplete return to alpha: %f", nowAlpha);
            [self setNeedsNavigationBackground:nowAlpha];
        }];
    }
}

For the operations of directly clicking the return button and push ing to the next interface, you can also add a processing:

#pragma mark - UINavigationBar Delegate
- (void)navigationBar:(UINavigationBar *)navigationBar didPopItem:(UINavigationItem *)item {
    if (self.viewControllers.count >= navigationBar.items.count) {// Click the back button
        UIViewController *popToVC = self.viewControllers[self.viewControllers.count - 1];
        [self setNeedsNavigationBackground:[popToVC.navBarBgAlpha floatValue]];
    }
}

- (void)navigationBar:(UINavigationBar *)navigationBar didPushItem:(UINavigationItem *)item {
    // push to a new interface
    [self setNeedsNavigationBackground:[self.topViewController.navBarBgAlpha floatValue]];
}

But it doesn't mean much.

junction

The above processes are basically written in the Category and done at one time. All you really need to do in your ViewController is one sentence:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    
    self.navBarBgAlpha = @"0.0";
}

It's very simple ~ those interested in more effects can continue to repair by themselves. This process is also very interesting.

Again, the code can be downloaded from the sample project (Please add Star ~) if you feel helpful: https://github.com/Cloudox/SmoothNavDemo

Reference (swift): http://www.jianshu.com/p/454b06590cf1

Posted on Tue, 23 Nov 2021 02:44:26 -0500 by KoopaTroopa