Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

SPStore/NestedPageViewController

Repository files navigation

一个用于iOS的嵌套页面视图控制器,提供平滑的滚动协调体验。

功能特点

  • 支持头部视图、标签栏和多个子视图控制器
  • 支持内容滚动位置记录(该功能是设计本框架的最大动力来源)
  • 支持局部刷新和全局刷新
  • 支持子页面预加载(默认是滑动到指定页才加载)
  • 支持头部视图手指拖拽滚动并带动整体,可配置控制内容scrollView是否惯性滚动
  • 支持自定义标签栏
  • 支持旋转
  • 更多细节和功能请下载demo

功能演示

记录滚动位置 局部刷新 全局刷新
头部始终固定不动 头部缩放+导航栏隐藏 显示底部tabBar
子VC的sectionHeader吸顶 运行时修改头部高度 没有头部
滚到顶部 自定义标签栏1 自定义标签栏2

系统要求

  • iOS 13.0+
  • Swift 5.0+

安装

Swift Package Manager

在Xcode中,选择 File > Swift Packages > Add Package Dependency,然后输入以下URL:

https://github.com/SPStore/NestedPageViewController.git

CocoaPods

在你的Podfile中添加:

pod 'NestedPageViewController'

然后运行:

pod install

注意:如果CocoaPods的方式安装,编译报错:Xcode error when building app: line 7: /resources-to-copy-Project.txt: Permission denied,请在你的主工程中的Targets -> Build Settings -> User Script Sandboxing 改为No

使用方法

NestedPageViewController提供两种使用方式:添加子控制器方式和继承方式。

方式一:添加子控制器方式

import UIKit
import NestedPageViewController
class YourViewController: UIViewController {
 
 // MARK: - Properties
 
 private var nestedPageViewController = NestedPageViewController()
 private var coverView = YourHeaderView()
 private var customTabStrip = YourCustomTabStrip()
 
 // MARK: - View Controllers
 
 private let childControllerTitles = ["标签一", "标签二", "标签三", "标签四"]
 
 // MARK: - Lifecycle
 override func viewDidLoad() {
 super.viewDidLoad()
 
 setupNestedPageViewController()
 }
 
 // MARK: - Setup
 
 private func setupNestedPageViewController() {
 nestedPageViewController.dataSource = self
 nestedPageViewController.delegate = self
 
 // 添加为子控制器
 addChild(nestedPageViewController)
 view.addSubview(nestedPageViewController.view)
 nestedPageViewController.didMove(toParent: self)
 }
 
 override func viewDidLayoutSubviews() {
 super.viewDidLayoutSubviews()
 
 // 更新NestedPageViewController的frame
 let safeAreaTop = view.safeAreaInsets.top
 nestedPageViewController.view.frame = CGRect(
 x: 0,
 y: safeAreaTop,
 width: view.bounds.width,
 height: view.bounds.height - safeAreaTop
 )
 }
}
// MARK: - NestedPageViewControllerDataSource
extension YourViewController: NestedPageViewControllerDataSource {
 
 func numberOfViewControllers(in pageViewController: NestedPageViewController) -> Int {
 return childControllerTitles.count
 }
 
 func pageViewController(_ pageViewController: NestedPageViewController, viewControllerAt index: Int) -> NestedPageScrollable? {
 guard index >= 0 && index < childControllerTitles.count else { return nil }
 
 switch index {
 case 0:
 return YourChildViewController1() // 必须遵守NestedPageScrollable协议
 case 1:
 return YourChildViewController2() // 必须遵守NestedPageScrollable协议
 case 2:
 return YourChildViewController3() // 必须遵守NestedPageScrollable协议
 case 3:
 return YourChildViewController4() // 必须遵守NestedPageScrollable协议
 default:
 return nil
 }
 }
 
 func coverView(in pageViewController: NestedPageViewController) -> UIView? {
 return coverView
 }
 
 func heightForCoverView(in pageViewController: NestedPageViewController) -> CGFloat {
 return 200.0
 }
 
 func tabStrip(in pageViewController: NestedPageViewController) -> UIView? {
 return customTabStrip // 使用自定义标签栏
 }
 
 func heightForTabStrip(in pageViewController: NestedPageViewController) -> CGFloat {
 return 50.0
 }
 
 func titlesForTabStrip(in pageViewController: NestedPageViewController) -> [String]? {
 return nil // 使用自定义标签栏时返回nil
 }
}
// MARK: - NestedPageViewControllerDelegate
extension YourViewController: NestedPageViewControllerDelegate {
 
 // 页面横向滚动到指定索引位置的回调方法
 func pageViewController(_ pageViewController: NestedPageViewController, didScrollToPageAt index: Int) {
 // 页面切换回调
 print("当前页面索引: \(index)")
 }
 
 // 内容垂直滚动视图的滚动状态变化回调方法
 func pageViewController(_ pageViewController: NestedPageViewController, contentScrollViewDidScroll scrollView: UIScrollView, headerOffset: CGFloat, isSticked: Bool) {
 // headerOffset: 头部相对contentScrollView顶部的偏移量
 // isSticked: 是否处于完全吸顶状态
 
 // 例如:根据滚动状态控制导航栏的显示/隐藏
 if isSticked {
 // 头部完全吸顶,可以显示导航栏标题
 } else {
 // 头部未完全吸顶,可以隐藏导航栏标题
 }
 }
}

方式二:继承方式

import UIKit
import NestedPageViewController
class YourNestedPageViewController: NestedPageViewController {
 
 // MARK: - Properties
 
 private var coverView = YourHeaderView()
 private var customTabStrip = YourCustomTabStrip()
 
 // MARK: - View Controllers
 
 private let childControllerTitles = ["标签一", "标签二", "标签三", "标签四"]
 // MARK: - Lifecycle
 override func viewDidLoad() {
 super.viewDidLoad()
 
 setupNestedPageViewController()
 }
 
 override func viewDidLayoutSubviews() {
 let safeTop = view.safeAreaInsets.top
 containerInsets = UIEdgeInsets(top: safeTop, left: 0, bottom: 0, right: 0)
 
 // 采用继承方式时,需要在super之前设置containerInsets
 super.viewDidLayoutSubviews()
 }
 
 // MARK: - Setup
 
 private func setupNestedPageViewController() {
 // 设置数据源
 dataSource = self
 
 // 设置代理(继承方式下,可以直接重写代理方法)
 delegate = self
 }
 
 // MARK: - NestedPageViewControllerDelegate
 
 // 页面横向滚动到指定索引位置的回调方法
 override func pageViewController(_ pageViewController: NestedPageViewController, didScrollToPageAt index: Int) {
 super.pageViewController(pageViewController, didScrollToPageAt: index)
 
 // 页面切换回调
 print("当前页面索引: \(index)")
 }
 
 // 内容垂直滚动视图的滚动状态变化回调方法
 override func pageViewController(_ pageViewController: NestedPageViewController, contentScrollViewDidScroll scrollView: UIScrollView, headerOffset: CGFloat, isSticked: Bool) {
 super.pageViewController(pageViewController, contentScrollViewDidScroll: scrollView, headerOffset: headerOffset, isSticked: isSticked)
 
 // headerOffset: 头部相对contentScrollView顶部的偏移量
 // isSticked: 是否处于完全吸顶状态
 
 // 例如:根据滚动状态控制导航栏的显示/隐藏
 if isSticked {
 // 头部完全吸顶,可以显示导航栏标题
 } else {
 // 头部未完全吸顶,可以隐藏导航栏标题
 }
 }
}
// MARK: - NestedPageViewControllerDataSource
extension YourNestedPageViewController: NestedPageViewControllerDataSource {
 
 func numberOfViewControllers(in pageViewController: NestedPageViewController) -> Int {
 return childControllerTitles.count
 }
 
 func pageViewController(_ pageViewController: NestedPageViewController, viewControllerAt index: Int) -> NestedPageScrollable? {
 guard index >= 0 && index < childControllerTitles.count else { return nil }
 
 switch index {
 case 0:
 return YourChildViewController1() // 必须遵守NestedPageScrollable协议
 case 1:
 return YourChildViewController2() // 必须遵守NestedPageScrollable协议
 case 2:
 return YourChildViewController3() // 必须遵守NestedPageScrollable协议
 case 3:
 return YourChildViewController4() // 必须遵守NestedPageScrollable协议
 default:
 return nil
 }
 }
 
 func coverView(in pageViewController: NestedPageViewController) -> UIView? {
 return coverView
 }
 
 func heightForCoverView(in pageViewController: NestedPageViewController) -> CGFloat {
 return 200.0
 }
 
 func tabStrip(in pageViewController: NestedPageViewController) -> UIView? {
 return customTabStrip // 使用自定义标签栏
 }
 
 func heightForTabStrip(in pageViewController: NestedPageViewController) -> CGFloat {
 return 50.0
 }
 
 func titlesForTabStrip(in pageViewController: NestedPageViewController) -> [String]? {
 return nil // 使用自定义标签栏时返回nil
 }
}

Objective-C 使用方式

NestedPageViewController原本是用OC编写,考虑到swift是主流,于是改成了swift版本,OC工程要使用需要做一个桥接。

示例工程中提供了完整的 Objective-C 桥接示例,可以参考 Example/NestedPageExample/Examples-OC 目录下的实现。

性能报告

NestedPageViewController在性能方面进行了多项优化,确保在复杂的嵌套滚动场景下仍能保持流畅的用户体验。以下是demo中4个子控制下的性能评测:

内存占用

内存占用

CPU使用率

CPU使用率

实现原理

参见实现原理

项目起源

本仓库的前身是我在8年前开发的一个名为HVScrollView的演示项目。当时由于经验有限,未能将其封装成一个通用组件。项目的思想萌芽实际上源自腾讯bugly发布的一篇关于特斯拉组件的文章,该文章介绍了iOS高性能PageController的实现原理。

时光荏苒,8年过去了,我积累了更多的开发经验和技术沉淀,现在将这个想法重新实现并开源,希望能为iOS开发社区提供一个更加完善、易用的嵌套滚动解决方案。NestedPageViewController在保留原有思想精髓的基础上,进一步优化了性能和用户体验,为现代iOS应用提供了更加流畅的页面嵌套滚动效果。

参与贡献

由于本人工作繁忙,可能无法投入大量时间进行持续的更新迭代。我们非常欢迎有兴趣的开发者加入到项目中来,通过提交Pull Request的方式参与贡献。无论是功能改进、bug修复、文档完善还是性能优化,您的每一份贡献都将帮助这个项目变得更好。

如果您有任何问题或建议,也欢迎通过Issues进行讨论,或直接联系作者邮箱:lesp163@163.com。让我们一起打造更好的NestedPageViewController!

许可证

NestedPageViewController 使用 MIT 许可证。详情请查看 LICENSE 文件。

AltStyle によって変換されたページ (->オリジナル) /