CocoaPods trunk is moving to be read-only. Read more on the blog, there are 7 months to go.

CLStickyPage 1.0.1

CLStickyPage 1.0.1

Maintained by ‘JmoVxia’.



  • By
  • JmoVxia

CLStickyPage

CLStickyPage 是一个 iOS 吸顶分页容器,解决 Header + Sticky Tab + Horizontal Pages + Nested Scroll 场景。

支持三种容器:

  • CLStickyPageView:纯 UIView 页面
  • CLStickyControllerPageView:纯 UIViewController 页面
  • CLStickyView:统一容器,可纯 View、可纯 Controller,也可混合

选型建议

方案 组件 页面返回类型 何时选
View 模式 CLStickyPageView CLStickyViewPage 页面是轻量视图,追求最小成本
Controller 模式 CLStickyControllerPageView CLStickyControllerPage 每页需要独立生命周期、路由、状态
统一容器模式 CLStickyView CLStickyPage(可返回 View/Controller) 你想用一套容器覆盖“纯”和“混合”两类需求

页面协议

public protocol CLStickyPage: AnyObject {
    var scrollView: UIScrollView { get }
}

public protocol CLStickyViewPage: CLStickyPage where Self: UIView {}
public protocol CLStickyControllerPage: CLStickyPage where Self: UIViewController {}

安装

Swift Package Manager

.package(url: "https://github.com/JmoVxia/CLStickyPage.git", from: "1.0.0")
dependencies: ["CLStickyPage"]

CocoaPods

pod 'CLStickyPage', '~> 1.0'

快速开始

1) View 模式:CLStickyPageView

final class DemoViewController: UIViewController {
    private lazy var pageView: CLStickyPageView = {
        let view = CLStickyPageView(isLazyLoading: false)
        view.dataSource = self
        view.delegate = self
        view.hoverStickyTopInset = 40
        return view
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(pageView)
        pageView.frame = view.bounds
        pageView.headerView = headerView
        pageView.hoverView = segmentedBar
        pageView.reload()
    }
}

extension DemoViewController: CLStickyPageViewDataSource {
    func numberOfPages(in pageView: CLStickyPageView) -> Int { 4 }

    func pageView(_ pageView: CLStickyPageView, pageAt index: Int) -> CLStickyViewPage {
        CLDemoStickyPageView()
    }
}

2) Controller 模式:CLStickyControllerPageView

final class DemoViewController: UIViewController {
    private lazy var pageView: CLStickyControllerPageView = {
        let view = CLStickyControllerPageView(isLazyLoading: true)
        view.dataSource = self
        view.delegate = self
        view.hoverStickyTopInset = 40
        return view
    }()
}

extension DemoViewController: CLStickyControllerPageViewDataSource {
    func numberOfPages(in controllerPageView: CLStickyControllerPageView) -> Int { 4 }

    func controllerPageView(
        _ controllerPageView: CLStickyControllerPageView,
        pageControllerAt index: Int
    ) -> CLStickyControllerPage {
        CLDemoTablePageController(title: "Page \(index)", accentColor: .systemBlue)
    }

    func parentViewController(for controllerPageView: CLStickyControllerPageView) -> UIViewController {
        self
    }
}

3) 统一容器:CLStickyView(可不混合,也可混合)

CLStickyView 的关键点是:pageAt 返回 CLStickyPage,所以你可以自由决定每一页返回 View 还是 Controller。

enum Mode {
    case viewOnly
    case controllerOnly
    case mixed
}

final class DemoViewController: UIViewController {
    var mode: Mode = .mixed
    private var viewCache: [Int: CLDemoStickyPageView] = [:]
    private var controllerCache: [Int: CLDemoTablePageController] = [:]

    private lazy var stickyView: CLStickyView = {
        let view = CLStickyView(isLazyLoading: true)
        view.dataSource = self
        view.delegate = self
        return view
    }()
}

extension DemoViewController: CLStickyViewDataSource {
    func numberOfPages(in stickyView: CLStickyView) -> Int { 4 }

    func stickyView(_ stickyView: CLStickyView, pageAt index: Int) -> CLStickyPage {
        switch mode {
        case .viewOnly:
            return viewCache[index] ?? {
                let page = CLDemoStickyPageView()
                viewCache[index] = page
                return page
            }()
        case .controllerOnly:
            return controllerCache[index] ?? {
                let page = CLDemoTablePageController(title: "Page \(index)", accentColor: .systemGreen)
                controllerCache[index] = page
                return page
            }()
        case .mixed:
            if index % 2 == 0 {
                return viewCache[index] ?? {
                    let page = CLDemoStickyPageView()
                    viewCache[index] = page
                    return page
                }()
            } else {
                return controllerCache[index] ?? {
                    let page = CLDemoTablePageController(title: "Page \(index)", accentColor: .systemOrange)
                    controllerCache[index] = page
                    return page
                }()
            }
        }
    }

    func parentViewController(for stickyView: CLStickyView) -> UIViewController? {
        self
    }
}

说明:

  • 纯 View 时,parentViewController 可不实现(默认 nil)。
  • 纯 Controller 或混合(包含 Controller 页)时,必须返回有效的父控制器。

常用 API

三种容器都支持以下核心能力:

  • 布局相关:headerViewhoverViewbackgroundViewhoverStickyTopInset
  • 交互相关:showIndicatorisHorizontalScrollEnabled
  • 状态相关:currentPageIndexnumberOfPages
  • 操作相关:reload(toPageAt:)scrollToPage(at:animated:)

CLStickyView 额外提供:

  • currentPage:当前页(CLStickyPage
  • currentController:当前控制器页(如果当前页是 Controller)
  • page(at:):获取指定页对象(懒加载时只对已创建页面生效)

Demo 说明

  • 入口:CLStickyPage/Demo/CLViewController.swift
  • View 模式:CLStickyPage/Demo/ViewMode/CLViewModeController.swift
  • Controller 模式:CLStickyPage/Demo/ControllerMode/CLControllerModeController.swift
  • 统一容器模式:CLStickyPage/Demo/MixedMode/CLMixedModeController.swift

统一容器 Demo 已内置三种组合切换:

  • 纯 View
  • 纯 Controller
  • 混合(View + Controller)

使用注意

  • 子页面的 scrollView 建议 contentInsetAdjustmentBehavior = .never,避免系统 inset 干扰吸顶计算。
  • reload(toPageAt:) 会重新获取页面,若你需要状态复用,请做好缓存。
  • 懒加载模式下,page(at:) 对尚未创建的索引会返回 nil,这是预期行为。

License

MIT