iOS土味儿讲义(一)--一个Button引发的血案

2,722 阅读6分钟

开篇做一下更新说明,当你看完这篇文章的时候,如果你觉得文章里面的实现方案与需求不是那么合拍,请不要怀疑自己,因为我也这么觉得。但其实主要目的还是为了介绍一下runtime中一个不太常用的知识点,实现需求只是顺带,感谢盒子大佬的讲解,学到了很多东西。

大地母亲在忽悠护佑着你

iOS开发做了好几年,一直想写点东西却没有动手,一个是因为懒,还有一个就是我一个同事说的,我写啥?想写的别人都写过了。

事实上也确实是这样,无论你想知道什么知识点,几乎都能在网上找到答案,实在是没那个必要在后面跟风,关键是你还没有人家讲得好。

但是凡事也有利弊,资料太多了难免选择恐惧症,随便搜一个组件化流程都能找到十几个不同版本的方案,最可怕的是我还觉得他们说的都对!这就很尴尬了。

更可悲的是,看了这么多的组件化教程,被安利了各种库之后,我依然没有把组件化学会。

就好比听过了许多大道理,却依旧过不好这一生。记住了许多理论,却依然写不好代码。

这也是土系魔法讲义的由来,这个系列的每一篇文章都会更接地气一些,以一个具体需求为起始,用一种特殊的方式将文章的中心点讲述出来。

文章会以code和思路为主,讲解部分比较少,根据需求随时变更。

如果有什么地方说的不对,不用过来打我,我肯定改。

从一个Button说开去

一个最基本的UIbutton的使用大概应该是这个样子的:

- (void)viewDidLoad {
    [super viewDidLoad];
    UIButton *testButton = [UIButton buttonWithType:UIButtonTypeCustom];
    testButton.backgroundColor = [UIColor redColor];
    testButton.frame = CGRectMake(100, 100, 100, 100);
    [self.view addSubview:testButton];
    [testButton addTarget:self action:@selector(test:) forControlEvents:UIControlEventTouchUpInside];
}

- (void)test:(UIButton*)sender{
    NSLog(@"test");
}

那么问题来了,想一想,如果现在临时加一个需求,button响应事件之前要先获取相机视频权限,应该怎么做?(举个例子,同样的需求还有获取位置权限,检测网络连接,查看登录状态等等)

先不说button,权限获取的代码大概应该长这样:

AVAuthorizationStatus authStatus =  [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
    if (authStatus == AVAuthorizationStatusRestricted || authStatus ==AVAuthorizationStatusDenied){
        //啥也不干
    }else if(authStatus == AVAuthorizationStatusNotDetermined){
        [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
            if(granted){
                dispatch_async(dispatch_get_main_queue(), ^{
                    //干正事儿
                });
            }
        }];
    }else{
        //干正事儿
    }

需要注意的就一点,同意获取权限的回调需要返回主线程。

还有你需要设置info.plist的Privacy。

说起来可能有些搞笑,很大一部分的工程中,上面那段代码就这堂而皇之的躺在- (void)test:(UIButton*)sender方法里面。

当然这个写法实在是太...,除了萌新之外很少真的有人这么写了,但是其实上面那段代码其实还有以下几个变种:

  1. 知道将button原本的响应事件单独提出来,至少不用写两遍。
  2. 将获取权限的代码封装起来,大概这样:
    [Util cameraAuth:^{
       //干点啥    
    } fail:^{
        
    }];
    
  3. 以上两种方法互相结合。

好了,写到这里,50%的开发者已经躺枪了。

“没错我们就是这么写的!”

这时候有人知道我想要说什么吗?对!万恶的产品经理又来了。

“我需要你在这个button事件里,再加上照片权限获取,位置权限获取,音频权限获取!”

然后代码就变成了:

[Util cameraAuth:^{
    [Util audioAuth:^{
        [Util photoAuth:^{
           [Util locationAuth:^{
           
    } fail:^{
        
    }];    
    } fail:^{
        
    }];      
    } fail:^{
        
    }];    
    } fail:^{
        
    }];

就问你怕不怕?

别着急,饭一口一口吃,我们现在先来拯救一下这50%的小伙伴。

其实很简单,你需要的是一个UIButton的子类。(什么玩意儿?裤子都脱了你就给我看这个??)

是的就是这样,一个UIbutton的子类,需要实现的方法大概如下:

-(void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{
    AVAuthorizationStatus authStatus =  [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
    if (authStatus == AVAuthorizationStatusRestricted || authStatus ==AVAuthorizationStatusDenied){
        
    }else if(authStatus == AVAuthorizationStatusNotDetermined){
        [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
            if(granted){
                dispatch_async(dispatch_get_main_queue(), ^{
                    [super sendAction:action to:target forEvent:event];
                });
            }
        }];
    }else{
         [super sendAction:action to:target forEvent:event];
    }
    
}

这样的话,你就可以肆无忌惮的和原生的UIbutton一样去使用它了,并不需要在原本的action中添加任何的代码。

当然有必要的话还应该对event做一下区分,以免影响到这个button的其他功能交互。

有没有人这么做?我想肯定有,而且不算少,初步估计应该有个10%左右吧。

如果你真的是这么做的,恭喜你掉坑了,这种写法有个弊端就是使用button子类替换很不方便,工作量大,而且降低了可读性,总感觉哪里不太对。

没关系,至少路子走对了,如果想要继续完善这个思路,下面的这个变种了解一下。

你知道安利runtime吗?

如果你对runtime的了解和使用仅限于Method Swizzlingobjc_setAssociatedObject的话,往下看一定会有收获。

新建一个UIbutton的类别,假设之前的button子类为SKButton,则添加方法如下:

- (void)setNeedsCameraPermission{
    object_setClass(self, [SKButton class]]);
}

是的你没有看错,只需要一句话,就可以把一个UIbutton,变成他的子类,不需要#import,不需要改类名,屠龙宝刀点击就送,是不是很方便?

but...

你以为完了吗?怎么可能。

上面的写法和直接替换一个UIbutton的子类一样,有一个共同的弊端,就是当工程内部使用的button控件本身就已经是一个写好的轮子了,也是UIbutton的子类,那你怎么办?

SKButton的父类由UIbutton改为当前子类?呸!不要脸!

这个思路对吗?当然对!

但是作为一个轮子,别人拿去之后还没使用就要先补胎,你好意思吗?

所以在runtime中,不仅可以动态变更类,还可以动态创建类,你知道吗?

一个动态创建的支持获取相机权限的button的代码大概长这样:

- (void)setNeedsCameraPermission{
    NSString *className = [NSString stringWithFormat:@"CameraPermission_%@",self.class];
    Class kclass = objc_getClass([className UTF8String]);
    if (!kclass)
    {
        kclass = objc_allocateClassPair([self class], [className UTF8String], 0);
    }
    SEL setterSelector = NSSelectorFromString(@"sendAction:to:forEvent:");
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    object_setClass(self, kclass);
    const char *types = method_getTypeEncoding(setterMethod);
    class_addMethod(kclass, setterSelector, (IMP)camerapermission_SendAction, types);
    objc_registerClassPair(kclass);
}

static void camerapermission_SendAction(id self, SEL _cmd, SEL action ,id target , UIEvent *event)
{
        struct objc_super superclass = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    void (*objc_msgSendSuperCasted)(const void *, SEL, SEL, id, UIEvent*) = (void *)objc_msgSendSuper;
    AVAuthorizationStatus authStatus =  [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
    if (authStatus == AVAuthorizationStatusRestricted || authStatus ==AVAuthorizationStatusDenied){
        
    }else if(authStatus == AVAuthorizationStatusNotDetermined){
        [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
            if(granted){
                dispatch_async(dispatch_get_main_queue(), ^{
                    objc_msgSendSuperCasted(&superclass, _cmd,action,target,event);
                });
            }
        }];
    }else{
        objc_msgSendSuperCasted(&superclass, _cmd,action,target,event);
    }
}

这样就动态创建并替换了一个叫做“CameraPermission_XXXXXX”的button子类,任何一个button,只需要调用setNeedsCameraPermission方法,就能够为button添加权限获取功能了。

能够写到这里的话,基本上就差不多了,不过真的有人有耐心把这么烂的文章看完吗?

如果你真的看到这的话,那一定是因为爱情了,你也一定发现了我似乎漏掉了什么东西,我当然是故意的!

好了下面给你留一个作业,如果让你动态创建一个可自由组合,同时获取多个权限的button子类,你会写吗?