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

Swift中UITableView拖拽超第10个单元格时数组越界问题求助

问题:UITableView拖拽时数组越界错误(index 191 beyond bounds [0 .. 8])

错误信息:Thread 1: *** -[__NSFrozenArrayM objectAtIndex:]: index 191 beyond bounds [0 .. 8]
我有一个可容纳100多个单元格的大型UITableView,希望实现单元格拖拽功能。前10个单元格拖拽正常,但超过该索引后出现上述数组越界错误。我使用了RxDataSources,尝试过状态机方案但未解决问题,以下是我的代码:

import Foundation
import RxSwift
import RxDataSources
import SnapKit
import RxRelay
import RxCocoa

class BindableTableViewController<T: AnimatableSectionModelType>: UIViewController, UITableViewDelegate, UITableViewDragDelegate {
    func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        let dragItem = UIDragItem(itemProvider: NSItemProvider())
        dragItem.localObject = tableView.visibleCells[indexPath.row]
        return [dragItem]
    }
    
    var items: RxSwift.Observable<[T]>?
    
    var onItemSelected: ((IndexPath) -> Void)?
    
    var onItemMoved: ((ItemMovedEvent) -> Void)?
    
    var onItemRemoved: ((IndexPath) -> Void)?
    
    var tableView: UITableView
    
    var classesToRegister: [String: AnyClass]
    
    weak var dataSource: RxTableViewSectionedAnimatedDataSource<T>?
    
    var heightForRow: CGFloat
    
    var supportsDragging: Bool
    
    private let disposeBag = DisposeBag()
    
    init(items: RxSwift.Observable<[T]>?,
         heightForRow: CGFloat,
         tableViewColor: UIColor = .clear,
         onItemSelected: ((IndexPath) -> Void)? = nil,
         onItemMoved: ((ItemMovedEvent) -> Void)? = nil,
         onItemRemoved: ((IndexPath) -> Void)? = nil,
         dataSource: RxTableViewSectionedAnimatedDataSource<T>,
         classesToRegister: [String: AnyClass],
         supportsDragging: Bool = false,
         style: UITableView.Style? = .plain) {
        self.heightForRow = heightForRow
        self.items = items
        self.dataSource = dataSource
        self.classesToRegister = classesToRegister
        self.onItemSelected = onItemSelected
        self.onItemMoved = onItemMoved
        self.onItemRemoved = onItemRemoved
        self.supportsDragging = supportsDragging
        tableView = UITableView(frame: .zero, style: style ?? .plain)
        super.init(nibName: nil, bundle: nil)
        tableView.backgroundColor = tableViewColor
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return heightForRow
    }
        
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        for (key, value) in classesToRegister {
            tableView.register(value, forCellReuseIdentifier: key)
        }
        
        items?.bind(to: tableView.rx.items(dataSource: dataSource!))
            .disposed(by: disposeBag)
        
        items?.subscribe(onNext: { sections in
            print(sections.first?.items.map({ $0.identity }))
        }).disposed(by: disposeBag)
                
        items?.takeLast(2).subscribe(onNext: { [weak self] sectionModels in
            sectionModels.first?.items.forEach({ el in
                print(el.identity)
            })
            let initialState = SectionedTableViewState(sections: sectionModels)
            
            let deleteCommand = self?.tableView.rx.itemDeleted.asObservable()
                .map(TableViewEditingCommand.DeleteItem)
            
            let movedCommand = self?.tableView.rx.itemMoved
                .map(TableViewEditingCommand.MoveItem)
            
            if let deleteCommand = deleteCommand, let movedCommand = movedCommand, !sectionModels.isEmpty, let self = self {
                Observable.of(deleteCommand, movedCommand)
                    .merge()
                    .scan(initialState) { (state: SectionedTableViewState, command: TableViewEditingCommand) -> SectionedTableViewState in
                        return state.execute(command: command)
                    }
                    .startWith(initialState)
                    .map {
                        $0.sections
                    }
                    .share(replay: 1)
                    .bind(to: self.tableView.rx.items(dataSource: self.dataSource!))
                    .disposed(by: self.disposeBag)
            }
        }).disposed(by: disposeBag)
        
        tableView.rx.itemSelected
            .asDriver()
            .drive(onNext: onItemSelected)
            .disposed(by: disposeBag)
        
        view.addSubview(tableView)
        tableView.snp.makeConstraints { make in
            make.size.equalToSuperview()
        }
        
        tableView.separatorColor = .clear
        
        if supportsDragging {
            tableView.dragDelegate = self
            tableView.rx.itemMoved
                .asDriver()
                .drive(onNext: onItemMoved)
                .disposed(by: disposeBag)
        }
        tableView.rx.setDelegate(self).disposed(by: disposeBag)
    }
}

enum TableViewEditingCommand {
    case MoveItem(event: ItemMovedEvent)
    case DeleteItem(IndexPath)
}

struct SectionedTableViewState<T: AnimatableSectionModelType> {
    fileprivate var sections: [T]
    
    init(sections: [T]) {
        self.sections = sections
    }
    
    func execute(command: TableViewEditingCommand) -> SectionedTableViewState {
        switch command {
        case .DeleteItem(let indexPath):
            var sections = self.sections
            var items = sections[indexPath.section].items
            items.remove(at: indexPath.row)
            sections[indexPath.section] = T(original: sections[indexPath.section], items: items)
            return SectionedTableViewState(sections: sections)
        case .MoveItem(let moveEvent):
            var sections = self.sections
            
            var sourceItems = sections[moveEvent.sourceIndex.section].items
            var destinationItems = sections[moveEvent.destinationIndex.section].items
            
            
            if moveEvent.sourceIndex.section == moveEvent.destinationIndex.section {
                destinationItems.insert(destinationItems.remove(at: moveEvent.sourceIndex.row),
                                        at: moveEvent.destinationIndex.row)
                let destinationSection = T(original: sections[moveEvent.destinationIndex.section], items: destinationItems)
                sections[moveEvent.sourceIndex.section] = destinationSection
                
                return SectionedTableViewState(sections: sections)
            } else {
                let item = sourceItems.remove(at: moveEvent.sourceIndex.row)
                destinationItems.insert(item, at: moveEvent.destinationIndex.row)
                let sourceSection = T(original: sections[moveEvent.sourceIndex.section], items: sourceItems)
                let destinationSection = T(original: sections[moveEvent.destinationIndex.section], items: destinationItems)
                sections[moveEvent.sourceIndex.section] = sourceSection
                sections[moveEvent.destinationIndex.section] = destinationSection
                
                return SectionedTableViewState(sections: sections)
            }
        }
    }
}

问题定位与修复

1. 核心错误修复

越界的直接原因是拖拽方法中获取单元格的方式错误:
tableView.visibleCells[indexPath.row] 是错误的,visibleCells只包含当前屏幕上显示的单元格,其数组长度远小于总单元格数(比如只有8个可见时,索引191必然越界)。

替换为正确的获取方式:

func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
    guard let cell = tableView.cellForRow(at: indexPath) else {
        return []
    }
    let dragItem = UIDragItem(itemProvider: NSItemProvider())
    dragItem.localObject = cell
    return [dragItem]
}

tableView.cellForRow(at:) 会根据indexPath获取对应单元格,无论是否在屏幕可见范围内(不可见时tableView会提前加载该单元格)。

2. 其他潜在问题修复

  • 移除重复数据源绑定:viewDidLoad中同时存在items?.bind(to: tableView.rx.items(...))和状态机的绑定,这会导致数据源被重复驱动,引发数据不一致。保留状态机的绑定逻辑即可。
  • 修复状态机初始化逻辑:items?.takeLast(2).subscribe 写法错误,takeLast(2)需要序列完成才会发送数据,若items是持续更新的序列(比如Relay),该订阅永远不会触发。建议重构为:
items?.startWith([])
    .flatMapLatest { initialSections in
        Observable.merge(
            self.tableView.rx.itemDeleted.asObservable().map(TableViewEditingCommand.DeleteItem),
            self.tableView.rx.itemMoved.asObservable().map(TableViewEditingCommand.MoveItem)
        )
        .scan(SectionedTableViewState(sections: initialSections)) { state, command in
            state.execute(command: command)
        }
        .startWith(SectionedTableViewState(sections: initialSections))
        .map { $0.sections }
    }
    .bind(to: tableView.rx.items(dataSource: dataSource!))
    .disposed(by: disposeBag)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 17:17:41