UITabBar 点击刷新功能

我们都知道 UITabBar 只能添加 UITabBarItem,而 UITabBarItem 是继承自 NSObject 的,但是我们可以发现 UITabBarItem 内部有一个 _view 属性与私有类 UITabBarButton 关联着,而这个 UITabBarButton 正是我们在 UITabBar 上看到的一个个按钮对象。

经过调试,发现 UITabBarButton 的 UIControlEventTouchUpInside 事件与 UITabBar 的 _buttonUp: 方法关联着,所以我们只需要通过 runtime hook 该方法即可拦截到 UITabBarButton 的点击事件。

这里提醒一下,虽然 UITabBarButton 继承自 UIControl,但是对于是否选中的标记,UITabBarButton 并没有采用 UIControl 的 selected 属性,而是在内部用了一个私有变量 _selected 来标记。

附上 Demo

特性

  • 不依赖 UITabBarControllerDelegate,通过对 UITabBarItem 进行扩展来实现
  • UITabBar 支持刷新动画 ,默认关闭动画
  • UITabBar 支持自定义刷新动画,需遵守XPTabBarRefreshViewAnimating协议,默认动画是UIActivityIndicatorView
  • 支持 iPhone/iPad,适配横竖屏

效果演示

gif

代码使用演示

// 根据项目实际场景获取UITabBarItem
UITabBarItem *tabBarItem =  self.tabBarItem; //self.navigationController.tabBarItem;
// 启用刷新动画
tabBarItem.enabledRefreshAnimation = YES;
// 自定义动画
tabBarItem.refreshView = [[CustomRefreshView alloc] init];
// 监听刷新回调
[tabBarItem setRefreshBlock:^(UITabBar *tabBar, UITabBarItem *tabBarItem) {
    // 发送网络请求刷新数据
    NSLog(@"Refresh");
    // 当网络请求完毕时记得调用 `stopRefresh` 方法
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
       [tabBarItem stopRefresh];
    });
}];

关于 UITabBarItem 的获取说明

  • 结构1
UITabBarController
│
├── UIViewController
│
├── UIViewController
│
└── UIViewController
  • 结构2
UINavigationController
│
└── UITabBarController
    │
    ├── UIViewController
    │
    ├── UIViewController
    │
    └── UIViewController

对于上图的结构1、2,直接使用 UIViewController.tabBarItem 即可。

  • 结构3
    UITabBarController
    │
    └── UINavigationController
      │
      ├── UIViewController
      │
      ├── UIViewController
      │
      └── UIViewController
    

对于上图的结构3,使用 UIViewController.navigationController.tabBarItem 即可。

源码

  • .h 文件
#import <UIKit/UIKit.h>

@protocol XPTabBarRefreshViewAnimating;
typedef void(^XPTabBarRefreshBlock)(UITabBar *tabBar, UITabBarItem *tabBarItem);


NS_CLASS_AVAILABLE_IOS(8_0) @interface UITabBarItem (XPTabBarRefresh)

/// 刷新回调
@property (nonatomic, copy) XPTabBarRefreshBlock refreshBlock;
/// 刷新动画视图, 默认`UIActivityIndicatorView`
@property (nonatomic, strong) UIView<XPTabBarRefreshViewAnimating> *refreshView;
/// 是否启用刷新动画, 默认`NO`
@property (nonatomic, assign, getter=isEnabledRefreshAnimation) BOOL enabledRefreshAnimation;

/// 停止刷新(请在`refreshBlock`回调中调用该方法)
- (void)stopRefresh;

@end


NS_CLASS_AVAILABLE_IOS(8_0) @interface UITabBar (XPTabBarRefresh)

@end


@protocol XPTabBarRefreshViewAnimating <NSObject>

/// 开始刷新动画
- (void)startRefreshAnimating;
/// 结束刷新动画
- (void)stopRefreshAnimating;

@end
  • .m 文件
#import "UITabBar+XPTabBarRefresh.h"
#import <objc/runtime.h>

@interface UITabBarItem (XPTabBarRefreshPrivate)

/// 是否正在刷新
@property (nonatomic, assign, getter=isRefreshing) BOOL refreshing;

/// 开始刷新
- (void)startRefresh;

@end

@implementation UITabBarItem (XPTabBarRefreshPrivate)

- (void)startRefresh {
    self.refreshing = YES;
    if (!self.isEnabledRefreshAnimation) {
        return;
    }
    UIView *containerView = (UIView *)[self valueForKey:@"_view"];
    if (containerView == nil || ![containerView isKindOfClass:UIView.class]) {
        return;
    }
    for (UIView *subview in containerView.subviews) {
        subview.hidden = YES;
    }
    UIView<XPTabBarRefreshViewAnimating> *animatingView = self.refreshView;
    [animatingView setUserInteractionEnabled:NO];
    [animatingView setTranslatesAutoresizingMaskIntoConstraints:NO];
    [containerView addSubview:animatingView];
    NSDictionary *views = @{@"view" : animatingView};
    [NSLayoutConstraint activateConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[view]|" options:0 metrics:nil views:views]];
    [NSLayoutConstraint activateConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[view]|" options:0 metrics:nil views:views]];
    [animatingView startRefreshAnimating];
}

- (void)stopRefresh {
    self.refreshing = NO;
    if (!self.isEnabledRefreshAnimation) {
        return;
    }
    UIView<XPTabBarRefreshViewAnimating> *animatingView = self.refreshView;
    for (UIView *subview in animatingView.superview.subviews) {
        subview.hidden = NO;
    }
    [animatingView stopRefreshAnimating];
    [animatingView removeFromSuperview];
}

- (void)setRefreshBlock:(XPTabBarRefreshBlock)refreshBlock {
    objc_setAssociatedObject(self, @selector(refreshBlock), refreshBlock, OBJC_ASSOCIATION_COPY);
}

- (XPTabBarRefreshBlock)refreshBlock {
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setRefreshView:(UIView<XPTabBarRefreshViewAnimating> *)refreshView {
    objc_setAssociatedObject(self, @selector(refreshView), refreshView, OBJC_ASSOCIATION_RETAIN);
}

- (UIView<XPTabBarRefreshViewAnimating> *)refreshView {
    UIView<XPTabBarRefreshViewAnimating> *view = objc_getAssociatedObject(self, _cmd);
    if (nil == view) {
        UIActivityIndicatorView *indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
        indicator.hidesWhenStopped = YES;
        view = (UIView<XPTabBarRefreshViewAnimating> *)indicator;
        [self setRefreshView:view];
    }
    return view;
}

- (void)setEnabledRefreshAnimation:(BOOL)enabledRefreshAnimation {
    objc_setAssociatedObject(self, @selector(isEnabledRefreshAnimation), @(enabledRefreshAnimation), OBJC_ASSOCIATION_RETAIN);
}

- (BOOL)isEnabledRefreshAnimation {
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}

- (void)setRefreshing:(BOOL)refreshing {
    objc_setAssociatedObject(self, @selector(isRefreshing), @(refreshing), OBJC_ASSOCIATION_RETAIN);
}

- (BOOL)isRefreshing {
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}

@end


#pragma mark -

@implementation UITabBar (XPTabBarRefresh)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalSelector = NSSelectorFromString(@"_buttonUp:");
        SEL swizllingSelector = @selector(xp_buttonUp:);
        Method originalMethod = class_getInstanceMethod(self, originalSelector);
        Method swizlledMethod = class_getInstanceMethod(self, swizllingSelector);
        BOOL flag = class_addMethod(self, originalSelector, method_getImplementation(swizlledMethod), method_getTypeEncoding(swizlledMethod));
        if (flag) {
            class_replaceMethod(self, swizllingSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizlledMethod);
        }
    });
}

- (void)xp_buttonUp:(UIControl *)sender {
    BOOL selected = [[sender valueForKey:@"_selected"] boolValue];
    if (selected) {
        UITabBarItem *item = self.selectedItem;
        XPTabBarRefreshBlock refreshBlock = [item refreshBlock];
        if (refreshBlock && !item.isRefreshing) {
            [item startRefresh];
            refreshBlock(self, item);
            return;
        }
    }
    [self xp_buttonUp:sender];
}

@end


#pragma mark -

@interface UIActivityIndicatorView (XPTabBarRefresh)<XPTabBarRefreshViewAnimating>

@end

@implementation UIActivityIndicatorView (XPTabBarRefresh)

- (void)startRefreshAnimating {
    [self startAnimating];
}

- (void)stopRefreshAnimating {
    [self stopAnimating];
}

@end

最后提一下,除了 runtime 之外,也可以通过 UITabBarControllerDelegate 代理实现,可以从 - (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController 这两个方法下手。

网上随便一搜都是基于 - (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController 方法的实现,这里就不再累赘了。