iOS runtime實用篇--監聽方法的呼叫

weixin_34185560發表於2016-12-16
1594675-5a218a8e5f50b6e7.jpg

若覺得前面的廢話太多,大家可直接看

  • 3、第三種方法也是這篇部落格重點需要講解的方法,用runtime來處理,大致步驟如下幾點

原始碼地址:

https://github.com/chenfanfang/CollectionsOfExample

1594675-56bb39f3d740c7e8.png

背景

在以前的app專案中,由於懶得封裝載入的蒙版,就直接使用了MBProgressHUD和SVProgressHUD,不得不說用得的確很方便,特別是SVProgressHUD。不過不知大家有沒有發現這樣顯示蒙版和隱藏蒙版非常突兀嗎?特別是網路請求非常快的時候,可能1s的時間都不到,給人有種一閃的感覺,當然處理方法很多(比如自己寫個方法進行延遲隱藏.....)這裡不做過多的闡述。
由於個人實在不喜歡那種一閃的感覺,所以打算在新啟動的專案中自己寫一個提示載入的蒙版,思來想去,最終還是覺得類似微信載入網頁的那種進度條比較符合我的胃口,於是封裝了個簡易的進度條,並且新增到整個專案的基類控制器(百分百AOP主義者不必做過多關於基類與AOP的評論,本文主要進行runtime監聽方法呼叫的實踐)。

老樣子先附上效果圖一張

1594675-a2a2b7d310b8cd2d.gif
進度條測試.gif

下面先附上簡單的程式碼(由於部分用到巨集,為了直觀看到效果圖,大家可以下載原始碼檢視)

封裝的進度條.h檔案

#import <UIKit/UIKit.h>

/**
 *  進度條
 */
@interface FFProgressView : UIView


- (void)star;

- (void)end;

@end


封裝的進度條.m檔案

#import "FFProgressView.h"


@interface FFProgressView ()

/** 前景view */
@property (nonatomic, strong) UIView *foregroundView;

@end

@implementation FFProgressView

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        //背景色
        self.backgroundColor = [UIColor clearColor];
        
        self.foregroundView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 3)];
        
        //前景色
        self.foregroundView.backgroundColor = FFColor(54, 188, 37);
        [self addSubview:self.foregroundView];
        
    }
    return self;
}

/// 開始載入進度條
- (void)star {
    self.foregroundView.width = 0;
    self.hidden = NO;
    
    [UIView animateWithDuration:10 delay:0 usingSpringWithDamping:1.0f initialSpringVelocity:1.0f options:kNilOptions animations:^{
        self.foregroundView.width = FFScreenW * 0.8;
        
    } completion:nil];
}

/// 結束載入進度條
- (void)end {
    
    [UIView animateWithDuration:0.5 animations:^{
        self.foregroundView.width = FFScreenW;
    } completion:^(BOOL finished) {
        self.hidden = YES;
    }];
}

基類控制器的.h檔案

#import <UIKit/UIKit.h>
/**
 *  基類控制器
 */
@interface FFRuntimeBaseController : UIViewController

/** 啟動進度條 */
- (void)starProgress;

/** 結束進度條 */
- (void)endProgress;

@end

基類控制器的.m檔案

#import "FFRuntimeBaseController.h"

//view
#import "FFProgressView.h"

@interface FFRuntimeBaseController ()

/** 網路請求的進度條 */
@property (nonatomic, strong) FFProgressView *progressView;

@end

@implementation FFRuntimeBaseController


- (void)viewDidLoad {
    [super viewDidLoad];
    
    
}

//=================================================================
//                           懶載入
//=================================================================
#pragma mark - 懶載入

- (FFProgressView *)progressView {
    if (_progressView == nil) {
        _progressView = [FFProgressView new];
        [self.view addSubview:_progressView];
        
        _progressView.frame = CGRectMake(0, 0, FFScreenW, 3);
        
        _progressView.hidden = YES;
        
    }
    
    return _progressView;
}



//=================================================================
//                           進度條
//=================================================================
#pragma mark - 進度條

- (void)starProgress {
    
    [self.progressView star];
}

- (void)endProgress {
    
    [self.progressView end];
}


@end

在子類控制器中使用進度條

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //網路請求前開始載入進度條
    [self starProgress];
    
    
    //模擬網路延遲
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        //網路請求完成結束進度條的載入
        [self endProgress];
    });
    
}

遇到問題

怎樣保持進度條始終在控制器view的最前面呢?(若不做處理,可能進度條會在控制器的view addsubView的時候給遮擋住) 我的第一反應是用KVO監聽做處理,最終以失敗告終(若簡書朋友發現用KVO可以處理的話,麻煩留下處理的方法)。

解決方案

想了三種解決方案,並且列出了可行性。

  • 1、用UIWindow來處理進度條,這樣進度條就永遠不會被遮擋。

可行性分析:若整個專案中只用一個進度條就可以用這個方式解決(什麼叫只用一個進度條呢?就是一個頁面若沒有載入完成進入不了別的介面。舉個例子,兩個介面同時處於載入狀態,若共用一個進度條則不合理。)


  • 2、在基類控制器重寫-(void)loadView方法,自定義self.view即可,簡易步驟如下

- (void)loadView {
   self.view = [[FFCustomView alloc] init];
}

- (void)viewDidLoad {
   [super viewDidLoad];
   
   NSLog(@"%@",[self.view class]);
}

//控制器輸出為 :FFCustomView

這樣子就很容易監聽到addSubView了,在FFCustomView的.h定義一個回撥block,在FFCustomView的.m重寫addSubview,簡易程式碼如下
FFCustomView.h

#import <UIKit/UIKit.h>

@interface FFCustomView : UIView

/** addSubView完成的回撥block */
@property (nonatomic, copy) void(^didAddSubView)();

@end

FFCustomView.m

#import "FFCustomView.h"

@implementation FFCustomView

- (void)addSubview:(UIView *)view {
    
    [super addSubview:view];
    
    if (self.didAddSubView) {
        self.didAddSubView();
    }
}

@end

在控制器中監聽addSubView簡易程式碼如下

    FFCustomView *customView = (FFCustomView *)self.view;
    customView.didAddSubView = ^ {
        NSLog(@"addSubView啦");
        //addSubView之後需要做何處理的程式碼寫在這即可
    };
    
    [self.view addSubview:[UIView new]];
    
    //控制器輸出:addSubView啦

可行性分析:在解決監聽控制器的view 新增子控制元件的情景下是可行的。這種方法只是作為一種普通的解決方案,本文主要介紹如何用runtime監聽所有方法呼叫。所以大家重點請看第三點解決方案。

  • 3、第三種方法也是這篇部落格重點需要講解的方法,用runtime來處理,大致步驟如下幾點
    • 新建一個UIView的category
    • 通過runtime關聯的方式新增屬性 objc_setAssociatedObject、objc_getAssociatedObject,新增一個addSubview之後的回撥block的屬性:void(^didAddsubView)()
    • 通過runtime進行方法的交換,監聽addSubView

下面直接附上程式碼

category的.h檔案

#import <UIKit/UIKit.h>

@interface UIView (FFExtension)

/** addSubview之後的回撥block */
@property (nonatomic, copy) void(^didAddsubView)();

@end

category的.m檔案

#import "UIView+FFExtension.h"
#import <objc/runtime.h>

@implementation UIView (FFExtension)

/// 新增屬性

// 定義關聯的key
static const char *key_didAddsubView = "didAddsubView";

- (void (^)())didAddsubView {
    return objc_getAssociatedObject(self, key_didAddsubView);
}

- (void)setDidAddsubView:(void (^)())didAddsubView {
    // 第一個引數:給哪個物件新增關聯
    // 第二個引數:關聯的key,通過這個key獲取
    // 第三個引數:關聯的value
    // 第四個引數:關聯的策略
    objc_setAssociatedObject(self, key_didAddsubView, didAddsubView, OBJC_ASSOCIATION_COPY_NONATOMIC);
}



/// 黑魔法交換方法

+ (void)load {
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        //進行方法交換,目的:讓UIView addSubView的時候可以被監聽到
        Class class_UIView = NSClassFromString(@"UIView");
        
        SEL originalSelector = @selector(addSubview:);
        SEL swizzledSelector = @selector(swizzlingAddSubview:);
        
        Method originalMethod = class_getInstanceMethod(class_UIView, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class_UIView, swizzledSelector);
        
        BOOL didAddMethod =
        class_addMethod(class_UIView,
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            class_replaceMethod(class_UIView,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}


- (void)swizzlingAddSubview:(UIView *)view {
    
    [self swizzlingAddSubview:view];
    
    if (self.didAddsubView) {
        self.didAddsubView();
    }
}

@end

這樣就可以監聽所有的view的addSubview了,監聽方式如下

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.didAddsubView = ^ {
        NSLog(@"我是控制器的view,我新增了一個新控制元件");
    };
    
    
    [self.view addSubview:[UIView new]];
}

//控制檯輸出:我是控制器的view,我新增了一個新控制元件

可行性分析:個人是比較推薦用這種方法進行處理。按理說,要監聽iOS中方法的呼叫,用此方法即可實現。萬能KVO.......

解決上面進度條可能被其他view遮擋的問題

將原本進度條的懶載入改成如下即可

//=================================================================
//                           懶載入
//=================================================================
#pragma mark - 懶載入

- (FFProgressView *)progressView {
    if (_progressView == nil) {
        _progressView = [FFProgressView new];
        [self.view addSubview:_progressView];
        _progressView.frame = CGRectMake(0, 0, FFScreenW, 3);
        _progressView.hidden = YES;
        

        //和前面的懶載入相比,主要新增了如下的程式碼
        __weak typeof(self) weakSelf = self;
        __weak typeof(_progressView) weakProgressView = _progressView;
        
        self.view.didAddsubView = ^ {
            //將進度條挪到最前面
            [weakSelf.view bringSubviewToFront:weakProgressView];
        };
        
    }
    
    return _progressView;
}

總結

按理說,利用方式3的方式可以監聽到所有方法的呼叫,這樣也就補充了KVO的不足之處。
部落格中若有什麼錯誤或者不足,還望指出

相關文章