You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift UICollectionView单元格上滑展开实现方案咨询

实现UICollectionView单元格上滑展开效果(Swift)

我来帮你实现这个类似Todo Concept的单元格上滑展开效果,核心思路是通过动态调整单元格高度+手势识别来完成,下面是完整的实现步骤和代码:

1. 核心思路拆解

  • 给每个待办项添加展开状态标记,用于控制单元格显示样式
  • 自定义UICollectionViewCell,包含折叠/展开两种状态的UI元素
  • 给单元格添加拖动手势,识别上滑/下滑动作来触发状态切换
  • 基于展开状态动态计算单元格高度,通过批量更新实现平滑过渡

2. 完整代码实现

2.1 数据模型定义

首先定义一个包含展开状态的待办项模型:

struct TodoItem {
    let title: String
    let description: String
    var isExpanded: Bool = false // 标记是否展开
}

2.2 自定义CollectionViewCell

创建带手势识别和动态UI的自定义单元格:

import UIKit

protocol TodoCellDelegate: AnyObject {
    func toggleExpansion(for cell: TodoCollectionViewCell)
}

class TodoCollectionViewCell: UICollectionViewCell {
    static let reuseIdentifier = "TodoCollectionViewCell"
    
    // MARK: - UI元素
    private let titleLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 18, weight: .medium)
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    private let descriptionLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 14)
        label.numberOfLines = 0
        label.translatesAutoresizingMaskIntoConstraints = false
        label.isHidden = true // 默认隐藏展开内容
        return label
    }()
    
    // 用于动态控制展开内容高度的约束(可选)
    private var descriptionHeightConstraint: NSLayoutConstraint!
    weak var delegate: TodoCellDelegate?
    
    // MARK: - 初始化
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupUI()
        addPanGesture()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - UI布局
    private func setupUI() {
        contentView.backgroundColor = .white
        contentView.layer.cornerRadius = 12
        contentView.layer.shadowColor = UIColor.black.cgColor
        contentView.layer.shadowOffset = CGSize(width: 0, height: 2)
        contentView.layer.shadowOpacity = 0.1
        contentView.layer.shadowRadius = 4
        
        contentView.addSubview(titleLabel)
        contentView.addSubview(descriptionLabel)
        
        NSLayoutConstraint.activate([
            titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16),
            titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
            titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
            
            descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 12),
            descriptionLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
            descriptionLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
            descriptionLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16)
        ])
        
        // 可选:保存高度约束用于更精细的动画控制
        descriptionHeightConstraint = descriptionLabel.heightAnchor.constraint(equalToConstant: 0)
        descriptionHeightConstraint.isActive = false
    }
    
    // MARK: - 手势识别
    private func addPanGesture() {
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
        addGestureRecognizer(panGesture)
    }
    
    @objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
        let translation = gesture.translation(in: self)
        
        // 上滑超过30pt触发展开,下滑超过30pt触发折叠(阈值可调整)
        switch gesture.state {
        case .ended:
            if translation.y < -30 {
                delegate?.toggleExpansion(for: self)
            } else if translation.y > 30 {
                delegate?.toggleExpansion(for: self)
            }
        default:
            break
        }
    }
    
    // MARK: - 配置单元格数据
    func configure(with item: TodoItem) {
        titleLabel.text = item.title
        descriptionLabel.text = item.description
        
        // 根据展开状态更新UI
        if item.isExpanded {
            descriptionLabel.isHidden = false
            descriptionHeightConstraint.isActive = false
        } else {
            descriptionLabel.isHidden = true
            descriptionHeightConstraint.isActive = true
        }
    }
}

2.3 主ViewController实现

配置CollectionView并处理展开/折叠逻辑:

import UIKit

class TodoViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, TodoCellDelegate {
    
    private var collectionView: UICollectionView!
    private var todoItems: [TodoItem] = [
        TodoItem(title: "Finish Swift Project", description: "Need to implement collection view cell expansion and fix layout bugs. Make sure to test on different screen sizes."),
        TodoItem(title: "Grocery Shopping", description: "Buy milk, eggs, bread, vegetables, and a carton of orange juice. Don't forget to check the discount section."),
        TodoItem(title: "Call Mom", description: "Catch up with mom about her weekend trip to the lake. Ask how the dog is doing and if she needs any help with gardening.")
    ]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupCollectionView()
        view.backgroundColor = .systemGray6
    }
    
    // MARK: - 配置CollectionView
    private func setupCollectionView() {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .vertical
        layout.sectionInset = UIEdgeInsets(top: 20, left: 16, bottom: 20, right: 16)
        layout.minimumLineSpacing = 12
        
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.backgroundColor = .clear
        collectionView.register(TodoCollectionViewCell.self, forCellWithReuseIdentifier: TodoCollectionViewCell.reuseIdentifier)
        collectionView.dataSource = self
        collectionView.delegate = self
        
        view.addSubview(collectionView)
        
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
        ])
    }
    
    // MARK: - UICollectionView DataSource
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return todoItems.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TodoCollectionViewCell.reuseIdentifier, for: indexPath) as! TodoCollectionViewCell
        cell.delegate = self
        cell.configure(with: todoItems[indexPath.item])
        return cell
    }
    
    // MARK: - UICollectionViewDelegateFlowLayout
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let width = view.bounds.width - 32 // 减去左右内边距
        let item = todoItems[indexPath.item]
        
        if item.isExpanded {
            // 动态计算展开后的高度
            let titleHeight = item.title.height(withFont: .systemFont(ofSize: 18, weight: .medium), width: width - 32)
            let descriptionHeight = item.description.height(withFont: .systemFont(ofSize: 14), width: width - 32)
            return CGSize(width: width, height: titleHeight + descriptionHeight + 44) // 44是内边距和间距总和
        } else {
            // 折叠状态固定高度
            return CGSize(width: width, height: 60)
        }
    }
    
    // MARK: - TodoCellDelegate
    func toggleExpansion(for cell: TodoCollectionViewCell) {
        guard let indexPath = collectionView.indexPath(for: cell) else { return }
        
        // 切换展开状态
        todoItems[indexPath.item].isExpanded.toggle()
        
        // 平滑更新单元格布局(比reloadData更流畅)
        collectionView.performBatchUpdates({
            self.collectionView.reloadItems(at: [indexPath])
        }, completion: nil)
    }
}

// MARK: - 扩展:计算文本高度
extension String {
    func height(withFont font: UIFont, width: CGFloat) -> CGFloat {
        let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
        let boundingBox = self.boundingRect(
            with: constraintRect,
            options: .usesLineFragmentOrigin,
            attributes: [.font: font],
            context: nil
        )
        return ceil(boundingBox.height)
    }
}

3. 关键细节说明

  • 手势阈值:你可以修改handlePan方法中的30这个值,调整触发展开/折叠的滑动距离
  • 平滑动画:使用performBatchUpdates来更新单元格,能实现高度变化的平滑过渡
  • 动态高度计算:通过字符串扩展计算文本高度,确保展开后的单元格能完整显示所有内容
  • 解耦设计:使用代理模式让单元格通知控制器状态变化,避免直接依赖

内容的提问来源于stack exchange,提问作者user9334348

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.22 08:21:39