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

UIKit横向滑动卡片功能异常求助:左滑移除/右滑追加问题

修复UIKit横向滑动卡片(左滑移除/右滑移至末尾)的问题

核心问题分析

你的代码存在几个关键问题导致功能失效:

  • 手势总位移计算错误:每次移动后重置了手势translation,导致结束时无法获取完整滑动距离,阈值判断失效
  • 堆叠卡片的层级错误:未设置zIndex,下层卡片可能拦截手势,且显示层级混乱
  • 右滑逻辑缺失:仅重置卡片位置,未实现将卡片移至数据源末尾的核心操作
  • 卡片位置重置错误:使用collectionView.center(父视图坐标系)而非cell所在的collectionView内部坐标系
  • 手势范围失控:给整个collectionView加手势,导致下层卡片也能响应滑动

分步修复方案

1. 修复手势位移计算

添加属性记录总滑动距离,避免因重置translation丢失完整位移数据,同时限制仅最上层卡片响应手势:

// 添加属性记录手势总位移
private var totalPanX: CGFloat = 0

@objc func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
    let location = gesture.location(in: collectionView)
    // 仅允许最上层卡片(index=0)响应手势
    guard let indexPath = collectionView.indexPathForItem(at: location),
          indexPath.item == 0,
          let cell = collectionView.cellForItem(at: indexPath) as? SwipeCardCell else {
        return
    }
    
    let translation = gesture.translation(in: collectionView)
    totalPanX += translation.x
    cell.center.x += translation.x
    gesture.setTranslation(.zero, in: collectionView)
    
    if gesture.state == .ended {
        let threshold: CGFloat = 100
        let direction: CGFloat = totalPanX > 0 ? 1 : -1
        if abs(totalPanX) > threshold {
            if direction < 0 {
                swipeLeftAction(cell, indexPath)
            } else {
                swipeRightAction(cell, indexPath)
            }
        } else {
            UIView.animate(withDuration: 0.3) {
                // 使用collectionView内部的中点坐标
                cell.center = CGPoint(x: self.collectionView.bounds.midX, y: self.collectionView.bounds.midY)
            }
        }
        // 重置总位移
        totalPanX = 0
    }
}

2. 修复卡片层级(TinderLayout)

在布局属性中设置zIndex,确保最上层卡片(索引最小)拥有最高显示层级:

override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
    guard let collectionView = collectionView else { return nil }
    
    let attr = UICollectionViewLayoutAttributes(forCellWith: indexPath)
    
    attr.size = itemSize
    // 使用collectionView内部的中点坐标
    let center = CGPoint(x: collectionView.bounds.midX, y: collectionView.bounds.midY)
    attr.center = center
    
    // 调整堆叠位置,同时设置zIndex确保上层卡片在最前面
    let itemOffset = CGFloat(indexPath.item) * interItemSpacing
    attr.transform = CGAffineTransform(translationX: 0, y: -itemOffset)
    // 索引越小,zIndex越高,显示在最上层
    attr.zIndex = collectionView.numberOfItems(inSection: 0) - indexPath.item
    
    return attr
}

3. 实现右滑追加逻辑

修改swipeRightAction,完成将当前卡片移至数据源末尾的核心操作:

func swipeRightAction(_ cell: SwipeCardCell, _ indexPath: IndexPath) {
    UIView.animate(withDuration: 0.3, animations: {
        cell.center = CGPoint(x: self.collectionView.bounds.midX, y: self.collectionView.bounds.midY)
    }, completion: { _ in
        // 将当前卡片移至数据源末尾
        let movedCard = self.dataSource.remove(at: indexPath.item)
        self.dataSource.append(movedCard)
        // 刷新collectionView触发布局更新
        self.collectionView.reloadData()
    })
}

4. 优化移除卡片后的布局

删除卡片后强制刷新布局,确保下层卡片正确上移:

func swipeLeftAction(_ cell: SwipeCardCell, _ indexPath: IndexPath) {
    UIView.animate(withDuration: 0.3, animations: {
        cell.center.x -= 1000
    }, completion: { _ in
        self.removedCards.append((self.dataSource[indexPath.item], indexPath))
        self.dataSource.remove(at: indexPath.item)
        self.collectionView.deleteItems(at: [indexPath])
        // 强制布局更新,确保下层卡片上移
        self.collectionView.collectionViewLayout.invalidateLayout()
    })
}

完整修复后的代码

class SwipeCardComponentVC: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
    
    @IBOutlet weak var collectionView: UICollectionView!
    
    var dataSource = ["Card 1", "Card 2", "Card 3", "Card 4", "Card 5"]
    var removedCards: [(card: String, indexPath: IndexPath)] = []
    private var totalPanX: CGFloat = 0 // 记录手势总位移
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let layout = TinderLayout()
        collectionView.collectionViewLayout = layout
        collectionView.dataSource = self
        collectionView.delegate = self
        
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
        collectionView.addGestureRecognizer(panGesture)
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return dataSource.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SwipeCardCell", for: indexPath) as! SwipeCardCell
        let color = ["DBA979","BACD92","9BB0C1","8E7AB5","80BCBD","C3E2C2","AFC8AD","D2E0FB"]
        cell.bgView.backgroundColor = hexStringToUIColor(hex: color[indexPath.row % color.count])
        
        cell.center = CGPoint(x: collectionView.bounds.midX, y: collectionView.bounds.midY)
        return cell
    }
    
    @objc func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
        let location = gesture.location(in: collectionView)
        // 仅允许最上层卡片(index=0)响应手势
        guard let indexPath = collectionView.indexPathForItem(at: location),
              indexPath.item == 0,
              let cell = collectionView.cellForItem(at: indexPath) as? SwipeCardCell else {
            return
        }
        
        let translation = gesture.translation(in: collectionView)
        totalPanX += translation.x
        cell.center.x += translation.x
        gesture.setTranslation(.zero, in: collectionView)
        
        if gesture.state == .ended {
            let threshold: CGFloat = 100
            let direction: CGFloat = totalPanX > 0 ? 1 : -1
            if abs(totalPanX) > threshold {
                if direction < 0 {
                    swipeLeftAction(cell, indexPath)
                } else {
                    swipeRightAction(cell, indexPath)
                }
            } else {
                UIView.animate(withDuration: 0.3) {
                    cell.center = CGPoint(x: self.collectionView.bounds.midX, y: self.collectionView.bounds.midY)
                }
            }
            totalPanX = 0
        }
    }
    
    func swipeLeftAction(_ cell: SwipeCardCell, _ indexPath: IndexPath) {
        UIView.animate(withDuration: 0.3, animations: {
            cell.center.x -= 1000
        }, completion: { _ in
            self.removedCards.append((self.dataSource[indexPath.item], indexPath))
            self.dataSource.remove(at: indexPath.item)
            self.collectionView.deleteItems(at: [indexPath])
            self.collectionView.collectionViewLayout.invalidateLayout()
        })
    }
    
    func swipeRightAction(_ cell: SwipeCardCell, _ indexPath: IndexPath) {
        UIView.animate(withDuration: 0.3, animations: {
            cell.center = CGPoint(x: self.collectionView.bounds.midX, y: self.collectionView.bounds.midY)
        }, completion: { _ in
            let movedCard = self.dataSource.remove(at: indexPath.item)
            self.dataSource.append(movedCard)
            self.collectionView.reloadData()
        })
    }
}

class TinderLayout: UICollectionViewLayout {
    private let itemSize = CGSize(width: 400, height: 600)
    private let interItemSpacing: CGFloat = 10
    
    override func prepare() {
        super.prepare()
    }

    override var collectionViewContentSize: CGSize {
        guard let collectionView = collectionView else { return .zero }
        return collectionView.bounds.size
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let collectionView = collectionView else { return nil }
        
        var attributes: [UICollectionViewLayoutAttributes] = []
        let numberOfItems = collectionView.numberOfItems(inSection: 0)
        
        for index in 0..<numberOfItems {
            let indexPath = IndexPath(item: index, section: 0)
            if let attr = layoutAttributesForItem(at: indexPath) {
                attributes.append(attr)
            }
        }
        
        return attributes
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        guard let collectionView = collectionView else { return nil }
        
        let attr = UICollectionViewLayoutAttributes(forCellWith: indexPath)
        
        attr.size = itemSize
        let center = CGPoint(x: collectionView.bounds.midX, y: collectionView.bounds.midY)
        attr.center = center
        
        let itemOffset = CGFloat(indexPath.item) * interItemSpacing
        attr.transform = CGAffineTransform(translationX: 0, y: -itemOffset)
        // 设置zIndex确保上层卡片显示在最前面
        attr.zIndex = collectionView.numberOfItems(inSection: 0) - indexPath.item
        
        return attr
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 17:05:55