{"id":15030621,"url":"https://github.com/liangdrime/swipetableview","last_synced_at":"2025-05-14T05:10:49.945Z","repository":{"id":44430253,"uuid":"59530386","full_name":"liangdrime/SwipeTableView","owner":"liangdrime","description":"Both scroll horizontal and vertical for segment scrollview which have  a same header. —  类似半糖、美丽说主页与QQ音乐歌曲列表布局效果，实现不同菜单的左右滑动切换，同时支持类似tableview的顶部工具栏悬停（既可以左右滑动，又可以上下滑动）。兼容下拉刷新，自定义 collectionview实现自适应 contentSize 还可实现瀑布流功能","archived":false,"fork":false,"pushed_at":"2024-12-17T06:44:24.000Z","size":28765,"stargazers_count":2290,"open_issues_count":102,"forks_count":442,"subscribers_count":65,"default_branch":"master","last_synced_at":"2025-04-13T13:54:25.345Z","etag":null,"topics":["bantang","collectionview","commonheader","swipeview","tableview"],"latest_commit_sha":null,"homepage":"","language":"Objective-C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/liangdrime.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"License","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-05-24T01:28:56.000Z","updated_at":"2025-03-23T08:20:44.000Z","dependencies_parsed_at":"2025-01-04T14:00:31.330Z","dependency_job_id":"25b37488-ac43-4d5d-8bba-7e035412d448","html_url":"https://github.com/liangdrime/SwipeTableView","commit_stats":{"total_commits":99,"total_committers":3,"mean_commits":33.0,"dds":0.0505050505050505,"last_synced_commit":"d79e724250cc3a12a29aa1b07c5d2dc00513ad3f"},"previous_names":["roylee-ml/swipetableview"],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/liangdrime%2FSwipeTableView","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/liangdrime%2FSwipeTableView/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/liangdrime%2FSwipeTableView/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/liangdrime%2FSwipeTableView/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/liangdrime","download_url":"https://codeload.github.com/liangdrime/SwipeTableView/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254076850,"owners_count":22010611,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bantang","collectionview","commonheader","swipeview","tableview"],"created_at":"2024-09-24T20:13:53.130Z","updated_at":"2025-05-14T05:10:49.839Z","avatar_url":"https://github.com/liangdrime.png","language":"Objective-C","funding_links":[],"categories":[],"sub_categories":[],"readme":"![LOGO](https://github.com/Roylee-ML/SwipeTableView/blob/master/ScreenShots/logo.png)\n\n[![CocoaPods](https://img.shields.io/badge/pod-v0.2.4-28B9FE.svg)](http://cocoapods.org/pods/SwipeTableView)\n![Platforms](https://img.shields.io/badge/platforms-iOS-orange.svg)\n[![License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://github.com/Roylee-ML/SwipeTableView/blob/master/License)\n\n功能类似半糖首页菜单与QQ音乐歌曲列表页面。即支持UITableview的上下滚动，同时也支持不同列表之间的滑动切换。同时可以设置顶部header view与列表切换功能bar，使用方式类似于原生UITableview的tableHeaderView的方式。 [`Engilish→`](https://github.com/Roylee-ML/SwipeTableView/blob/master/README_EN.md)\n\n⚠️**注意**: 由于该组件的实现原理并不是十分安全可靠，该组件已经停止维护。如有相同需求，请移步到 [HBHybridCollectionView](https://github.com/Ban-Tang/HBHybridCollectionView) 或者类似的组件。HBHybridCollectionView 更稳定，接入更灵活，目前知乎个人页的底层组件使用的是相同的代码。\n****\n\n# 预览\n\u003cimg src=\"https://github.com/Roylee-ML/SwipeTableView/blob/master/ScreenShots/screenshot1.gif\" width = \"290\" height = \"517\" alt=\"OverView1\" align=center /\u003e\n\u003cimg src=\"https://github.com/Roylee-ML/SwipeTableView/blob/master/ScreenShots/screenshot2.gif\" width = \"290\" height = \"517\" alt=\"OverView1\" align=center /\u003e\n\u003cimg src=\"https://github.com/Roylee-ML/SwipeTableView/blob/master/ScreenShots/screenshot3.gif\" width = \"290\" height = \"517\" alt=\"OverView1\" align=center /\u003e\n\n\n# 使用 Cocoapods 导入\nSwipeTableView is available on [CocoaPods](http://cocoapods.org).  Add the following to your Podfile:\n\n```ruby\npod 'SwipeTableView'\n```\n\n\n# 目录\n1. [实现原理](https://github.com/Roylee-ML/SwipeTableView/blob/master/README.md#实现的原理)\n2. [基本用法](https://github.com/Roylee-ML/SwipeTableView/blob/master/README.md#怎样使用使用方式类似uitableview)\n3. [下拉刷新](https://github.com/Roylee-ML/SwipeTableView/blob/master/README.md#如何支持下拉刷新)\n4. [混合模式](https://github.com/Roylee-ML/SwipeTableView/blob/master/README.md#混合模式uitableview--uicollectionview--uiscrollview)\n5. [示例代码](https://github.com/Roylee-ML/SwipeTableView/blob/master/README.md#示例代码)\n6. [Demo介绍](https://github.com/Roylee-ML/SwipeTableView/blob/master/README.md#使用的详细用法在swipetableviewdemo文件夹中提供了五种示例)\n\n\n\n## 实现的原理\n\u003e为了兼容下拉刷新，采用了两种实现方式，但基本构造都是一样的\n      \n### Mode 1\n\n![Mode 1](https://github.com/Roylee-ML/SwipeTableView/blob/master/ScreenShots/SwipeTableViewStruct1.png)\n   \n1. 使用`UICollectionView`作为item的载体，实现左右滑动的功能。\n\n2. 在支持左右滑动之后，最关键的问题就是是滑动后相邻item的对齐问题。\n\u003e为实现前后item对齐，需要在itemView重用的时候，比较前后两个itemView的contentOffset，然后设置后一个itemView的contentOffset与前一个相同。这样就实现了左右滑动后前后itemView的offset是对齐的。 \n\n3. 由于多个item共用一个header与bar，所以，header与bar必须是根视图的子视图，即与CollectionView一样是`SwipeTableView`的子视图，并且在CollectionView的图层之上。\n\u003eheadr \u0026 bar的滚动与悬停实现是，对当前的itemView的contentOffset进行KVO。然后在当前itemView的contentOffset发生变化时，去改变header与bar的Y坐标值。\n \n4. 顶部header \u0026 bar在图层的最顶部，所以每个itemView的顶部需要做出一个留白来作为header \u0026 bar的显示空间。在`Mode 1`中，采用修改`UIScrollView`的contentInsets的top值来留出顶部留白。\n\n5. 由于header在图层的最顶部，所以要实现滑动header的同时使当前itemView跟随滚动，需要根据header的frame的变化回调给当前的itemView来改变contentOffset，同时也要具有ScrollView的弹性等效果。\n\u003e这里采用`UIKit Dynamic`物理动画引擎自定义`STHeaderView`实现自定义`UIScrollView`效果解决上述问题 [`参考文章`](http://philcai.com/2016/03/15/%E7%94%A8UIKit-Dynamics%E6%A8%A1%E4%BB%BFUIScrollView/) [`英文博客`](http://holko.pl/2014/07/06/inertia-bouncing-rubber-banding-uikit-dynamics/)。\n \n \n \n### Mode 2\n\n![Mode 2](https://github.com/Roylee-ML/SwipeTableView/blob/master/ScreenShots/SwipeTableViewStruct2.png)\n\n1. 在`Mode 2`中，基本结构与`Mode 1`一样，唯一的不同在于每个itemView顶部留白的的方式。\n\u003e通过设置`UITabelView`的`tableHeaderView`，来提供顶部的占位留白，CollectionView采用自定义`STCollectionView`的`collectionHeaderView`来实现占位留白。（目前不支持`UIScrollView`）\n\n\n2. 如何设置区分`Mode 1`与`Mode 2`模式？\n\u003e正常条件下即为`Mode 1`模式；在`SwipeTableView.h`中或者在工程PCH文件中设置宏`#define ST_PULLTOREFRESH_HEADER_HEIGHT xx`设置为`Mode 2`模式。\n\n\n\n## 使用用法\n### 怎样使用？使用方式类似UITableView\n\n**实现 `SwipeTableViewDataSource` 代理的两个方法：**\n\n```objc\n- (NSInteger)numberOfItemsInSwipeTableView:(SwipeTableView *)swipeView     \n```\n\u003e返回列表item的个数\n\n   \n```objc\n- (UIScrollView *)swipeTableView:(SwipeTableView *)swipeView viewForItemAtIndex:(NSInteger)index reusingView:(UIScrollView *)view\n```    \n\u003e返回对应index下的itemView，返回的视图类型需要是`UIScrollView`及其子类：`UITableView`或者`UICollectionView`。这里采用重用机制，需要根据reusingView来创建单一的itemView。\n\n**使用的`swipeHeaderView`必须是`STHeaderView`及其子类的实例。**\n \n\n### 如何支持下拉刷新？\n\n\u003e**下拉刷新有两种实现方式，一种用户自定义下拉刷新组件（局部修改自定义），一种是简单粗暴设置宏：**\n\n\n**1. 一行代码支持常用的下拉刷新控件，只需要在项目的PCH文件中或者在`SwipeTableView.h`文件中设置如下的宏：**\n \n```objc\n#define ST_PULLTOREFRESH_HEADER_HEIGHT xx   \n```\n\n\u003e上述宏中的`xx`要与您使用的第三方下拉刷新控件的refreshHeader高度相同：      \n`MJRefresh` 为 `MJRefreshHeaderHeight`，`SVPullToRefresh` 为 `SVPullToRefreshViewHeight`（注：此时视图结构为`Model 2`）\n\n\n新增下拉刷新代理，可以控制每个item下拉临界高度，并自由控制每个item是否支持下拉刷新\n \n```objc\n- (BOOL)swipeTableView:(SwipeTableView *)swipeTableView shouldPullToRefreshAtIndex:(NSInteger)index\n```\n \u003e根据item所在index，设置item是否支持下拉刷新。**在设置`#define ST_PULLTOREFRESH_HEADER_HEIGHT xx`的时候默认是YES（全部支持），否则默认为NO。**\n \n \n```objc\n- (CGFloat)swipeTableView:(SwipeTableView *)swipeTableView heightForRefreshHeaderAtIndex:(NSInteger)index\n``` \n\u003e返回对应item下拉刷新的临界高度，如果没有实现此代理，在设置`#define ST_PULLTOREFRESH_HEADER_HEIGHT xx`的时候默认是`ST_PULLTOREFRESH_HEADER_HEIGHT`的高度。**如果没有设置宏，并且想要自定义修改下拉刷新，必须实现此代理，提供下拉刷新控件RefreshHeader的高度（RefreshHeader全部露出的高度），来通知`SwipeTableView`触发下拉刷新。**\n\n\n**2. 如果想要更好的扩展性，以及喜欢自己研究的同学，可以尝试修改或者自定义下拉控件来解决下拉刷新的兼容问题，同时这里提供一些思路：**\n\n如果下拉刷新控件的frame是固定的（比如header的frame），这样可以在初始化下拉刷新的header或者在数据源的代理中重设下拉header的frame。\n \n\u003e获取下拉刷新的header，将header的frame的y值减去`swipeHeaderView`与`swipeHeaderBar`的高度和（或者重写RefreshHeader的setFrame方法），就可以消除itemView contentInsets顶部留白top值的影响（否则添加的下拉header是隐藏在底部的）。\n \n```objc\n- (UIScrollView *)swipeTableView:(SwipeTableView *)swipeView viewForItemAtIndex:(NSInteger)index reusingView:(UIScrollView *)view {\n   ...\n   STRefreshHeader * header = scrollView.header;\n   header.y = - (header.height + (swipeHeaderView.height + swipeHeaderBar.height));\n   ...\n}\n \n\nor\n\n\n- (instancetype)initWithFrame:(CGRect)frame {\n   ...\n   STRefreshHeader * header = [STRefreshHeader headerWithRefreshingBlock:^(STRefreshHeader *header) {\n\n}];\n   header.y = - (header.height + (swipeHeaderView.height + swipeHeaderBar.height)); \n   scrollView.header = header;\n   ...\n}\n``` \n\n对于一些下拉刷新控件，RefreshHeader的frame设置可能会在`layoutSubviews`中，所以，对RefreshHeader frame的修改,需要等执行完`layouSubviews`之后，在 \u003cu\u003e*有效的方法*\u003c/u\u003e 中操作，比如：\n   \n   \n```objc\n- (void)scrollViewDidScroll:(UIScrollView *)scrollView {\n    STRefreshHeader * header = self.header;\n    CGFloat orginY = - (header.height + self.swipeTableView.swipeHeaderView.height + self.swipeTableView.swipeHeaderBar.height);\n    if (header.y != orginY) {\n        header.y = orginY;\n    }\n}\n```\n\n\n如何判断下拉刷新的控件的frame是不是固定不变的呢？\n \n\u003e一是可以研究源码查看RefreshHeader的frame是否固定不变；另一个简单的方式是，在ScrollView的滚动代理中log RefreshHeader的frame（大部分的下拉控件的frame都是固定的）。\n\n \n如果使用的下拉刷新控件的frame是变化的（个人感觉极少数），那么只能更深层的修改下拉刷新控件或者自定义下拉刷新。也可以更直接的采用第一种设置宏的方式支持下拉刷新。\u003c/br\u003e\n \n\n### 混合模式（UItableView \u0026 UICollectionView \u0026 UIScrollView）\n\n1. 在`Mode 1`模式下，属于最基本的模式，可扩展性也是最强的，此时，支持`UITableView`、`UICollectionView`、`UIScrollView`。**如果，同时设置`shouldAdjustContentSize`为YES，实现自适应contentSize，在`UICollectionView`内容不足的添加下，只能使用`STCollectionView`及其子类**\n   \n   \u003e**`UICollectionView`不支持通过contentSize属性设置contentSize。**\n\n2. 在`Mode 2`模式下，**`SwipeTableView`支持的collectionView必须是`STCollectionView`及其子类的实例**，目前，不支持`UIScrollView`。\n\n\n## **示例代码**：\n\n### 初始化并设置header与bar\n```objc\nself.swipeTableView = [[SwipeTableView alloc]initWithFrame:[UIScreen mainScreen].bounds];\n_swipeTableView.delegate = self;\n_swipeTableView.dataSource = self;\n_swipeTableView.shouldAdjustContentSize = YES;\n_swipeTableView.swipeHeaderView = self.tableViewHeader;\n_swipeTableView.swipeHeaderBar = self.segmentBar;\n```\n   \n### 实现数据源代理：\n```objc\n- (NSInteger)numberOfItemsInSwipeTableView:(SwipeTableView *)swipeView {\n    return 4;\n}\n\n- (UIScrollView *)swipeTableView:(SwipeTableView *)swipeView viewForItemAtIndex:(NSInteger)index reusingView:(UIScrollView *)view {\n    UITableView * tableView = view;\n    if (nil == tableView) {\n        UITableView * tableView = [[UITableView alloc]initWithFrame:swipeView.bounds style:UITableViewStylePlain];\n        tableView.backgroundColor = [UIColor whiteColor];\n        ...\n    }\n    // 这里刷新每个item的数据\n    [tableVeiw refreshWithData:dataArray];\n    ...\n    return tableView;\n}\n```\n   \n### `STCollectionView`使用方法：\n```objc\nMyCollectionView.h\n\n@interface MyCollectionView : STCollectionView\n\n@property (nonatomic, assign) NSInteger numberOfItems;\n@property (nonatomic, assign) BOOL isWaterFlow;\n\n@end\n\n\n\nMyCollectionView.m\n\n- (instancetype)initWithFrame:(CGRect)frame {\n\n    self = [super initWithFrame:frame];\n    if (self) {\n        STCollectionViewFlowLayout * layout = self.st_collectionViewLayout;\n        layout.minimumInteritemSpacing = 5;\n        layout.minimumLineSpacing = 5;\n        layout.sectionInset = UIEdgeInsetsMake(5, 5, 5, 5);\n        self.stDelegate = self;\n        self.stDataSource = self;\n        [self registerClass:UICollectionViewCell.class forCellWithReuseIdentifier:@\"item\"];\n        [self registerClass:UICollectionReusableView.class forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@\"header\"];\n        [self registerClass:UICollectionReusableView.class forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@\"footer\"];\n    }\n    return self;\n}\n\n\n- (NSInteger)collectionView:(UICollectionView *)collectionView layout:(STCollectionViewFlowLayout *)layout numberOfColumnsInSection:(NSInteger)section {\n    return _numberOfColumns;\n}\n\n- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {\n    return CGSizeMake(0, 100);\n}\n\n- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section {\n    return CGSizeMake(kScreenWidth, 35);\n}\n\n- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section {\n    return CGSizeMake(kScreenWidth, 35);\n}\n\n- (UICollectionReusableView *)stCollectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {\n    UICollectionReusableView * reusableView = nil;\n    if ([kind isEqualToString:UICollectionElementKindSectionHeader]) {\n        reusableView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:@\"header\" forIndexPath:indexPath];\n        // custom UI......\n    }else if ([kind isEqualToString:UICollectionElementKindSectionFooter]) {\n        reusableView = [collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:@\"footer\" forIndexPath:indexPath];\n        // custom UI......\n    }\n    return reusableView;\n}\n\n- (NSInteger)numberOfSectionsInStCollectionView:(UICollectionView *)collectionView {\n    return _numberOfSections;\n} \n\n- (NSInteger)stCollectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {\n    return _numberOfItems;\n}\n\n- (UICollectionViewCell *)stCollectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {\n    UICollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:@\"item\" forIndexPath:indexPath];\n    // do something .......\n    return cell;\n}\n\n```\n\n**如果`STCollectionViewFlowLayout`已经不能满足`UICollectionView`的布局的话，用户自定义的`flowlayout`需要继承自`STCollectionViewFlowLayout`，并在重写相应方法的时候需要调用父类方法，并需要遵循一定规则，如下：**\n   \n\n```objc\n- (void)prepareLayout {\n    [super prepareLayout];\n    // do something in sub class......\n}\n\n- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {\n    NSArray * superAttrs = [super layoutAttributesForElementsInRect:rect];\n    NSMutableArray * itemAttrs = [superAttrs mutableCopy];\n\n    // filter subClassAttrs to rect\n    NSArray * filteredSubClassAttrs = ........;\n\n    [itemAttrs addObjectsFromArray:fittesSubClassAttrs];\n\n    return itemAttrs;\n}\n\n- (CGSize)collectionViewContentSize {\n    CGSize superSize = [super collectionViewContentSize];\n\n    CGSize subClassSize = .......;\n    subClassSize.height += superSize.height;\n\n    // fit mincontentSize\n    STCollectionView * collectionView = (STCollectionView *)self.collectionView;\n    subClassSize.height = fmax(subClassSize.height, collectionView.minRequireContentSize.height);\n\n    return subClassSize;\n}\n\n```\n\n\n## Demo 介绍\n### 使用的详细用法在SwipeTableViewDemo文件夹中，提供了五种示例：\n\n  - `SingleOneKindView`   \n     数据源提供的是单一类型的itemView，这里默认提供的是 `CustomTableView` （`UITableView`的子类），并且每一个itemView的数据行数有多有少，因此在滑动到数据少的itemView时，再次触碰界面，当前的itemView会有回弹的动作（由于contentSize小的缘故）。\n\n  - `HybridItemViews`     \n     数据源提供的itemView类型是混合的，即 `CustomTableView` 与 `CustomCollectionView` （`UICollectionView`的子类）。\n\n  - \u003cp\u003e\u003cdel\u003e`AdjustContentSize`   \n     自适应调整cotentOffszie属性，这里不同的itemView的数据行数有多有少，当滑动到数据较少的itemView时，再次触碰界面并不会导致当前itemView的回弹，这里当前数据少的itemView已经做了最小contentSize的设置。\u003c/del\u003e\u003c/p\u003e在0.2.3版本中去除了 demo 中的这一模块，默认除了`SingleOneKindView`模式下全部是自适应 contentSize。\n\n  - `DisabledBarScroll`         \n     取消顶部控制条的跟随滚动，只有在swipeHeaderView是nil的条件下才能生效。这样可以实现一个类似网易新闻首页的滚动菜单列表的布局。\n\n  - `HiddenNavigationBar` \n     隐藏导航。自定义了一个返回按钮（支持手势滑动返回）。\n\n  -  Demo支持添加移除header（定义的`UIImageView`）与bar（自定义的 `CutomSegmentControl` ）的功能。\n\n  -  示例代码新增点击图片全屏查看。\n  \n  -  Demo中提供简单的自定义下拉刷新控件`STRefreshHeader`，供参考\n\n# License\nSwipeTableView is available under the MIT license. See the LICENSE file for more info.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fliangdrime%2Fswipetableview","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fliangdrime%2Fswipetableview","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fliangdrime%2Fswipetableview/lists"}