最近的需求自己造个轮子
Lits+Exposure.h
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef enum : NSUInteger {
YBMExposureScrollDirectionVertical = 0,
YBMExposureScrollDirectionHorizontal = 1,
} YBMExposureScrollDirection;
@protocol YBMExposureDelegate <NSObject>
@optional
/// 提供给TableView / CollectionView回调的曝光方法
/// @param indexPath 对应的列表位置
- (void)exposureBuriedPointWithIndexPath:(NSIndexPath *)indexPath;
/// 提供给ScrollView 回调曝光的方法 使用时要注意scrollView上自带的滚动条
/// @param view 对应的子视图
- (void)exposureBuiedPointWithView:(UIView *)view;
/// 完全停止滚动的回调
/// @param scrollView 当前的滚动视图
- (void)scrollViewDidEndScroll:(UIScrollView *)scrollView;
@end
@interface UIScrollView (Exposure)
@property (nonatomic, weak) id<YBMExposureDelegate> exposureDelegate;
/// 设置滚动方向 默认YBMExposureScrollDirectionVertical
@property (nonatomic, assign) YBMExposureScrollDirection direction;
@end
@interface UITableView (Exposure)
@property (nonatomic, weak) id<YBMExposureDelegate> exposureDelegate;
@property (nonatomic, readonly, getter=getLastVisibleIndexPaths , copy) NSArray<NSIndexPath *> * lastVisibleIndexPaths;
@property (nonatomic, readonly, getter=getCalculateExposureIndexPaths , copy) NSArray<NSIndexPath *> * calculateExposureIndexPaths;
@end
@interface UICollectionView (Exposure)
@property (nonatomic, weak) id<YBMExposureDelegate> exposureDelegate;
@property (nonatomic, readonly, getter=getLastVisibleIndexPaths , copy) NSArray<NSIndexPath *> * lastVisibleIndexPaths;
@property (nonatomic, readonly, getter=getCalculateExposureIndexPaths , copy) NSArray<NSIndexPath *> * calculateExposureIndexPaths;
/// 设置滚动方向 默认YBMExposureScrollDirectionVertical
@property (nonatomic, assign) YBMExposureScrollDirection direction;
@end
NS_ASSUME_NONNULL_END
Lits+Exposure.m
#import "List+Exposure.h"
#import <objc/runtime.h>
static const int TableCacheIndexsKey;
static const int CollectionCacheIndexsKey;
static const int ScrollExposureDelegateKey;
static const int ScrollExposureDirection;
// 露出曝光 百分比
CGFloat const ExposurePercentage = 0.8;
void SwizzlingMethod(Class cls, SEL originSEL, SEL swizzledSEL) {
Method originMethod = class_getInstanceMethod(cls, originSEL);
Method swizzledMethod = nil;
if (!originMethod) {
originMethod = class_getClassMethod(cls, originSEL);
if (!originMethod) {
return;
}
swizzledMethod = class_getClassMethod(cls, swizzledSEL);
if (!swizzledMethod) {
return;
}
} else {
swizzledMethod = class_getInstanceMethod(cls, swizzledSEL);
if (!swizzledMethod) {
return;
}
}
if (class_addMethod(cls, originSEL, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
class_replaceMethod(cls, swizzledSEL, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
} else {
method_exchangeImplementations(originMethod, swizzledMethod);
}
}
void SwizzlingDelegateMethod(Class delegateClass, Class listClass, SEL originSEL, SEL swizzledSEL) {
Method originMethod = class_getInstanceMethod(delegateClass, originSEL);
Method swizzledMethod = class_getInstanceMethod(listClass, swizzledSEL);
class_addMethod(delegateClass, swizzledSEL, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
BOOL c = class_addMethod(delegateClass, originSEL, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (!c) {
method_exchangeImplementations(originMethod, swizzledMethod);
}
}
@interface UIView (Window)
@end
@implementation UIView (Window)
- (UIWindow *)lastWindow{
NSArray *windows = [UIApplication sharedApplication].windows;
for (UIWindow *window in [windows reverseObjectEnumerator]) {
if ([window isKindOfClass:[UIWindow class]] && CGRectEqualToRect(window.bounds, [UIScreen mainScreen].bounds)) {
return window;
}
}
return windows.lastObject;
}
@end
#pragma mark - 用来弱引用动态绑定的代理类
@interface YBMExposureDelegateModel: NSObject
@property (nonatomic, weak) id<YBMExposureDelegate> delegate;
@end
@implementation YBMExposureDelegateModel
@end
#pragma mark - 用来缓存已经上报的indexpaths
@interface YBMExposureCacheIndexPaths: NSObject
@property (nonatomic, strong) NSMutableArray<NSIndexPath *> * curExposureIndexPaths;
@property (nonatomic, strong) NSMutableArray<NSIndexPath *> * historyIndexPaths;
@end
@implementation YBMExposureCacheIndexPaths
- (NSMutableArray<NSIndexPath *> *)curExposureIndexPaths {
if (!_curExposureIndexPaths) {
_curExposureIndexPaths = [NSMutableArray array];
}
return _curExposureIndexPaths;
}
- (NSMutableArray<NSIndexPath *> *)historyIndexPaths {
if (!_historyIndexPaths) {
_historyIndexPaths = [NSMutableArray array];
}
return _historyIndexPaths;
}
@end
@implementation UIScrollView (Exposure)
+ (void)load {
/// hook 设置代理的方法
SEL originSelector = @selector(setDelegate:);
SEL swizzledSelector = @selector(swizzled_setDelegate:);
SwizzlingMethod(self, originSelector, swizzledSelector);
}
- (void)swizzled_setDelegate:(id<UIScrollViewDelegate>)delegate {
[self swizzled_setDelegate: delegate];
/// 如果当前类 是tableview 或其子类 并且准守 曝光代理 则进行hook方法 检测滚动停止及刷新方法
if ([self isKindOfClass:[UIScrollView class]] && [delegate conformsToProtocol:@protocol(YBMExposureDelegate)]) {
[self hookScrollEndMethod];
}
}
#pragma mark - hook滚动停止方法
- (void)hookScrollEndMethod {
/// hook 自然滚动停止
[self hookEndDecelerating];
/// hook 手势介入停止
[self hookEndDragging];
/// hook 滚动到顶部
[self hookScrollTop];
}
- (void)hookEndDecelerating {
SEL originSelector = @selector(scrollViewDidEndDecelerating:);
SEL swizzledSelector = @selector(swizzled_scrollViewDidEndDecelerating:);
SwizzlingDelegateMethod([self.delegate class], [self class], originSelector, swizzledSelector);
}
- (void)hookEndDragging {
SEL originSelector = @selector(scrollViewDidEndDragging:willDecelerate:);
SEL swizzledSelector = @selector(swizzled_scrollViewDidEndDragging:willDecelerate:);
SwizzlingDelegateMethod([self.delegate class], [self class], originSelector, swizzledSelector);
}
- (void)hookScrollTop {
SEL originSelector = @selector(scrollViewDidScrollToTop:);
SEL swizzledSelector = @selector(swizzled_scrollViewDidScrollToTop:);
SwizzlingDelegateMethod([self.delegate class], [self class], originSelector, swizzledSelector);
}
#pragma mark - 监听滚动停止
- (void)swizzled_scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
if ([self respondsToSelector:@selector(swizzled_scrollViewDidEndDecelerating:)]) {
[self swizzled_scrollViewDidEndDecelerating:scrollView];
}
BOOL scrollToScrollStop = !scrollView.tracking && !scrollView.dragging && !scrollView.decelerating;
if (scrollToScrollStop) {
[scrollView scrollViewDidEndScroll];
}
}
- (void)swizzled_scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if ([self respondsToSelector:@selector(swizzled_scrollViewDidEndDragging:willDecelerate:)]) {
[self swizzled_scrollViewDidEndDragging:scrollView willDecelerate:decelerate];
}
if (!decelerate) {
BOOL dragToDragStop = scrollView.tracking && !scrollView.dragging && !scrollView.decelerating;
if (dragToDragStop) {
[scrollView scrollViewDidEndScroll];
}
}
}
- (void)swizzled_scrollViewDidScrollToTop:(UIScrollView *)scrollView {
if ([self respondsToSelector:@selector(swizzled_scrollViewDidScrollToTop:)]) {
[self swizzled_scrollViewDidScrollToTop: scrollView];
}
[scrollView scrollViewDidEndScroll];
}
- (void)scrollViewDidEndScroll {
if (self.exposureDelegate && [self.exposureDelegate respondsToSelector:@selector(scrollViewDidEndScroll:)]) {
[self.exposureDelegate scrollViewDidEndScroll:self];
}
for (UIView * view in self.subviews.objectEnumerator) {
if (view.isHidden == NO && view.alpha > 0) {
CGRect previousCellRect = view.frame;
UIWindow * window = [self lastWindow];
CGRect convertRect = [self convertRect:previousCellRect toView:window];
CGRect scrollRect = CGRectIntersection([self.superview convertRect:self.frame toView:window], window.bounds);
if (CGRectContainsRect(scrollRect, convertRect)) {
if (self.exposureDelegate && [self.exposureDelegate respondsToSelector:@selector(exposureBuiedPointWithView:)]) {
[self.exposureDelegate exposureBuiedPointWithView:view];
}
}
}
}
}
#pragma mark - 动态绑定代理
- (id<YBMExposureDelegate>)exposureDelegate {
YBMExposureDelegateModel * model = objc_getAssociatedObject(self, &ScrollExposureDelegateKey);
return model.delegate;
}
- (void)setExposureDelegate:(id<YBMExposureDelegate>)exposureDelegate {
YBMExposureDelegateModel * model = [[YBMExposureDelegateModel alloc] init];
model.delegate = exposureDelegate;
objc_setAssociatedObject(self, &ScrollExposureDelegateKey, model, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
#pragma mark - 绑定属性
- (YBMExposureScrollDirection)direction {
NSNumber * number = objc_getAssociatedObject(self, &ScrollExposureDirection);
YBMExposureScrollDirection direction = [number unsignedIntegerValue];
return direction;
}
- (void)setDirection:(YBMExposureScrollDirection)direction {
NSNumber * number = [NSNumber numberWithUnsignedInteger:direction];
objc_setAssociatedObject(self, &ScrollExposureDirection, number, OBJC_ASSOCIATION_ASSIGN);
}
@end
#pragma mark - UITableView
@interface UITableView()
@property (nonatomic, strong) YBMExposureCacheIndexPaths * cacheIndexs;
@end
@implementation UITableView (Exposure)
#pragma mark - hook滚动停止方法
- (void)hookScrollEndMethod {
[super hookScrollEndMethod];
/// 设置缓存属性
self.cacheIndexs = [[YBMExposureCacheIndexPaths alloc] init];
/// hook Reload 方法
[self hookReload];
}
- (void)hookReload {
SEL originSelector = @selector(reloadData);
SEL swizzledSelector = @selector(swizzled_Reload);
SwizzlingMethod([self class], originSelector, swizzledSelector);
}
#pragma mark - 获取上一次上报的位置信息,供外部使用
- (NSArray<NSIndexPath *> *)getLastVisibleIndexPaths {
return self.cacheIndexs.curExposureIndexPaths;
}
#pragma mark - 重新计算当前区域曝光的IndexPath
- (NSArray<NSIndexPath *> *)getCalculateExposureIndexPaths {
__block NSMutableArray * array = [NSMutableArray array];
NSArray<NSIndexPath *> * indexPathsForVisibleRows = self.indexPathsForVisibleRows;
[indexPathsForVisibleRows enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([self calculateExposureForIndexPath:obj]) {
[array addObject:obj];
}
}];
return array;
}
#pragma mark - 监听滚动停止
- (void)scrollViewDidEndScroll {
if (self.exposureDelegate && [self.exposureDelegate respondsToSelector:@selector(scrollViewDidEndScroll:)]) {
[self.exposureDelegate scrollViewDidEndScroll:self];
}
NSLog(@"分割线 -----------**************----------- 分割线");
self.cacheIndexs.historyIndexPaths = [self.cacheIndexs.curExposureIndexPaths copy];
[self.cacheIndexs.curExposureIndexPaths removeAllObjects];
NSArray<NSIndexPath *> * indexPathsForVisibleRows = self.indexPathsForVisibleRows;
[indexPathsForVisibleRows enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[self exposureBuriedPoint: obj];
}];
}
#pragma mark - 曝光位置判断
- (void)exposureBuriedPoint:(NSIndexPath *)indexPath {
BOOL isExposure = [self calculateExposureForIndexPath: indexPath];
if (isExposure) {
[self callDelegateExposure:indexPath];
}
}
- (BOOL)calculateExposureForIndexPath:(NSIndexPath *)indexPath {
CGRect previousCellRect = [self rectForRowAtIndexPath:indexPath];
UIWindow * window = [self lastWindow];
CGRect convertRect = [self convertRect:previousCellRect toView:window];
CGRect tabRect = CGRectIntersection([self.superview convertRect:self.frame toView:window], window.bounds);
CGFloat currentTop = CGRectGetMinY(convertRect) - CGRectGetMinY(tabRect);
if (currentTop < 0) {
CGFloat percentage = (convertRect.size.height + currentTop) / convertRect.size.height;
if (percentage >= ExposurePercentage) {
return YES;
}
} else {
CGFloat currentBottom = CGRectGetMaxY(tabRect) - CGRectGetMaxY(convertRect);
if (currentBottom < 0) {
CGFloat percentage = (convertRect.size.height + currentBottom) / convertRect.size.height;
if (percentage >= ExposurePercentage) {
return YES;
}
} else {
return YES;
}
}
return NO;
}
- (void)callDelegateExposure:(NSIndexPath *)indexPath {
/// 重复上报控制
if (![self.cacheIndexs.historyIndexPaths containsObject:indexPath]) {
if (self.exposureDelegate && [self.exposureDelegate respondsToSelector:@selector(exposureBuriedPointWithIndexPath:)]) {
[self.exposureDelegate exposureBuriedPointWithIndexPath:indexPath];
}
}
[self.cacheIndexs.curExposureIndexPaths addObject:indexPath];
}
/// 计算tableview的总行数
- (NSInteger)calculationTotalNumber {
NSInteger totalNumber = 0;
NSInteger sections = self.numberOfSections;
for (NSInteger section = 0; section < sections; section ++) {
totalNumber += [self numberOfRowsInSection:section];
}
return totalNumber;
}
#pragma mark - 刷新方法
- (void)swizzled_Reload {
/// 记录页面Item个数
/// 刷新后item个数增加 则判定为加载更多,此时不做重置处理。 如果刷新后item个数减少或不变则判定为刷新,重置上次曝光的位置信息
/// 记录item数量
NSInteger curNumber = [self calculationTotalNumber];
/// 调用原方法
[self swizzled_Reload];
dispatch_async(dispatch_get_main_queue(), ^{
NSInteger endNumber = [self calculationTotalNumber];
if (endNumber <= curNumber) {
[self.cacheIndexs.curExposureIndexPaths removeAllObjects];
}
/// 刷新结束后 触发一次上报
[self scrollViewDidEndScroll];
});
}
#pragma mark - 绑定属性
- (YBMExposureCacheIndexPaths *)cacheIndexs {
YBMExposureCacheIndexPaths * cache = objc_getAssociatedObject(self, &TableCacheIndexsKey);
return cache;
}
- (void)setCacheIndexs:(YBMExposureCacheIndexPaths *)cacheIndexs {
objc_setAssociatedObject(self, &TableCacheIndexsKey, cacheIndexs, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
#pragma mark - UICollectionView
@interface UICollectionView()
@property (nonatomic, strong) YBMExposureCacheIndexPaths * cacheIndexs;
@end
@implementation UICollectionView (Exposure)
#pragma mark - hook滚动停止方法
- (void)hookScrollEndMethod {
[super hookScrollEndMethod];
/// 设置缓存属性
self.cacheIndexs = [[YBMExposureCacheIndexPaths alloc] init];
/// hook Reload 方法
[self hookReload];
}
- (void)hookReload {
SEL originSelector = @selector(reloadData);
SEL swizzledSelector = @selector(swizzled_reload);
SwizzlingMethod([self class], originSelector, swizzledSelector);
}
#pragma mark - 获取当前可见的IndexPath 供外部使用
- (NSArray<NSIndexPath *> *)getLastVisibleIndexPaths {
return self.cacheIndexs.curExposureIndexPaths;
}
#pragma mark - 重新计算当前区域曝光的IndexPath
- (NSArray<NSIndexPath *> *)getCalculateExposureIndexPaths {
__block NSMutableArray * array = [NSMutableArray array];
NSArray<NSIndexPath *> * indexPathsForVisibleRows = self.indexPathsForVisibleItems;
[indexPathsForVisibleRows enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([self calculateExposureForIndexPath:obj]) {
[array addObject:obj];
}
}];
return array;
}
#pragma mark - 监听滚动停止
- (void)scrollViewDidEndScroll {
if (self.exposureDelegate && [self.exposureDelegate respondsToSelector:@selector(scrollViewDidEndScroll:)]) {
[self.exposureDelegate scrollViewDidEndScroll:self];
}
NSLog(@"分割线 -----------**************----------- 分割线");
self.cacheIndexs.historyIndexPaths = [self.cacheIndexs.curExposureIndexPaths copy];
[self.cacheIndexs.curExposureIndexPaths removeAllObjects];
NSArray<NSIndexPath *> * indexPathsForVisibleRows = self.indexPathsForVisibleItems;
[indexPathsForVisibleRows enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[self exposureBuriedPoint: obj];
}];
}
#pragma mark - 曝光位置判断
- (void)exposureBuriedPoint:(NSIndexPath *)indexPath {
BOOL isExposure = [self calculateExposureForIndexPath: indexPath];
if (isExposure) {
[self callDelegateExposure:indexPath];
}
}
- (BOOL)calculateExposureForIndexPath:(NSIndexPath *)indexPath {
CGRect previousCellRect = [self.collectionViewLayout layoutAttributesForItemAtIndexPath:indexPath].frame;
UIWindow * window = [self lastWindow];
CGRect convertRect = [self convertRect:previousCellRect toView:window];
CGRect tabRect = CGRectIntersection([self.superview convertRect:self.frame toView:window], window.bounds);
if (self.direction == YBMExposureScrollDirectionVertical) {
CGFloat currentTop = CGRectGetMinY(convertRect) - CGRectGetMinY(tabRect);
if (currentTop < 0) {
CGFloat percentage = (convertRect.size.height + currentTop) / convertRect.size.height;
if (percentage >= ExposurePercentage) {
return YES;
}
} else {
CGFloat currentBottom = CGRectGetMaxY(tabRect) - CGRectGetMaxY(convertRect);
if (currentBottom < 0) {
CGFloat percentage = (convertRect.size.height + currentBottom) / convertRect.size.height;
if (percentage >= ExposurePercentage) {
return YES;
}
} else {
return YES;
}
}
} else {
CGFloat currentLeft = CGRectGetMinX(convertRect) - CGRectGetMinX(tabRect);
if (currentLeft < 0) {
CGFloat percentage = (convertRect.size.width + currentLeft) / convertRect.size.width;
if (percentage >= ExposurePercentage) {
return YES;
}
} else {
CGFloat currentRight = CGRectGetMaxX(tabRect) - CGRectGetMaxX(convertRect);
if (currentRight < 0) {
CGFloat percentage = (convertRect.size.width + currentRight) / convertRect.size.width;
if (percentage >= ExposurePercentage) {
return YES;
}
} else {
return YES;
}
}
}
return NO;
}
- (void)callDelegateExposure:(NSIndexPath *)indexPath {
/// 重复上报控制
if (![self.cacheIndexs.historyIndexPaths containsObject:indexPath]) {
if (self.exposureDelegate && [self.exposureDelegate respondsToSelector:@selector(exposureBuriedPointWithIndexPath:)]) {
[self.exposureDelegate exposureBuriedPointWithIndexPath:indexPath];
}
}
[self.cacheIndexs.curExposureIndexPaths addObject:indexPath];
}
/// 计算CollectionView的总行数
- (NSInteger)calculationTotalNumber {
NSInteger totalNumber = 0;
if (self.dataSource != nil) {
NSInteger sections = [self.dataSource numberOfSectionsInCollectionView:self];
for (NSInteger section = 0; section < sections; section ++) {
totalNumber += [self.dataSource collectionView:self numberOfItemsInSection:section];
}
}
return totalNumber;
}
#pragma mark - 刷新方法
- (void)swizzled_reload {
/// 记录页面Item个数
/// 刷新后item个数增加 则判定为加载更多,此时不做重置处理。 如果刷新后item个数减少或不变则判定为刷新,重置上次曝光的位置信息
/// 记录item数量
NSInteger curNumber = [self calculationTotalNumber];
/// 调用原方法
[self swizzled_reload];
dispatch_async(dispatch_get_main_queue(), ^{
NSInteger endNumber = [self calculationTotalNumber];
if (endNumber <= curNumber) {
[self.cacheIndexs.curExposureIndexPaths removeAllObjects];
}
/// 刷新结束后 触发一次上报
[self scrollViewDidEndScroll];
});
}
#pragma mark - 绑定属性
- (YBMExposureCacheIndexPaths *)cacheIndexs {
YBMExposureCacheIndexPaths * cache = objc_getAssociatedObject(self, &CollectionCacheIndexsKey);
return cache;
}
- (void)setCacheIndexs:(YBMExposureCacheIndexPaths *)cacheIndexs {
objc_setAssociatedObject(self, &CollectionCacheIndexsKey, cacheIndexs, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
网友评论