CocoaPods trunk is moving to be read-only. Read more on the blog, there are 14 months to go.
Maintained by Roy lee.
功能类似半糖首页菜单与QQ音乐歌曲列表页面。即支持UITableview的上下滚动,同时也支持不同列表之间的滑动切换。同时可以设置顶部header view与列表切换功能bar,使用方式类似于原生UITableview的tableHeaderView的方式。
SwipeTableView is available on CocoaPods. Add the following to your Podfile:
pod 'SwipeTableView'为了兼容下拉刷新,采用了两种实现方式,但基本构造都是一样的
使用UICollectionView作为item的载体,实现左右滑动的功能。
在支持左右滑动之后,最关键的问题就是是滑动后相邻item的对齐问题。
为实现前后item对齐,需要在itemView重用的时候,比较前后两个itemView的contentOffset,然后设置后一个itemView的contentOffset与前一个相同。这样就实现了左右滑动后前后itemView的offset是对齐的。
由于多个item共用一个header与bar,所以,header与bar必须是根视图的子视图,即与CollectionView一样是SwipeTableView的子视图,并且在CollectionView的图层之上。
headr & bar的滚动与悬停实现是,对当前的itemView的contentOffset进行KVO。然后在当前itemView的contentOffset发生变化时,去改变header与bar的Y坐标值。
顶部header & bar在图层的最顶部,所以每个itemView的顶部需要做出一个留白来作为header & bar的显示空间。在Model 1中,采用修改UIScrollView的contentInsets的top值来留出顶部留白。
由于header在图层的最顶部,所以要实现滑动header的同时使当前itemView跟随滚动,需要根据header的frame的变化回调给当前的itemView来改变contentOffset,同时也要具有ScrollView的弹性等效果。
这里采用
UIKit Dynamic物理动画引擎自定义STHeaderView实现自定义UIScrollView效果解决上述问题参考文章英文博客。
Model 2中,基本结构与Model 1一样,唯一的不同在于每个itemView顶部留白的的方式。
>通过设置UITabelView的tableHeaderView,来提供顶部的占位留白,CollectionView采用自定义STCollectionView的collectionHeaderView来实现占位留白。(目前不支持UIScrollView)Model 1与Model 2模式?
>正常条件下即为Model 1模式;在SwipeTableView.h中或者在工程PCH文件中设置宏#define ST_PULLTOREFRESH_HEADER_HEIGHT xx设置为Modle 2模式。实现 SwipeTableViewDataSource 代理的两个方法:
- (NSInteger)numberOfItemsInSwipeTableView:(SwipeTableView *)swipeView
返回列表item的个数
- (UIScrollView *)swipeTableView:(SwipeTableView *)swipeView viewForItemAtIndex:(NSInteger)index reusingView:(UIScrollView *)view
返回对应index下的itemView,返回的视图类型需要是
UIScrollView及其子类:UITableView或者UICollectionView。这里采用重用机制,需要根据reusingView来创建单一的itemView。
使用的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 is available under the MIT license. See the LICENSE file for more info.