PullDownStretchable 快速实现顶部背景下拉放大

前言

你的工程中是否有下面这种需求呢

  • 顶部背景视图下拉放大,上滑跟随滑走
  • 宽度可能跟随放大,也可能不变
  • 背景视图跨越多个Cell,顶部Cell种类不固定

PullDownStretchable

使用 PullDownStretchable 可以快速实现上面的需求

下面是 PullDownStretchable 接口及实现,你也可到 TableViewSections 工程中查看实现 PullDownStretchable.swift 和使用示例

swift 复制代码
public protocol PullDownStretchable: class {
    /// 被拉伸的视图
    var stretchableView: UIView { get }
    /// 适用于跟随滑动view的拉伸,在 scrollViewDidScroll  中调用,offset = scrollView.contentOffset.y
    func stretch(with offset: CGFloat, scaleWidth: Bool)
    /// 适用于固定view的拉伸
    func stretchFixed(with offset: CGFloat, scaleWidth: Bool, ratio: CGFloat)
}

public extension PullDownStretchable {
    func stretch(with offset: CGFloat, scaleWidth: Bool = true) {
        if offset < 0 {
            let scale = 1 - offset / stretchableView.bounds.height
            let a = scaleWidth ? scale : 1
            stretchableView.transform = CGAffineTransform(a: a, b: 0, c: 0, d: scale, tx: 0, ty: offset * 0.5)
        } else {
            stretchableView.transform = .identity
        }
    }
    
    func stretchFixed(with offset: CGFloat, scaleWidth: Bool = true, ratio: CGFloat = 1) {
        if offset < 0 {
            let scale = 1 - offset / stretchableView.bounds.height
            let a = scaleWidth ? scale : 1
            stretchableView.transform = CGAffineTransform(a: a, b: 0, c: 0, d: scale, tx: 0, ty: -offset * 0.5 * ratio)
        } else if offset == 0 {
            stretchableView.transform = .identity
        } else {
            stretchableView.transform = CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 0, ty: -offset * ratio)
        }
    }
}

public extension PullDownStretchable where Self: UIView {
    var stretchableView: UIView { return self }
}

如何使用?

在 viewController 中定义 stretchableView

其中有一个注意点:

当 tableView 不是 viewController.view 的根视图时,系统自动填充机制会有一些问题,我也没有深入研究,这里自己处理的 ContentInset

swift 复制代码
class PlanType1ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .groupTableViewBackground
        
        // 1. 放在 tableView 下面,作为背景
        view.addSubview(topBgView)
        topBgView.snp.makeConstraints {
            $0.top.leading.width.equalToSuperview()
            $0.height.equalTo(230)
        }
        
        view.addSubview(tableView)
        // 2. tableView 不是根视图,需要自己处理内容填充,不然会产生奇怪的问题
        tableView.contentInsetAdjustmentBehavior = .never
        tableView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        
        tableView.ps.item = PSLabelItem.empty(text: "加载中...")
            .config { item in
                item.layoutOffset = CGPoint(x: 0, y: -150)
            }
        // 3. 请求接口时隐藏 topBgView
        topBgView.isHidden = true
        viewModel.loadData { [weak tableView, weak topBgView] error in
            // 4. 请求接口时隐藏 topBgView
            topBgView?.isHidden = false
            tableView?.ps.item = nil
        }
    }
    
    let viewModel = PlanType1TableVM()
    
    // 5. 定义顶部背景视图
    private let topBgView = UIView().ns.config{
        $0.backgroundColor = .orange
    }
    
    lazy var tableView: UITableView = {
        let tableView = UITableView(frame: .zero, style: .grouped)
        viewModel.associateTableView(tableView, viewController: self, additional: nil)
        // ... 省略 部分代码
        // 6. 背景色设置为头没,避免遮挡 topBgView
        tableView.backgroundColor = .clear
        return tableView
    }()
    
    // 7. 响应 safeAreaInsets 变更,调整 tableView.contentInset
    override func viewSafeAreaInsetsDidChange() {
        super.viewSafeAreaInsetsDidChange()
        print(view.safeAreaInsets)
        tableView.contentInset = UIEdgeInsets(top: view.safeAreaInsets.top, left: 0, bottom: 0, right: 0)
    }
}

// 8. 遵守 PullDownStretchable 协议
extension PlanType1ViewController: PullDownStretchable {
    // 9. 返回可拉伸的view
    var stretchableView: UIView {
        return topBgView
    }
}

响应 scrollViewDidScroll,调用拉伸方法

Demo 使用的 TableViewSections 架构组织 tableView 代码

PlanType1TableVM 作为 TableView 的 DataSourece、Delegate,通过 context 获取到 viewController

swift 复制代码
class PlanType1TableVM: EstimatedTableViewModel {
    
    // ... 省略无关代码
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        // 根据 contentOffset 控制 stretchableView 形变
        let continer = context.viewController as? PullDownStretchable
        
        // scaleWidth == false,stretchableView 宽度不变
        continer?.stretchFixed(with: scrollView.contentOffset.y + scrollView.adjustedContentInset.top, scaleWidth: false)
        
        // scaleWidth == true,默认值,stretchableView 宽度改变
//        continer?.stretchFixed(with: scrollView.contentOffset.y + scrollView.adjustedContentInset.top)
    }
}
相关推荐
报错小能手5 小时前
ios开发方向——swift错误处理:do/try/catch、Result、throws
开发语言·学习·ios·swift
小夏子_riotous8 小时前
openstack的使用——5. Swift服务的基本使用
linux·运维·开发语言·分布式·云计算·openstack·swift
mCell12 小时前
MacOS 下实现 AI 操控电脑(Computer Use)的思考
macos·agent·swift
用户794572239541312 小时前
【DGCharts】iOS 图表渲染事实标准——8 种图表类型、高度可定制,3 行代码画出一条折线
swiftui·swift
chaoguo12341 天前
Any metadata 的内存布局
swift·metadata·value witness table
冬奇Lab2 天前
一天一个开源项目(第71篇):awesome-design-md - 让 AI 彻底读懂你的设计规范
人工智能·开源·ui kit
tangweiguo030519872 天前
SwiftUI布局完全指南:从入门到精通
ios·swift
用户79457223954133 天前
【RxSwift】Swift 版 ReactiveX,响应式编程优雅处理异步事件流
swift·rxswift
战族狼魂3 天前
XCode 发起视频 和 收到视频通话邀请实现双语功能 中文和俄语
swift
UXbot3 天前
2026年AI全链路产品开发工具对比:5款从创意到上线一站式平台深度解析
前端·ui·kotlin·软件构建·swift·原型模式