最近的需求自己造個(gè)輪子
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回調(diào)的曝光方法
/// @param indexPath 對(duì)應(yīng)的列表位置
- (void)exposureBuriedPointWithIndexPath:(NSIndexPath *)indexPath;
/// 提供給ScrollView 回調(diào)曝光的方法 使用時(shí)要注意scrollView上自帶的滾動(dòng)條
/// @param view 對(duì)應(yīng)的子視圖
- (void)exposureBuiedPointWithView:(UIView *)view;
/// 完全停止?jié)L動(dòng)的回調(diào)
/// @param scrollView 當(dāng)前的滾動(dòng)視圖
- (void)scrollViewDidEndScroll:(UIScrollView *)scrollView;
@end
@interface UIScrollView (Exposure)
@property (nonatomic, weak) id<YBMExposureDelegate> exposureDelegate;
/// 設(shè)置滾動(dòng)方向 默認(rèn)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;
/// 設(shè)置滾動(dòng)方向 默認(rèn)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 - 用來弱引用動(dòng)態(tài)綁定的代理類
@interface YBMExposureDelegateModel: NSObject
@property (nonatomic, weak) id<YBMExposureDelegate> delegate;
@end
@implementation YBMExposureDelegateModel
@end
#pragma mark - 用來緩存已經(jīng)上報(bào)的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 設(shè)置代理的方法
SEL originSelector = @selector(setDelegate:);
SEL swizzledSelector = @selector(swizzled_setDelegate:);
SwizzlingMethod(self, originSelector, swizzledSelector);
}
- (void)swizzled_setDelegate:(id<UIScrollViewDelegate>)delegate {
[self swizzled_setDelegate: delegate];
/// 如果當(dāng)前類 是tableview 或其子類 并且準(zhǔn)守 曝光代理 則進(jìn)行hook方法 檢測(cè)滾動(dòng)停止及刷新方法
if ([self isKindOfClass:[UIScrollView class]] && [delegate conformsToProtocol:@protocol(YBMExposureDelegate)]) {
[self hookScrollEndMethod];
}
}
#pragma mark - hook滾動(dòng)停止方法
- (void)hookScrollEndMethod {
/// hook 自然滾動(dòng)停止
[self hookEndDecelerating];
/// hook 手勢(shì)介入停止
[self hookEndDragging];
/// hook 滾動(dòng)到頂部
[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 - 監(jiān)聽滾動(dòng)停止
- (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 - 動(dòng)態(tài)綁定代理
- (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滾動(dòng)停止方法
- (void)hookScrollEndMethod {
[super hookScrollEndMethod];
/// 設(shè)置緩存屬性
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 - 獲取上一次上報(bào)的位置信息,供外部使用
- (NSArray<NSIndexPath *> *)getLastVisibleIndexPaths {
return self.cacheIndexs.curExposureIndexPaths;
}
#pragma mark - 重新計(jì)算當(dāng)前區(qū)域曝光的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 - 監(jiān)聽滾動(dòng)停止
- (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 {
/// 重復(fù)上報(bào)控制
if (![self.cacheIndexs.historyIndexPaths containsObject:indexPath]) {
if (self.exposureDelegate && [self.exposureDelegate respondsToSelector:@selector(exposureBuriedPointWithIndexPath:)]) {
[self.exposureDelegate exposureBuriedPointWithIndexPath:indexPath];
}
}
[self.cacheIndexs.curExposureIndexPaths addObject:indexPath];
}
/// 計(jì)算tableview的總行數(shù)
- (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個(gè)數(shù)
/// 刷新后item個(gè)數(shù)增加 則判定為加載更多克婶,此時(shí)不做重置處理。 如果刷新后item個(gè)數(shù)減少或不變則判定為刷新阱表,重置上次曝光的位置信息
/// 記錄item數(shù)量
NSInteger curNumber = [self calculationTotalNumber];
/// 調(diào)用原方法
[self swizzled_Reload];
dispatch_async(dispatch_get_main_queue(), ^{
NSInteger endNumber = [self calculationTotalNumber];
if (endNumber <= curNumber) {
[self.cacheIndexs.curExposureIndexPaths removeAllObjects];
}
/// 刷新結(jié)束后 觸發(fā)一次上報(bào)
[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滾動(dòng)停止方法
- (void)hookScrollEndMethod {
[super hookScrollEndMethod];
/// 設(shè)置緩存屬性
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 - 獲取當(dāng)前可見的IndexPath 供外部使用
- (NSArray<NSIndexPath *> *)getLastVisibleIndexPaths {
return self.cacheIndexs.curExposureIndexPaths;
}
#pragma mark - 重新計(jì)算當(dāng)前區(qū)域曝光的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 - 監(jiān)聽滾動(dòng)停止
- (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 {
/// 重復(fù)上報(bào)控制
if (![self.cacheIndexs.historyIndexPaths containsObject:indexPath]) {
if (self.exposureDelegate && [self.exposureDelegate respondsToSelector:@selector(exposureBuriedPointWithIndexPath:)]) {
[self.exposureDelegate exposureBuriedPointWithIndexPath:indexPath];
}
}
[self.cacheIndexs.curExposureIndexPaths addObject:indexPath];
}
/// 計(jì)算CollectionView的總行數(shù)
- (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個(gè)數(shù)
/// 刷新后item個(gè)數(shù)增加 則判定為加載更多淹禾,此時(shí)不做重置處理。 如果刷新后item個(gè)數(shù)減少或不變則判定為刷新茴扁,重置上次曝光的位置信息
/// 記錄item數(shù)量
NSInteger curNumber = [self calculationTotalNumber];
/// 調(diào)用原方法
[self swizzled_reload];
dispatch_async(dispatch_get_main_queue(), ^{
NSInteger endNumber = [self calculationTotalNumber];
if (endNumber <= curNumber) {
[self.cacheIndexs.curExposureIndexPaths removeAllObjects];
}
/// 刷新結(jié)束后 觸發(fā)一次上報(bào)
[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