iOS防止button重复点击

      项目中常会遇到在按钮的点击事件中去执行一些耗时操作。如果处理不当经常会出现连续多次点击push多次的情况,造成不好的用户体验。

      一种情况是用户快速连续点击,这种情况无法避免。另一种情况是点击一次后响应时间太长,导致用户一直停留在点击界面,也会去再此点击按钮确认是否能执行下一个界面。虽然我们可以在用户点击一次后去显示一个HUB窗口隔绝用户操作,但我们并不清楚服务器去响应这个操作究竟需要多长时间,如果HUB指示器显示时间太长会显得响应特别慢,如果太短,用户很可能在指示器消失后再去点击Button,这时也会出现重复push多次。

      通常有三种方式解决此问题。

一、先说一种最不推荐使用的方法。

      如果你的Navigation是自定义的,可以重写- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated方法,在此方法中做处理,代码如下:

      - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
      {
            if (![[super topViewController] isKindOfClass:[viewController class]]) {  // 如果和上一个控制器一样,隔绝此操作
                [super pushViewController:viewController animated:animated];
             }
      }

     此中方法可以防止多次重复push,但如果你想push的下一个控制器恰好和上一个控制器类型(Class)一样,就不会push成功。所以并不推荐使用此方法。

二、第二种方式,点击一次后将button的enabled变为NO。

     具体思路是:如果在button的点击事件中要做耗时操作,可能是网络请求和请求成功后的数据处理比较耗时。如果只是单纯的在请求成功和失败的回调中写一遍btn.enabled = YES就会发现如果连续点击还是会出现push多次的情况。原因可能是push操作需要时间去执行,我们在这段时间连续快速点击还是会导致push多次,感兴趣的可以去试一下。我的思路是在请求失败后单独将buuton的enabled设为YES,请求成功后不对button做任何操作。最后在- viewWillAppear方法中将button的enabled设为YES,以防在pop回本控制器的时候button不可点击。下面上代码:

其中的WXDHTTPTool是封装了一层AFNetworking的网络请求类。

在- viewWillAppear方法中将button的enabled设为YES:

这样做也可以实现防止重复push的问题,但并不能做到一劳永逸。每个控制器都需要这么去做,虽然代码并不复杂,但方式并不优雅。

三、最优雅的方式,使用Runtime监听点击事件,忽略重复点击,设置一个eventTimeInterval属性,使其规定时间内只响应一次点击事件。废话不多说,上代码。

1、为UIButton创建一个分类,这里我起名为WXD。

2、.h文件:添加一个属性eventTimeInterval,用来设置button点击间隔时间。

     #import <UIKit/UIKit.h>
    @interface UIButton (WXD)
     /**
     *  为按钮添加点击间隔 eventTimeInterval秒
     */
    @property (nonatomic, assign) NSTimeInterval eventTimeInterval;
    @end

3、.m文件:需要import<objc/runtime.h>库。

     #import "UIButton+WXD.h"
     #import <objc/runtime.h>
     #define defaultInterval 1  //默认时间间隔

    @interface UIButton ()

     /**
     *  bool YES 忽略点击事件   NO 允许点击事件
     */
    @property (nonatomic, assign) BOOL isIgnoreEvent;

    @end

    @implementation UIButton (WXD)

    static const char *UIControl_eventTimeInterval = "UIControl_eventTimeInterval";
    static const char *UIControl_enventIsIgnoreEvent = "UIControl_enventIsIgnoreEvent";


    // runtime 动态绑定 属性
    - (void)setIsIgnoreEvent:(BOOL)isIgnoreEvent
    {
         objc_setAssociatedObject(self, UIControl_enventIsIgnoreEvent, @(isIgnoreEvent), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    - (BOOL)isIgnoreEvent{
         return [objc_getAssociatedObject(self, UIControl_enventIsIgnoreEvent) boolValue];
    }

    - (NSTimeInterval)eventTimeInterval
   {
       return [objc_getAssociatedObject(self, UIControl_eventTimeInterval) doubleValue];
   }

   - (void)setEventTimeInterval:(NSTimeInterval)eventTimeInterval
   {
      objc_setAssociatedObject(self, UIControl_eventTimeInterval, @(eventTimeInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
   }

  + (void)load
  {
       // Method Swizzling
      static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{
            SEL selA = @selector(sendAction:to:forEvent:);
            SEL selB = @selector(_wxd_sendAction:to:forEvent:);
            Method methodA = class_getInstanceMethod(self,selA);
            Method methodB = class_getInstanceMethod(self, selB);

            BOOL isAdd = class_addMethod(self, selA, method_getImplementation(methodB), method_getTypeEncoding(methodB));

            if (isAdd) {
                class_replaceMethod(self, selB, method_getImplementation(methodA), method_getTypeEncoding(methodA));
            }else{
                //添加失败了 说明本类中有methodB的实现,此时只需要将methodA和methodB的IMP互换一下即可。
               method_exchangeImplementations(methodA, methodB);
           }
      });
  }

  - (void)_wxd_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
  {
       self.eventTimeInterval = self.eventTimeInterval == 0 ? defaultInterval : self.eventTimeInterval;
       if (self.isIgnoreEvent){
           return;
       }else if (self.eventTimeInterval > 0){
           dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.eventTimeInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [self setIsIgnoreEvent:NO];
            });
       }
      
       self.isIgnoreEvent = YES;
       // 这里看上去会陷入递归调用死循环,但在运行期此方法是和sendAction:to:forEvent:互换的,相当于执行sendAction:to:forEvent:方法,所以并不会陷入死循环。
       [self _wxd_sendAction:action to:target forEvent:event];
  }  

最后就可以去在想要设置点击间隔的控制器引入分类的头文件,可以手动设置button.eventTimeInterval = 点击间隔,也可以不设任何值去使用默认的时间间隔。还可以在pch文件中引入分类头文件,让项目中所有button都添加此分类。

写在最后:如果有意见或者更优雅的解决方式,欢迎沟通交流。

posted @ 2016-11-17 16:23  GnodUxn  阅读(11542)  评论(6编辑  收藏  举报