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)
    }
}
相关推荐
iOS大前端海猫18 小时前
Swift 中的Any、AnyObject和 AnyClass
ios·swift
仿生狮子1 天前
Reka UI 是个啥?
vue.js·nuxt.js·ui kit
数学人学c语言2 天前
通义千问模型微调——swift框架
python·深度学习·swift
coooliang2 天前
【iOS】SwiftUI 路由管理(NavigationStack)
ios·swiftui·swift
Sinyu10123 天前
Flutter 自定义 CustomPaint 实现流体液态加载动画
前端·flutter·ui kit
淘小欣3 天前
10分钟打造专属AI助手:用ms-swift实现自我认知微调
开发语言·人工智能·ai·swift·模型微调
Tee xm5 天前
清晰易懂的 Swift 安装与配置教程
开发语言·ios·swift
无知的前端6 天前
一文读懂 iOS 程序生命周期和视图生命周期
ios·面试·swift
山水域6 天前
串行队列(Serial Queue)和并行队列(Concurrent Queue)详解
ios·swift
Swift社区6 天前
Swift 并发中的任务让步(Yielding)和防抖(Debouncing)
开发语言·ios·swift