SwipeTableView 0.2.6

SwipeTableView 0.2.6

测试已测试
语言语言 Objective-CObjective C
许可证 MIT
发布上次发布2016年9月

Roy lee维护。



  • 作者
  • Roylee-ML

功能类似于半糖首页菜单与QQ音乐歌曲列表页面。即支持UITableview的上下滚动,同时也支持不同列表之间的滑动切换。同时可以设置顶部header view与列表切换功能bar,使用方式类似于原生UITableview的tableHeaderView的方式。

概述

Demo OverView1 Demo OverView2


快速开始

SwipeTableView 可在 CocoaPods 上使用。将以下内容添加到您的 Podfile 中:

pod 'SwipeTableView'


介绍

  1. 实现原理
  2. 基本用法
  3. 下拉刷新
  4. 混合模式
  5. 示例代码
  6. 演示介绍


实现的原理

为了兼容下拉刷新,采用了两种实现方式,但基本构造都是一样的

模式 1

Mode 1

  1. 使用 UICollectionView 作为 item 的载体,实现左右滑动的功能。

  2. 在支持左右滑动之后,最关键的问题就是滑动后相邻 item 的对齐问题。

    为实现前后 item 对齐,需要在 item 视图重用的时候,比较前后两个 item 视图的 contentOffset,然后设置后一个 item 视图的 contentOffset 与前一个相同。这样就实现了滑动后前后 item 视图的 offset 是对齐的。

  3. 由于多个 item 共用一个 header 与 bar,所以,header 与 bar 必须是根视图的子视图,即与 CollectionView 一样是 SwipeTableView 的子视图,并且在 CollectionView 的图层之上。

    header & bar 的滚动与悬停实现是,对当前 item 视图的 contentOffset 进行 KVO。然后在当前 item 视图的 contentOffset 改变时,去改变 header 与 bar 的 Y 坐标值。

  4. 顶部 header & bar 在图层的最顶部,所以每个 item 视图的顶部需要做出一个留白来作为 header & bar 的显示空间。在 Model 1 中,采用修改 UIScrollView 的 contentInsets 的 top 值来留出顶部离白。

  5. 由于 header 在图层的最顶部,所以要实现滑动 header 的同时使当前 item 视图跟随滚动,需要根据 header 的 frame 的变化回调给当前的 item 视图来改变 contentOffset,同时也要具有 ScrollView 的弹性等效果。

    这里采用 UIKit Dynamic 物理动画引擎自定义 STHeaderView 来实现自定义 UIScrollView 效果解决上述问题 参考文章 英文博客

模式 2

Mode 2

  1. Model 2 中,基本结构与 Model 1 一样,唯一的不同在于每个 item 视图顶部留白的的方式来提供顶部的留白,CollectionView 采用自定义 STCollectionViewcollectionHeaderView 来实现留白。(目前不支持 UIScrollView


  1. 如何设置区分 Model 1Model 2 模式? > 正常情况下即为 Model 1 模式;在 SwipeTableView.h 中或者在工程 PCH 文件中设置宏 #define ST_PULLTOREFRESH_HEADER_HEIGHT xx 设置为 Model 2 模式。


基本使用

怎样使用?使用方式类似于 UITableView

实现 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高度相同:
MJRefreshMJRefreshHeaderHeightSVPullToRefreshSVPullToRefreshViewHeight(注:此时视图结构为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值减去swipeHeaderViewswipeHeaderBar的高度和(或者重写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是变化的(个人感觉极少数),那么只能进行更深层地修改下拉刷新控件或自定义下拉刷新。也可以直接采用第一种设置宏的方法支持下拉刷新。


混合模式(UItableView & UICollectionView & UIScrollView)

  1. Model 1模式下,属于最基本的模式,可扩展性也是最强的,此时支持UITableViewUICollectionViewUIScrollView如果同时设置shouldAdjustContentSize为YES,实现自适应contentSize,在UICollectionView内容不足时只能使用STCollectionView及其子类

    UICollectionView不支持通过contentSize属性设置contentSize。

  2. Model 2模式下,SwipeTableView支持的collectionView必须是STCollectionView及其子类的实例,目前不支持UIScrollView


示例代码

初始化并设置header与bar

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;
}


Demo Info

使用的详细用法在SwipeTableViewDemo文件夹中,提供了五种示例:

  • SingleOneKindView
    数据源提供的是单一类型的itemView,这里默认提供的是 CustomTableViewUITableView的子类),并且每一个itemView的数据行数有多有少,因此在滑动到数据少的itemView时,再次触碰界面,当前的itemView会有回弹的动作(由于contentSize小的缘故)。

  • HybridItemViews
    数据源提供的itemView类型是混合的,即 CustomTableViewCustomCollectionViewUICollectionView的子类)。

  • AdjustContentSize
    自适应调整cotentOffszie属性,这里不同的itemView的数据行数有多有少,当滑动到数据较少的itemView时,再次触碰界面并不会导致当前itemView的回弹,这里当前数据少的itemView已经做了最小contentSize的设置。

    在0.2.3版本中移除了demo中的这个模块,默认除了SingleOneKindView模式下全部自适应 contentSize。
  • DisabledBarScroll
    取消顶部控制条的跟随滚动,只有在swipeHeaderView为nil的情况下才能生效。这样可以实现类似网易新闻首页滚动菜单列表的布局。

  • HiddenNavigationBar 隐藏导航。自定义了一个返回按钮(支持手势滑动返回)。

  • Demo支持添加/移除header(定义的UIImageView)与bar(自定义的 CutomSegmentControl )的功能。

  • 示例代码新增点击图片全屏查看功能。

  • Demo中提供简单的自定义下拉刷新控件STRefreshHeader,供参考。

License

SwipeTableView遵循MIT许可。查看LICENSE文件获取更多信息。