首页 > 解决方案 > RxSwift 不同库崩溃`NSInternalInconsistencyException`

问题描述

我现在正在尝试在一个基本示例中使用Diffingwith 。RxSwift我正在使用差异库。

这是我的Interactor+ ViewModels 关联:

import Foundation
import RxSwift
import RxCocoa

class Interactor {

    var items = [
            [1,5,6,7,4,6,7,1,5],
            [1,5,2,1,0,6,7],
    ]

    let viewModel: BehaviorRelay<ViewModel>

    var currentObjects: Int = 0 {
        didSet {
            viewModel.accept(.init(with: .loaded(items[currentObjects])))
        }
    }

    init() {
        viewModel = BehaviorRelay(value: .init(with: .initialized))
    }

    func fetchValue() {
        currentObjects = currentObjects == 0 ? 1 : 0
    }


}

struct ViewModel {

    enum ViewModelType: Equatable {
        case cell(CellViewModel)
    }

    enum State {
        case initialized
        case loaded([Int])
    }

    let state: State
    let viewModels: [ViewModelType]

    init(with state: State) {
        self.state = state
        switch state {
        case .initialized: viewModels = []
        case .loaded(let values):
            viewModels = CellViewModel.from(values).map(ViewModelType.cell)
        }
    }
}

extension ViewModel: Equatable {

    static func ==(left: ViewModel, right: ViewModel) -> Bool {
        return left.state == left.state
    }
}

extension ViewModel.State: Equatable {

    static func ==(left: ViewModel.State, right: ViewModel.State) -> Bool {
        switch (left, right) {
        case (.initialized, .initialized): return true
        case let (.loaded(l), .loaded(r)): return l == r
        default: return false
        }
    }
}

struct CellViewModel {
    let description: String
}

extension CellViewModel {

    static func from(_ values: [Int]) -> [CellViewModel] {
        return values.map { CellViewModel(description: String($0)) }
    }
}

extension CellViewModel: Equatable {

    static func ==(left: CellViewModel, right: CellViewModel) -> Bool {
        return left.description == right.description
    }
}

现在对于视图,我使用了一个简单的 `UITableView

import UIKit
import Differ
import RxSwift

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        ...

        interactor
            .viewModel
            .asObservable()
            .scan([], accumulator: { (previous, current) in
              Array(previous + [current]).suffix(2)
            })
            .map({ (arr) -> (previous: ViewModel?, current: ViewModel) in
              (arr.count > 1 ? arr.first : nil, arr.last!)
            }).subscribe(onNext: { [weak self] (previous, current) in
                if let prev = previous {
                    print("Previous => State: \(prev.state) | ViewModelType.count: \(prev.viewModels.count)")
                } else {
                    print("Previous => State: nil | ViewModelType.count: nil")
                }
                print("Current => State: \(current.state) | ViewModelType.count: \(current.viewModels.count)")
                guard let strongSelf = self else { return }
                DispatchQueue.main.async {
                    strongSelf.tableView.animateRowChanges(oldData: previous?.viewModels ?? [], newData: current.viewModels)
                }
            }).disposed(by: disposeBag)

        interactor.fetchValue()
    }

    @objc
    func onRefresh() {
        interactor.fetchValue()
    }
}

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return interactor.viewModel.value.viewModels.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cellViewModel = interactor.viewModel.value.viewModels[indexPath.row]
        switch cellViewModel {
        case .cell(let viewModel):
            let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
            cell.textLabel?.text = viewModel.description
            return cell
        }
    }
}

在一切都符合 Equatable 的情况下,我认为这项工作会完成,但我遇到了一个NSInternalInconsistencyException例外。

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (7) must be equal to the number of rows contained in that section before the update (7), plus or minus the number of rows inserted or deleted from that section (7 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'

我在崩溃前检查来自 Rx 的打印显示:

Previous => State: nil | ViewModelType.count: nil
Current => State: initialized | ViewModelType.count: 0
Previous => State: initialized | ViewModelType.count: 0
Current => State: loaded([1, 5, 2, 1, 0, 6, 7]) | ViewModelType.count: 7

从逻辑的角度来看,流程对我来说看起来不错。我错过了什么吗?


编辑 2019/10/29

我制作了另一个版本,但不RxSwift知道问题是否出在RxSwift

protocol InteractorDelegate: class {
    func viewModelDidChange(_ old: ViewModel?, _ new: ViewModel)
}

class Interactor {

    weak var delegate: InteractorDelegate?

    var items = [
            [1,5,6,7,4,6,7,1,5],
            [1,5,2,1,0,6,7],
    ]

    var viewModel: ViewModel? {
        didSet {
            delegate?.viewModelDidChange(oldValue, viewModel!)
        }
    }

    var currentObjects: Int = 0 {
        didSet {
            viewModel = .init(with: .loaded(items[currentObjects]))
        }
    }

    init() {
        viewModel = .init(with: .initialized)
    }

    func fetchValue() {
        currentObjects = currentObjects == 0 ? 1 : 0
    }
}

对于ViewController

extension ViewController: InteractorDelegate {

    func viewModelDidChange(_ old: ViewModel?, _ new: ViewModel) {

        if let prev = old {
            print("Previous => State: \(prev.state) | ViewModelType.count: \(prev.viewModels.count)")
        } else {
            print("Previous => State: nil | ViewModelType.count: nil")
        }
        print("Current => State: \(new.state) | ViewModelType.count: \(new.viewModels.count)")
        DispatchQueue.main.async {
            self.tableView.animateRowChanges(oldData: old?.viewModels ?? [], newData: new.viewModels)
        }
    }
}

似乎没有以下问题,问题仍然相同RxSwift

Previous => State: initialized | ViewModelType.count: 0
Current => State: loaded([1, 5, 2, 1, 0, 6, 7]) | ViewModelType.count: 7
2019-10-29 13:45:56.636678+0900 TestDiffer[93631:21379549] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (7) must be equal to the number of rows contained in that section before the update (7), plus or minus the number of rows inserted or deleted from that section (7 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'

Equatable我的一致性有问题吗?


编辑 2019/10/29 #2

经过新的测试,我看到只有在之前的值为空时才会发生崩溃。

像这样更改代码一切正常:

extension ViewController: InteractorDelegate {

    func viewModelDidChange(_ old: ViewModel?, _ new: ViewModel) {

        DispatchQueue.main.async {
            guard old != nil && !old!.viewModels.isEmpty else {
                self.tableView.reloadData()
                return
            }
            self.tableView.animateRowChanges(oldData: old!.viewModels, newData: new.viewModels)
        }
    }
}

放回 RxSwift 而不是委托时同样成功。

即使它现在按预期工作。我仍然在质疑为什么当数组为空时差异不起作用。之前的值是空的,而具有 2 个元素的新值应该被分析为 2 次插入,不是吗?这里发生了什么?

标签: iosswiftuitableviewrx-swift

解决方案


如果您实现RxTableViewDataSourceType并将其传递给表视图的items操作员会更好。执行此操作的示例项目位于:https ://github.com/danielt1263/RxMultiCounter

我在示例代码中使用了 DifferenceKit,因此您必须对tableView(_:observedEvent:)方法进行一些更改,但这应该有助于为您指明正确的方向。

class RxSimpleAnimatableDataSource<E, Cell>: NSObject, 
    RxTableViewDataSourceType, 
    UITableViewDataSource 
    where E: Differentiable, Cell: UITableViewCell 
{
    typealias Element = [E]

    init(identifier: String, with animation: UITableView.RowAnimation = .automatic, configure: @escaping (Int, E, Cell) -> Void) {
        self.identifier = identifier
        self.animation = animation
        self.configure = configure
    }

    func tableView(_ tableView: UITableView, observedEvent: Event<Element>) {
        let source = values
        let target = observedEvent.element ?? []
        let changeset = StagedChangeset(source: source, target: target)
        tableView.reload(using: changeset, with: animation) { data in
            self.values = data
        }
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return values.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) as! Cell
        let row = indexPath.row
        configure(row, values[row], cell)
        return cell
    }

    let identifier: String
    let animation: UITableView.RowAnimation
    let configure: (Int, E, Cell) -> Void
    var values: Element = []
}

推荐阅读