1

I have a big tableView that can fit more than 100 cells, I want these cells to be draggable and it works fine with first 10 cells, but after that index I got this error. I use RxDataSources, I saw that someone wrote that stateMachine will help with this issue, but it didn't work for me. Here is my code:

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)
            }
        }
    }
}
Rebigo
  • 13
  • 3
  • Just a hunch that this is the culprit: `tableView.visibleCells[indexPath.row]`. Seems like you got 8 cells visible at a time but you try to access the whole range. Furthermore, relying on cells (which are by nature ephemeral - and certainly not suitable for a datasource) maybe is not the best choice. You could have a list of separate models for your drag items. – Alladinian Jun 20 '23 at 09:56
  • @Alladinian thanks, it really works ! I just replaced it with cellForRowAt and and crash-error is gone. – Rebigo Jun 20 '23 at 10:34

0 Answers0