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

Swift UITableView拖拽单元格到分区页脚附近时数组越界崩溃

问题现象

多分区UITableView实现拖拽交互时,常规拖拽运行正常,但将单元格拖拽到仅含单行的分区页脚附近时,应用触发崩溃,异常信息如下:

Swift/Array.swift:405: Fatal error: Array index is out of range

调试定位到崩溃发生在tableView(_:moveRowAt:to:)方法执行数据更新时,此时destinationIndexPath.row取值为1,但目标分区仅存在1行数据,数组索引越界。

触发崩溃的交互示例:
触发崩溃的交互示例

原有实现代码如下:

class ViewController: UIViewController {
    
    var data = [["1"],["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q"]]
    
    let tableView: UITableView = {
        let tableView = UITableView()
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.backgroundColor = .red
        tableView.rowHeight = 55
        tableView.sectionFooterHeight = 50
        return tableView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(tableView)
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
        
        tableView.delegate = self
        tableView.dataSource = self
        tableView.dragDelegate = self
        tableView.dragInteractionEnabled = true
        tableView.register(ShippedProductCell.self, forCellReuseIdentifier: ShippedProductCell.cellID)
    }
    
}

extension ViewController: UITableViewDelegate, UITableViewDataSource, UITableViewDragDelegate {
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return data.count
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return data[section].count
    }
    
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: ShippedProductCell.cellID) as! ShippedProductCell
        let shippedProductData = data[indexPath.section][indexPath.row]
        cell.label.text = shippedProductData
        return cell
    }
    
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return "Section \(section)"
    }
    
    func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
        return "Footer"
    }
    
    func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        guard indexPath.row != data[indexPath.section].count - 1 else { return [] }
        let dragItem = UIDragItem(itemProvider: NSItemProvider())
        dragItem.localObject = data[indexPath.section][indexPath.row]
        return [ dragItem ]
    }
    
    func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        let mover = data[sourceIndexPath.section].remove(at: sourceIndexPath.row)
        data[destinationIndexPath.section].insert(mover, at: destinationIndexPath.row)
        print(data)
    }
        
}
问题原因

UITableView 内置的拖拽逻辑在识别到拖拽位置悬停在分区页脚区域时,会自动生成一个提议的目标IndexPath,其row值等于对应分区的当前总行数。这个路径在跨分区拖拽插入到分区末尾时是合法的(数组插入操作允许at参数等于数组当前count,对应插入到末尾),但在同分区拖拽场景下,执行移动逻辑时会先移除源位置的单元格,对应分区数组元素数减1,此时系统提议的row值就会超出数组的合法插入范围,触发越界崩溃。
原有代码未实现系统提供的目标路径修正代理方法,直接使用系统给出的未做边界校验的destinationIndexPath执行数组操作,是崩溃的核心原因。

解决方案

实现UITableViewDelegate协议中的tableView(_:targetIndexPathForMoveFromRowAt:toProposedIndexPath:)方法,对系统返回的提议目标路径做边界校验,将非法的row值修正到目标分区的合法插入范围内,修正代码如下:

func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
    // 获取目标分区当前总行数
    let targetSectionRowCount = tableView.numberOfRows(inSection: proposedDestinationIndexPath.section)
    let maxValidRow: Int
    if sourceIndexPath.section == proposedDestinationIndexPath.section {
        // 同分区移动:移除源行后数组长度减1,最大合法插入位置为原行数-1
        maxValidRow = targetSectionRowCount - 1
    } else {
        // 跨分区移动:目标分区数组长度不变,最大合法插入位置为目标分区当前行数(对应末尾插入)
        maxValidRow = targetSectionRowCount
    }
    // 钳位row值到0...maxValidRow的合法范围
    let safeRow = min(max(proposedDestinationIndexPath.row, 0), maxValidRow)
    return IndexPath(row: safeRow, section: proposedDestinationIndexPath.section)
}

将上述方法添加到ViewController的UITableViewDelegate扩展中,即可修复越界崩溃问题。

补充说明:当前代码在tableView(_:itemsForBeginning:at:)方法中添加了guard indexPath.row != data[indexPath.section].count - 1 else { return [] }判断,该逻辑会禁止拖拽每个分区的最后一行,如果这不是特意设计的交互限制,可以删除该判断,否则第一个分区仅有的一行会永远无法被拖拽。

内容的提问来源于stack exchange,提问作者Leonardo Luna Palomino

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 13:45:35