如何优雅地拦截按钮事件

作者: 前行哲 | 来源:发表于2016-09-09 10:48 被阅读1280次

    关于这个标题,起因是这样的。

    最近一次做项目需求时,遇到这样一个需求,就是本来我们App是必须注册或者第三方登录才可以使用,现在希望不登录也可以浏览App里面的内容,只是在需要的时候才提示登录,并且在用户没有登录的情况下,用户选择并登录成功了,程序需自动完成用户操作登录前的操作。比如购买商品时没有登录,用户登录成功后,直接跳转至订单确认页面。

    在接到这个需求时,我们的App功能已经很多了,评估了下这个需求,发现App里面很多功能是需要登录才可以操作,比如关注用户、购买商品、私信聊天、评论等等,而且这些功能的入口也比较多。

    这么多的地方我们都要去写判断的代码显然是不科学的,那么有没有简单点的方式呢?怎么避免我们去做苦力活呢?🤔🤔🤔

    于是,进一步分析,发现这些功能大部分都是用户主动通过点击按钮来触发下一步操作。此时,我们把关注点移到按钮UIButton上。

    最开始想到的办法是自定义一个button,让所有需要登录操作的按钮继承这个按钮,然后,在这个按钮里面拦截自身事件进一步处理。但是,发现这么做还是需要改大量的代码。接着想到用类别来做,这样直接给按钮增加一个BOOL属性,设置为YES的按钮视为需要做登录才可以操作的按钮。然后,对于需要登录操作的按钮,在分类里面拦截其点击事件,并记录targetaction,然后先判断是否登录:如果没有登录则丢弃其target和action,并且提示用户登录;如果用户已经登录或者登录成功了,则继续让target执行action,这样完美解决我们的需求,也只需要很少的代码即可搞定。
    这个方案看似很不错,不过在实际做的时候还是走了弯路。一开始,我们想从下面方法入手

    - (void)addTarget:(nullableid)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents
    

    但是发现根本就不能实现。经过查找,找到了下面这个方法:

    - (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
    

    关于这个方法,苹果给了如下解释:

    send the action. the first method is called for the event and is a point at which you can observe or override behavior. it is called repeately by the second.
    这正是我们要找的方法,于是我们重写此方法,如下:

    - (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
    {
        if (self.checkLogin)
        {
            self.selector = NSStringFromSelector(action);
            self.objClass = target;
            [self checkIsLogin];
        }
        else
        {
            [super sendAction:action to:target forEvent:event];
        }
    }
    - (void)checkIsLogin
    {
        __weak typeof(self) weakSelf = self;
        [LoginManager checkLoginSuccess:^{
            SEL sel = NSSelectorFromString(weakSelf.selector);
            if ([weakSelf.objClass respondsToSelector:sel])
            {
                if ([weakSelf.selector hasSuffix:@":"])
                {
                    objc_msgSend(weakSelf.objClass, sel, self);
                }
                else
                {
                    objc_msgSend(weakSelf.objClass, sel);
                }
            }
        }];
    }
    

    简单解释下这段代码:
    当按钮事件执行时会走sendAction:to:forEvent:这个方法,于是,我们在这个方法里面,先判断该按钮是否需要登录后再操作,如果需要,阻断事件传递,并记录下按钮的actiontarget,然后判断是否登录了,如果已经登录或者用户登录成功了,那么再调用objc_msgSend(self.objClass, self.selector)去实现按钮事件,如果用户放弃登录或者登录失败,则不做处理。

    实现了上面的方法之后,我们只需要找出那些按钮事件需要登录后才能操作,然后,设置按钮的checkLogin = YES即可,这样是不是省了很多不必要的代码。

    到此,上面的实现已经解决了所有按钮点击需要判断登录的操作。还有些是上述方式解决不了的,则使用LoginManager单独处理下,幸运的是,几乎很少地方需要单独处理。

    通过这个案例:一方面巩固了对sendAction:to:forEvent:这个方法的理解;另一方面在做需求的时候一定要发散思维,找到更合理的解决方法。

    欢迎大家留言讨论,如果你有更好地方法,欢迎分享!

    相关文章

      网友评论

      • SoaringHeart:手势事件怎么拦截啊
        button用- (BOOL)sendAction:(SEL)action to:(id)target from:(id)sender forEvent:(UIEvent *)event 这个知道
      • 介是阿姐:写的很好
        前行哲:@邓姑娘 :blush:
      • Henrya:可以在网络模块内对服务端返回的未登录异常进行处理,比如发一个通知,然后在基类控制器内接收改通知,弹出登录控制器。这里为了防止其他重复弹出登录控制器可以判断在正在显示的控制器内响应该事件。判断屏幕当前显示的控制器可以用这段代码
        //在基类接收到登录异常通知方法里添加这段代码
        if (self.isViewLoaded && self.view.window) {
        // 弹出登录控制器
        ......
        }

        这样在项目里统一处理,不用担心哪些地方需要登录处理
        前行哲:其实,上面已经有人有类似的评论了,接口层也是有判断的,会统一处理。我们并不想完全依赖接口层去处理,并且有些操作的发起还没有涉及到接口调用。
      • 84d8724e256d:能不能给发给demo,万分感谢.新手不明白,1020868688@qq.com
        前行哲:上面说的有错误啊,增加属性用到的是objc_setAssociatedObject(),获取新加的属性用的是objc_getAssociatedObject()。主要用到的是这两个。
        前行哲:主要的思路和代码都在这里了,剩下的就是在分类里面添加属性,比如代码里面的checkLogin、selector、objClass,都是给按钮增加的属性。增加属性主要用到的是objc_setAssociatedObject()和objc_getAssociatedObject()这两个方法,具体用法可以自己查下。
      • f62385835449:objc_msgSend如果这个方法是为了执行按钮的点击事件的,为什么不直接调用[super sendAction:action to:target forEvent:event]这个方法呢?
        前行哲:@老王学安卓 显然,这个方法也是可以的,某种意义上来说这种方式更好吧,不过多了个事件类型,当然此场景可以忽略事件类型。另外,块代码使用super还没尝试过,不只会不会又问题。
      • f62385835449:这是类的继承和方法的重写,不能算是类的分类。
        前行哲:@老王学安卓 前面也提到为啥没用继承,这里用分类算是“偷懒”了。
      • 混不吝丶:用runtime交换两个方法的实现 应该也可以的
        前行哲:@混不吝丶 你是指hook?
      • 其实也没有:
        jihongboo刚刚
        我们的项目也遇到了这样的问题,我的解决方案是不拦截用户请求,直接发到后端,但需要带token(登录)才能操作的API后端会返回403(权限错误),我就是在统一的错误处理的地方判断服务器返回代码,如果代码是403则提示登录。当然这也有弊端,就是需要保证用户在登录后必须保证正常的请求发出的token必须正确。不过其优点也很明显,我们甚至不用去判断每个按钮是否需要登录,直接由后端来判断。
        前行哲:@xz1201 嗯,我们接口也是像你们这样处理的,但是我们并不想完全依赖后台处理,客户端环境比较复杂,用户体验也不能丢。
      • 心中的信念:有没有考虑用runtime实现呢
        邱读书:@心中的信念 具体怎么实现呢
      • 259ef7ae0bd1:所有action事件都是由UIApplication下发的,所以直接在UIApplication分类里hook这个方法
        - (BOOL)sendAction:(SEL)action to:(id)target from:(id)sender forEvent:(UIEvent *)event
        根据target的类型来判断是否下发。如果要下发就调用原来的实现,如果需要拦截就在做完你的事后直接return NO。这样效率会更高,也无需子类化。如果你要拦截的都是UIButton,那hook UIButton的这个方法就行了,给UIButton关联一个bool值,根据这个值判断是否需要拦截就行了。
        前行哲:@Simony 嗯,我们主要是针对button,所以没有从最上层的UIApplication入手。如果加到UIApplication这里的话,判断的复杂度也会增加,对于后期的维护,代码的修改,可能会忘记同步去修改而造成一些不必要的麻烦。
      • UItachi:并不是所有的“入口”都是UIButton吧
        前行哲:@UItachi 首先,我很肯定你的回复,你说的这两个问题正如你所说,这种方式确实不规范。这么做当时主要是为了降低项目的风险以及结合项目的实际情况进行考量,所以,想了这么一个不那么规范的方法,写这篇文章的目的也是希望大家参与讨论,更欢迎那些不一样的观点。也很期待你可以指正,并给出一些合理的建议🙏。
        UItachi:@继续前行 这个办法蠢到家了。(1) 在Category中拦截事件简直就是灾难,如果其他地方有需要拦截事件的需求怎办?(2) 拦截UIButton或者UIView事件的逻辑数组View层的逻辑,验证登录的逻辑应该属于Controller层的逻辑,不符合MVC规范。
        前行哲:@UItachi 是的,非button的也可以按照文中的思路去寻找合适的解决方法。
      • 十一岁的加重:sendAction:to:forEvent:
        这个方法是写在自定义按钮里还是写在控制器里啊
        前行哲:@十一岁的加重 我觉得并不仅仅是为了增加方法才写分类,比如你还可以在分类里面做hook,也能很好的做些解耦的事等等
        十一岁的加重:@继续前行 分类里不是一般情况下不重写原写方法吗,只是增加方法
        前行哲:@十一岁的加重 button的分类里面
      • 83445de57a78:真的要多思考,少写点重复的代码

      本文标题:如何优雅地拦截按钮事件

      本文链接:https://www.haomeiwen.com/subject/gqyeettx.html