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)
    }
}
相关推荐
大熊猫侯佩8 小时前
反抗军工程师的 “苹果智能” 实战指南:用本机基础模型打造 AI 利刃
ai编程·swift·apple
YungFan1 天前
iOS26适配指南之UIViewController
ios·swift
HarderCoder4 天前
我们真的需要 typealias 吗?——一次 Swift 抽象成本的深度剖析
swift
HarderCoder4 天前
ByAI-Swift 6 全览:一份面向实战开发者的新特性速查手册
swift
HarderCoder4 天前
Swift 中 let 与 var 的真正区别:不仅关乎“可变”与否
swift
HarderCoder4 天前
深入理解 Swift 6.2 并发:从默认隔离到@concurrent 的完整指南
swift
麦兜*5 天前
Swift + Xcode 开发环境搭建终极指南
开发语言·ios·swiftui·xcode·swift·苹果vision pro·swift5.6.3
HarderCoder6 天前
Swift Concurrency:彻底告别“线程思维”,拥抱 Task 的世界
swift
HarderCoder6 天前
深入理解 Swift 中的 async/await:告别回调地狱,拥抱结构化并发
swift
HarderCoder6 天前
深入理解 SwiftUI 的 ViewBuilder:从隐式语法到自定义容器
swiftui·swift