功能类似于半糖首页菜单与QQ音乐歌曲列表页面。即支持UITableview的上下滚动,同时也支持不同列表之间的滑动切换。同时可以设置顶部header view与列表切换功能bar,使用方式类似于原生UITableview的tableHeaderView的方式。
SwipeTableView 可在 CocoaPods 上使用。将以下内容添加到您的 Podfile 中:
pod 'SwipeTableView'
为了兼容下拉刷新,采用了两种实现方式,但基本构造都是一样的
使用 UICollectionView
作为 item 的载体,实现左右滑动的功能。
在支持左右滑动之后,最关键的问题就是滑动后相邻 item 的对齐问题。
为实现前后 item 对齐,需要在 item 视图重用的时候,比较前后两个 item 视图的 contentOffset,然后设置后一个 item 视图的 contentOffset 与前一个相同。这样就实现了滑动后前后 item 视图的 offset 是对齐的。
由于多个 item 共用一个 header 与 bar,所以,header 与 bar 必须是根视图的子视图,即与 CollectionView 一样是 SwipeTableView
的子视图,并且在 CollectionView 的图层之上。
header & bar 的滚动与悬停实现是,对当前 item 视图的 contentOffset 进行 KVO。然后在当前 item 视图的 contentOffset 改变时,去改变 header 与 bar 的 Y 坐标值。
顶部 header & bar 在图层的最顶部,所以每个 item 视图的顶部需要做出一个留白来作为 header & bar 的显示空间。在 Model 1
中,采用修改 UIScrollView
的 contentInsets 的 top 值来留出顶部离白。
由于 header 在图层的最顶部,所以要实现滑动 header 的同时使当前 item 视图跟随滚动,需要根据 header 的 frame 的变化回调给当前的 item 视图来改变 contentOffset,同时也要具有 ScrollView 的弹性等效果。
这里采用
UIKit Dynamic
物理动画引擎自定义STHeaderView
来实现自定义UIScrollView
效果解决上述问题参考文章
英文博客
。
Model 2
中,基本结构与 Model 1
一样,唯一的不同在于每个 item 视图顶部留白的的方式来提供顶部的留白,CollectionView 采用自定义 STCollectionView
的 collectionHeaderView
来实现留白。(目前不支持 UIScrollView
)Model 1
与 Model 2
模式? > 正常情况下即为 Model 1
模式;在 SwipeTableView.h
中或者在工程 PCH 文件中设置宏 #define ST_PULLTOREFRESH_HEADER_HEIGHT xx
设置为 Model 2
模式。实现 SwipeTableViewDataSource
代理的两个方法:
- (NSInteger)numberOfItemsInSwipeTableView:(SwipeTableView *)swipeView
返回列表 item 的个数
- (UIScrollView *)swipeTableView:(SwipeTableView *)swipeView viewForItemAtIndex:(NSInteger)index reusingView:(UIScrollView *)view
返回对应 index 下的 item 视图,返回的视图类型需要是
UIScrollView
及其子类:UITableView
或者UICollectionView
。这里采用重用机制,需要根据 reusingView 来创建单一的 item 视图。
使用的 swipeHeaderView
必须是 STHeaderView
及其子类的实例。
1. 一行代码即可支持常用的下拉刷新控件,只需要在项目的PCH文件中或者SwipeTableView.h
文件中设置如下的宏:
#define ST_PULLTOREFRESH_HEADER_HEIGHT xx
上述宏中的
xx
应与您所使用的第三方下拉刷新控件的refreshHeader高度相同:
MJRefresh
为MJRefreshHeaderHeight
,SVPullToRefresh
为SVPullToRefreshViewHeight
(注:此时视图结构为Model 2
)
新增下拉刷新代理可以控制每个item的下拉临界高度,并可以自由控制每个item是否支持下拉刷新
- (BOOL)swipeTableView:(SwipeTableView *)swipeTableView shouldPullToRefreshAtIndex:(NSInteger)index
根据item所在index设置item是否支持下拉刷新。在设置
#define ST_PULLTOREFRESH_HEADER_HEIGHT xx
时默认为YES(全部支持),否则默认为NO。
- (CGFloat)swipeTableView:(SwipeTableView *)swipeTableView heightForRefreshHeaderAtIndex:(NSInteger)index
返回对应item下拉刷新的临界高度,如果没有实现此代理,在设置
#define ST_PULLTOREFRESH_HEADER_HEIGHT xx
时默认是ST_PULLTOREFRESH_HEADER_HEIGHT
的高度。如果没有设置宏,并且想要自定义修改下拉刷新,必须实现此代理,提供下拉刷新控件RefreshHeader的高度(RefreshHeader全部露出的高度),来通知SwipeTableView
触发下拉刷新。
2. 如果想要更好的扩展性,以及喜欢自行研究的学生,可以尝试修改或自定义下拉控件来解决下拉刷新的兼容问题,同时这里提供一些思路:
如果下拉刷新控件的frame是固定的(比如header的frame),这样可以在初始化下拉刷新的header或者在数据源的代理中重设下拉header的frame。
获取下拉刷新的header,将header的frame的y值减去
swipeHeaderView
与swipeHeaderBar
的高度和(或者重写RefreshHeader的setFrame方法),就可以消除itemView contentInsets顶部留白top值的影响(否则添加的下拉header是隐藏在底部的)。
- (UIScrollView *)swipeTableView:(SwipeTableView *)swipeView viewForItemAtIndex:(NSInteger)index reusingView:(UIScrollView *)view {
...
STRefreshHeader * header = scrollView.header;
header.y = - (header.height + (swipeHeaderView.height + swipeHeaderBar.height));
...
}
or
- (instancetype)initWithFrame:(CGRect)frame {
...
STRefreshHeader * header = [STRefreshHeader headerWithRefreshingBlock:^(STRefreshHeader *header) {
}];
header.y = - (header.height + (swipeHeaderView.height + swipeHeaderBar.height));
scrollView.header = header;
...
}
对于一些下拉刷新控件,RefreshHeader的frame设置可能会在layoutSubviews
中,所以,对RefreshHeader frame的修改需要在执行完layouSubviews
之后,在有效的操作中进行,例如:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
STRefreshHeader * header = self.header;
CGFloat orginY = - (header.height + self.swipeTableView.swipeHeaderView.height + self.swipeTableView.swipeHeaderBar.height);
if (header.y != orginY) {
header.y = orginY;
}
}
如何判断下拉刷新控件的frame是否固定不变呢?
一是可以研究源码查看RefreshHeader的frame是否固定不变;另一个简单的方式是在ScrollView的滚动代理中log RefreshHeader的frame(大部分下拉控件的frame都是固定的)。
如果使用的下拉刷新控件的frame是变化的(个人感觉极少数),那么只能进行更深层地修改下拉刷新控件或自定义下拉刷新。也可以直接采用第一种设置宏的方法支持下拉刷新。
在Model 1
模式下,属于最基本的模式,可扩展性也是最强的,此时支持UITableView
、UICollectionView
、UIScrollView
。如果同时设置shouldAdjustContentSize
为YES,实现自适应contentSize,在UICollectionView
内容不足时只能使用STCollectionView
及其子类
UICollectionView
不支持通过contentSize属性设置contentSize。
在Model 2
模式下,SwipeTableView
支持的collectionView必须是STCollectionView
及其子类的实例,目前不支持UIScrollView
。
self.swipeTableView = [[SwipeTableView alloc]initWithFrame:[UIScreen mainScreen].bounds];
_swipeTableView.delegate = self;
_swipeTableView.dataSource = self;
_swipeTableView.shouldAdjustContentSize = YES;
_swipeTableView.swipeHeaderView = self.tableViewHeader;
_swipeTableView.swipeHeaderBar = self.segmentBar;
- (NSInteger)numberOfItemsInSwipeTableView:(SwipeTableView *)swipeView {
return 4;
}
- (UIScrollView *)swipeTableView:(SwipeTableView *)swipeView viewForItemAtIndex:(NSInteger)index reusingView:(UIScrollView *)view {
UITableView * tableView = view;
if (nil == tableView) {
UITableView * tableView = [[UITableView alloc]initWithFrame:swipeView.bounds style:UITableViewStylePlain];
tableView.backgroundColor = [UIColor whiteColor];
...
}
// 这里刷新每个item的数据
[tableVeiw refreshWithData:dataArray];
...
return tableView;
}
STCollectionView
使用方法:MyCollectionView.h
@interface MyCollectionView : STCollectionView
@property (nonatomic, assign) NSInteger numberOfItems;
@property (nonatomic, assign) BOOL isWaterFlow;
@end
MyCollectionView.m
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
STCollectionViewFlowLayout * layout = self.st_collectionViewLayout;
layout.minimumInteritemSpacing = 5;
layout.minimumLineSpacing = 5;
layout.sectionInset = UIEdgeInsetsMake(5, 5, 5, 5);
self.stDelegate = self;
self.stDataSource = self;
[self registerClass:UICollectionViewCell.class forCellWithReuseIdentifier:@"item"];
[self registerClass:UICollectionReusableView.class forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"header"];
[self registerClass:UICollectionReusableView.class forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"footer"];
}
return self;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView layout:(STCollectionViewFlowLayout *)layout numberOfColumnsInSection:(NSInteger)section {
return _numberOfColumns;
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return CGSizeMake(0, 100);
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section {
return CGSizeMake(kScreenWidth, 35);
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section {
return CGSizeMake(kScreenWidth, 35);
}
- (UICollectionReusableView *)stCollectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
UICollectionReusableView * reusableView = nil;
if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {
reusableView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@"header" forIndexPath:indexPath];
// custom UI......
}else if ([kind isEqualToString:UICollectionElementKindSectionFooter]) {
reusableView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@"footer" forIndexPath:indexPath];
// custom UI......
}
return reusableView;
}
- (NSInteger)numberOfSectionsInStCollectionView:(UICollectionView *)collectionView {
return _numberOfSections;
}
- (NSInteger)stCollectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return _numberOfItems;
}
- (UICollectionViewCell *)stCollectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"item" forIndexPath:indexPath];
// do something .......
return cell;
}
如果STCollectionViewFlowLayout
已经不能满足UICollectionView
的布局需求,用户自定义的flowlayout
需要继承自STCollectionViewFlowLayout
,并在重写相应方法时需要调用父类方法,并需要遵循一定的规则,如下:
- (void)prepareLayout {
[super prepareLayout];
// do something in sub class......
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray * superAttrs = [super layoutAttributesForElementsInRect:rect];
NSMutableArray * itemAttrs = [superAttrs mutableCopy];
// filter subClassAttrs to rect
NSArray * filteredSubClassAttrs = ........;
[itemAttrs addObjectsFromArray:fittesSubClassAttrs];
return itemAttrs;
}
- (CGSize)collectionViewContentSize {
CGSize superSize = [super collectionViewContentSize];
CGSize subClassSize = .......;
subClassSize.height += superSize.height;
// fit mincontentSize
STCollectionView * collectionView = (STCollectionView *)self.collectionView;
subClassSize.height = fmax(subClassSize.height, collectionView.minRequireContentSize.height);
return subClassSize;
}
SingleOneKindView
数据源提供的是单一类型的itemView,这里默认提供的是 CustomTableView
(UITableView
的子类),并且每一个itemView的数据行数有多有少,因此在滑动到数据少的itemView时,再次触碰界面,当前的itemView会有回弹的动作(由于contentSize小的缘故)。
HybridItemViews
数据源提供的itemView类型是混合的,即 CustomTableView
与 CustomCollectionView
(UICollectionView
的子类)。
AdjustContentSize
自适应调整cotentOffszie属性,这里不同的itemView的数据行数有多有少,当滑动到数据较少的itemView时,再次触碰界面并不会导致当前itemView的回弹,这里当前数据少的itemView已经做了最小contentSize的设置。
SingleOneKindView
模式下全部自适应 contentSize。DisabledBarScroll
取消顶部控制条的跟随滚动,只有在swipeHeaderView为nil的情况下才能生效。这样可以实现类似网易新闻首页滚动菜单列表的布局。
HiddenNavigationBar
隐藏导航。自定义了一个返回按钮(支持手势滑动返回)。
Demo支持添加/移除header(定义的UIImageView
)与bar(自定义的 CutomSegmentControl
)的功能。
示例代码新增点击图片全屏查看功能。
Demo中提供简单的自定义下拉刷新控件STRefreshHeader
,供参考。
SwipeTableView遵循MIT许可。查看LICENSE文件获取更多信息。