iOS: 記一次導(dǎo)航欄平滑過(guò)渡的實(shí)現(xiàn)

隨著技術(shù)的迭代,現(xiàn)在對(duì)App的效果要求越來(lái)越高,那么在這篇文章里面我們一起討論一下如何在控制器做跳轉(zhuǎn)的時(shí)候?qū)?dǎo)航欄做平滑過(guò)渡的轉(zhuǎn)場(chǎng)

構(gòu)思:

  • 首先要獲取到導(dǎo)航欄里的子控件來(lái)設(shè)置其透明度(實(shí)現(xiàn)透明度變化)
  • 為所有控制器添加一個(gè)導(dǎo)航欄透明度屬性,用于記錄當(dāng)前控制器的導(dǎo)航欄透明度(記錄透明度值)
  • 通過(guò)監(jiān)聽(tīng)手勢(shì)滑動(dòng)來(lái)獲取源和目的控制器,計(jì)算從源到目的控制器的透明度變化,來(lái)改變導(dǎo)航欄的透明度(實(shí)現(xiàn)平滑過(guò)渡)
bardemo

1、實(shí)現(xiàn)透明度變化

要想實(shí)現(xiàn)透明度變化,得先獲取到導(dǎo)航欄里的子控件,然后設(shè)置其alpha值。但是如何獲取呢? 首先考慮使用KVC,通過(guò)導(dǎo)航欄的'valueForKey:'方法來(lái)獲取子控件對(duì)象,但是在不同的系統(tǒng)上,導(dǎo)航欄里的子控件布局排布也是有所不同, 意味著key值并非固定,通過(guò)key值拿子控件對(duì)象的方法在不同的系統(tǒng)上就很容易拋異常。

左:iOS10 右:iOS9

考慮到這一點(diǎn),我采用了最直接的方式:遍歷導(dǎo)航欄的所有子控件,拿到首個(gè)子控件給其設(shè)置透明度。這樣不但不需要再去考慮系統(tǒng)的問(wèn)題了,同時(shí)也能滿足帶顏色的導(dǎo)航欄或者是帶背景圖的導(dǎo)航欄透明度的變化。

- (void)xa_changeNavBarAlpha:(CGFloat)navBarAlpha{
    NSMutableArray *barSubviews = [NSMutableArray array];
    //將導(dǎo)航欄的子控件添加到數(shù)組當(dāng)中,取首個(gè)子控件設(shè)置透明度(防止導(dǎo)航欄上存在非導(dǎo)航欄自帶的控件)
    for (UIView * view in self.navigationBar.subviews) {
        if(![view isMemberOfClass:[UIView class]]){
            [barSubviews addObject:view];
        }
    }
    UIView *barBackgroundView = [barSubviews firstObject];
    barBackgroundView.alpha   = navBarAlpha;
}

2、記錄透明度

每個(gè)控制器都應(yīng)該有自己的導(dǎo)航欄透明度且當(dāng)透明度發(fā)生變化后我們都應(yīng)該把值保存下來(lái),以方便下次的使用,這里我們就給UIViewController添加一個(gè)分類并加一個(gè)navBarAlpha的屬性,這樣我們就可以直接通過(guò)控制器去設(shè)置導(dǎo)航欄的透明度啦~

- (CGFloat)xa_navBarAlpha{
    return [objc_getAssociatedObject(self, _cmd)floatValue] ;
}

- (void)setXa_navBarAlpha:(CGFloat)xa_navBarAlpha{
    if(xa_navBarAlpha > 1){
        xa_navBarAlpha = 1;
    }
    if(xa_navBarAlpha < 0){
        xa_navBarAlpha = 0;
    }
    objc_setAssociatedObject(self, @selector(xa_navBarAlpha), @(xa_navBarAlpha), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    [self.navigationController xa_changeNavBarAlpha:xa_navBarAlpha];
}

3、實(shí)現(xiàn)平滑過(guò)渡

現(xiàn)在存在的問(wèn)題就是我在某個(gè)頁(yè)面設(shè)置了導(dǎo)航欄的透明度,back回上一個(gè)界面,導(dǎo)航欄的透明度值仍然是上個(gè)界面的


navbug

這里做過(guò)渡有兩種情況,一種是手勢(shì)滑動(dòng)back回上一個(gè)界面,還有一種情況是直接點(diǎn)擊了back按鈕回到上一個(gè)界面的。根據(jù)這兩種情況我們分別做一下處理。

3.1、手勢(shì)滑動(dòng)back

這種情況我們需要去監(jiān)聽(tīng)導(dǎo)航控制器的手勢(shì)滑動(dòng),導(dǎo)航控制器有個(gè)方法'_updateInteractiveTransition:',該方法可以監(jiān)聽(tīng)手勢(shì)滑動(dòng)以及當(dāng)前轉(zhuǎn)場(chǎng)的進(jìn)度,我們可以通過(guò)swizzing來(lái)交換方法實(shí)現(xiàn),來(lái)接手'_updateInteractiveTransition:'方法調(diào)用的監(jiān)聽(tīng)

+ (void)load{
    //交換導(dǎo)航控制器的手勢(shì)進(jìn)度轉(zhuǎn)場(chǎng)方法,來(lái)監(jiān)聽(tīng)手勢(shì)滑動(dòng)的進(jìn)度
    SEL originalSEL =  NSSelectorFromString(@"_updateInteractiveTransition:");
    SEL swizzledSEL =  NSSelectorFromString(@"xa_updateInteractiveTransition:");
    Method originalMethod = class_getInstanceMethod(self,  originalSEL);
    Method swizzledMethod = class_getInstanceMethod(self,  swizzledSEL);
    BOOL success = class_addMethod(self, originalSEL, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if(success){
        class_replaceMethod(self, swizzledSEL, method_getImplementation(originalMethod),  method_getTypeEncoding(originalMethod));
    }else{
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

然后通過(guò)轉(zhuǎn)場(chǎng)的上下文信息,拿到源和目的的控制器navBarAlpha值,再根據(jù)percentComplete轉(zhuǎn)場(chǎng)進(jìn)度參數(shù)計(jì)算并設(shè)置導(dǎo)航欄透明度值,這樣就完成了手勢(shì)滑動(dòng)的back

- (void)xa_updateInteractiveTransition:(CGFloat)percentComplete{
    [self xa_updateInteractiveTransition:percentComplete];
    UIViewController *topVC = self.topViewController;
    if(topVC){
        //通過(guò)transitionCoordinator拿到轉(zhuǎn)場(chǎng)的兩個(gè)控制器上下文信息
        id <UIViewControllerTransitionCoordinator> coordinator =  topVC.transitionCoordinator;
        if(coordinator != nil){
            //拿到源控制器和目的控制器的透明度(每個(gè)控制器都單獨(dú)保存了一份)
            CGFloat fromVCAlpha  = [coordinator viewControllerForKey:UITransitionContextFromViewControllerKey].xa_navBarAlpha;
            CGFloat toVCAlpha    = [coordinator viewControllerForKey:UITransitionContextToViewControllerKey].xa_navBarAlpha;
            //再通過(guò)源,目的控制器的導(dǎo)航條透明度和轉(zhuǎn)場(chǎng)的進(jìn)度(percentComplete)計(jì)算轉(zhuǎn)場(chǎng)時(shí)導(dǎo)航條的透明度
            CGFloat newAlpha     = fromVCAlpha + ((toVCAlpha - fromVCAlpha ) * percentComplete);
            //這里不要直接去修改控制器navBarAlpha屬性,會(huì)影響目的控制器的navBarAlpha的數(shù)值
            [self xa_changeNavBarAlpha:newAlpha];
        }
    }
}

3.2、按鈕點(diǎn)擊back

當(dāng)點(diǎn)擊buttonItem back控制器的時(shí)候我們可以在'viewWillAppear:'的時(shí)候設(shè)置回當(dāng)前控制器的透明度值,所以我們同樣要交換'viewWillAppear:'方法的實(shí)現(xiàn),那么每當(dāng)控制器要顯示的時(shí)候,我們總是要將它重置回當(dāng)前控制器應(yīng)有的透明度值。

這里另外還需要做兩個(gè)邏輯判斷:

  • 一個(gè)是判斷手勢(shì)是否正在滑動(dòng)。如果是YES表示當(dāng)前的狀態(tài)是手勢(shì)滑動(dòng)back的狀態(tài)則不需要處理。
  • 另外一個(gè)邏輯是判斷當(dāng)前控制器是否設(shè)置過(guò)navBarAlpha的屬性值。如果有設(shè)置過(guò),那么每次控制器要顯示的時(shí)候都要將導(dǎo)航欄透明度設(shè)置成控制器儲(chǔ)存的透明值。反之,我們給這個(gè)控制器設(shè)置一個(gè)默認(rèn)的透明度值
- (void)xa_viewWillAppear:(BOOL)animated{
    [self xa_viewWillAppear:animated];
    
    //當(dāng)前控制器父控制器是導(dǎo)航控制器并且不是通過(guò)手勢(shì)滑動(dòng)顯示的
    if([self.parentViewController isKindOfClass:[UINavigationController class]] &&
       (!self.navigationController.xa_isGrTransitioning)){
        //如果在控制器初始化的時(shí)候用戶設(shè)置過(guò)導(dǎo)航欄的值,那么我們直接設(shè)置該導(dǎo)航欄應(yīng)有的透明度值,沒(méi)有設(shè)置過(guò)的話默認(rèn)透明度給1
        if(self.xa_didSetBarAlpha){
            [self.navigationController xa_changeNavBarAlpha:self.xa_navBarAlpha];
        }else{
            self.xa_navBarAlpha = 1;
        }
    }
}

4、細(xì)節(jié)優(yōu)化與調(diào)整

在手勢(shì)滑動(dòng)的時(shí)候,如果滑動(dòng)到了一半就松手了,那么導(dǎo)航欄就可能自動(dòng)完成或者取消返回操作了,導(dǎo)致剩下的導(dǎo)航欄的透明度將無(wú)法計(jì)算,可以看到firstViewController的導(dǎo)航欄透明度并非是1

bug

對(duì)于這一點(diǎn)的話,我們可以添加手勢(shì)滑動(dòng)的交互的狀態(tài),如果當(dāng)前的滑動(dòng)的過(guò)程中中斷,那么判斷是取消操作還是完成操作,然后再完成剩余的動(dòng)畫效果。
首先我們要去監(jiān)聽(tīng)導(dǎo)航控制器的'popViewControllerAnimated:'的方法調(diào)用

+ (void)load{
    //交換導(dǎo)航控制器的popViewControllerAnimated:方法,來(lái)監(jiān)聽(tīng)什么時(shí)候當(dāng)前控制被back
    SEL popOriginalSEL =  @selector(popViewControllerAnimated:);
    SEL popSwizzledSEL =  NSSelectorFromString(@"xa_popViewControllerAnimated:");
    Method popOriginalMethod = class_getInstanceMethod(self,  popOriginalSEL);
    Method popSwizzledMethod = class_getInstanceMethod(self,  popSwizzledSEL);
    BOOL popSuccess = class_addMethod(self, popOriginalSEL, method_getImplementation(popSwizzledMethod), method_getTypeEncoding(popSwizzledMethod));
    if(popSuccess){
        class_replaceMethod(self, popSwizzledSEL, method_getImplementation(popOriginalMethod),  method_getTypeEncoding(popOriginalMethod));
    }else{
        method_exchangeImplementations(popOriginalMethod, popSwizzledMethod);
    }
    
}

然后再通過(guò)轉(zhuǎn)場(chǎng)協(xié)調(diào)器對(duì)象監(jiān)聽(tīng)手勢(shì)滑動(dòng)交互的改變

- (UIViewController *)xa_popViewControllerAnimated:(BOOL)animated{
    UIViewController *popVc =  [self xa_popViewControllerAnimated:animated];
    if(self.viewControllers.count <= 0){
        return popVc;
    }
    UIViewController *topVC = [self.viewControllers lastObject];
    if (topVC != nil) {
        id<UIViewControllerTransitionCoordinator> coordinator = topVC.transitionCoordinator;
        
        //監(jiān)聽(tīng)手勢(shì)返回的交互改變,如手勢(shì)滑動(dòng)過(guò)程當(dāng)中松手就會(huì)回調(diào)block
        if (coordinator != nil) {
            if([[UIDevice currentDevice].systemVersion intValue]  >= 10){//適配iOS10
                [coordinator notifyWhenInteractionChangesUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context){
                    [self dealNavBarChangeAction:context];
                }];
            }else{
                [coordinator notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
                    [self dealNavBarChangeAction:context];
                }];
            }
        }
    }
    return popVc;
}

最后我們通過(guò)轉(zhuǎn)場(chǎng)的上下文信息根據(jù)操作(自動(dòng)完成還是返回操作)來(lái)獲取剩余的動(dòng)畫時(shí)長(zhǎng),并完成剩余的動(dòng)畫

- (void)dealNavBarChangeAction:(id<UIViewControllerTransitionCoordinatorContext>)context {
    if ([context isCancelled]) {// 取消了(還在當(dāng)前頁(yè)面)
        //根據(jù)剩余的進(jìn)度來(lái)計(jì)算動(dòng)畫時(shí)長(zhǎng)xa_changeNavBarAlpha
        CGFloat animdDuration = [context transitionDuration] * [context percentComplete];
        CGFloat fromVCAlpha   = [context viewControllerForKey:UITransitionContextFromViewControllerKey].xa_navBarAlpha;
        [UIView animateWithDuration:animdDuration animations:^{
            [self xa_changeNavBarAlpha:fromVCAlpha];
        }];
        
    } else {// 自動(dòng)完成(pop到上一個(gè)界面了)
        
        CGFloat animdDuration = [context transitionDuration] * (1 -  [context percentComplete]);
        CGFloat toVCAlpha     = [context viewControllerForKey:UITransitionContextToViewControllerKey].xa_navBarAlpha;
        [UIView animateWithDuration:animdDuration animations:^{
            [self xa_changeNavBarAlpha:toVCAlpha];
        }];
    };
}

最后:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容