NSFetchedResultsController недоразумение - PullRequest
0 голосов
/ 02 марта 2020

Я реализовал обертку вокруг NSFetchedResultsController, которая преобразует управляемые объекты в массив некоторых простых структур Swift, которые будут использоваться в качестве наблюдаемых RxSwift. Давайте предположим, что у меня достаточно причин для создания такой оболочки вместо того, чтобы работать с NSFetchedResultsControllerDelegate напрямую.

Проблема: я не хочу воссоздавать весь массив разделов и все элементы каждый раз, когда controllerDidChangeContent(_:) называется, потому что это не оптимальное решение. Я хочу на самом деле внести частичные изменения в массив, изменяя элементы, которые фактически были изменены. Но похоже, что controller(_:didChange:at:for:newIndexPath:) передает изменения, адаптированные для использования в UITableView. Порядок применения изменений UITableView совершенно неочевиден для меня. Поэтому, когда я пытался применить изменения к моему массиву разделов, иногда я получал ошибку index out of range. Таким образом, я добавил некоторую проверку на выброс для индексов массива, но это не решает реальную проблему: вместо сбоев теперь иногда я дублирую элементы, что является неправильным поведением.

Может кто-нибудь объяснить мне, как правильно применить изменения от controller(_:didChange:at:for:newIndexPath:) к массиву.

private class DelegateHandler<Item>: NSObject, NSFetchedResultsControllerDelegate {
    weak var itemListObservable: RxItemListObservable<Item>?
    fileprivate var optionalSections = [ItemListSection<Item?>]()

    private var insertedSections = [Int]()
    private var deletedSections = [Int]()

    private var insertedItems = [Int: [Int]]()
    private var deletedItems = [Int: [Int]]()
    private var updatedItems = [Int: [Int]]()

    private func clear() {
        insertedSections = []
        deletedSections = []

        insertedItems = [:]
        deletedItems = [:]
        updatedItems = [:]
    }

    func controllerWillChangeContent(
        _ controller: NSFetchedResultsController<NSFetchRequestResult>
    ) {
        clear()
    }

    func controller(
        _ controller: NSFetchedResultsController<NSFetchRequestResult>,
        didChange sectionInfo: NSFetchedResultsSectionInfo,
        atSectionIndex sectionIndex: Int,
        for type: NSFetchedResultsChangeType
    ) {
        switch type {
        case .insert:
            insertedSections.append(sectionIndex)

        case .delete:
            deletedSections.append(sectionIndex)

        default:
            return
        }
    }

    func controller(
        _ controller: NSFetchedResultsController<NSFetchRequestResult>,
        didChange anObject: Any,
        at indexPath: IndexPath?,
        for type: NSFetchedResultsChangeType,
        newIndexPath: IndexPath?
    ) {
        func move(from indexPath: IndexPath, to newIndexPath: IndexPath) {
            insertedItems[newIndexPath.section, default: []].append(newIndexPath.item)
            deletedItems[indexPath.section, default: []].append(indexPath.item)
        }

        switch type {
        case .insert:
            let path = newIndexPath!
            insertedItems[path.section, default: []].append(path.item)

        case .delete:
            let path = indexPath!
            deletedItems[path.section, default: []].append(path.item)

        case .move:
            move(from: indexPath!, to: newIndexPath!)

        case .update:
            let indexPath = indexPath!

            if let newIndexPath = newIndexPath,
                indexPath != newIndexPath {
                move(from: indexPath, to: newIndexPath)
            } else {
                updatedItems[indexPath.section, default: []].append(indexPath.item)
            }

        @unknown default:
            return
        }
    }

    func controllerDidChangeContent(
        _ controller: NSFetchedResultsController<NSFetchRequestResult>
    ) {
        guard let itemListObservable = itemListObservable else { return }

        let sections: [ItemListSection<Item?>]
        do {
            sections = try processChanges(
                itemListObservable: itemListObservable
            )
        } catch {
            let resultsController = itemListObservable.resultsController

            sections = (resultsController.sections ?? [])
                .enumerated()
                .map { (sectionIndex, sectionInfo) in
                    let items = (0 ..< sectionInfo.numberOfObjects)
                        .map { index -> Item? in
                            let indexPath = IndexPath(item: index, section: sectionIndex)
                            let object = resultsController.object(at: indexPath)

                            return itemListObservable.converter(object)
                    }

                    return .init(name: sectionInfo.name,
                                 indexTitle: sectionInfo.indexTitle,
                                 items: items)
            }
        }

        optionalSections = sections
        itemListObservable.updateVisibleSections(sections)
        clear()
    }

    private func processChanges(
        itemListObservable: RxItemListObservable<Item>
    ) throws -> [ItemListSection<Item?>] {
        var sections = optionalSections

        for sectionIndex in insertedSections + deletedSections {
            insertedItems[sectionIndex] = nil
            deletedItems[sectionIndex] = nil
            updatedItems[sectionIndex] = nil
        }

        // Reload items
        for (sectionIndex, itemIndexes) in updatedItems {
            for itemIndex in itemIndexes {
                let indexPath = IndexPath(item: itemIndex, section: sectionIndex)
                let object = itemListObservable.resultsController.object(at: indexPath)
                let item: Item? = itemListObservable.converter(object)

                try sections.checkAccess(at: sectionIndex)
                try sections[sectionIndex].items.checkAccess(at: itemIndex)
                sections[sectionIndex].items[itemIndex] = item
            }
        }

        // Delete items
        for (sectionIndex, itemIndexes) in deletedItems {
            for itemIndex in itemIndexes.sorted(by: >) {
                try sections.checkAccess(at: sectionIndex)
                try sections[sectionIndex].items.checkAccess(at: itemIndex)
                sections[sectionIndex].items.remove(at: itemIndex)
            }
        }

        // Delete sections
        for sectionIndex in deletedSections.sorted(by: >) {
            try sections.checkAccess(at: sectionIndex)
            sections.remove(at: sectionIndex)
        }

        // Insert sections
        for sectionIndex in insertedSections.sorted(by: <) {
            guard let sectionInfo = itemListObservable
                .resultsController
                .sections?[sectionIndex] else {
                    continue
            }

            let items = (0 ..< sectionInfo.numberOfObjects).map { itemIndex -> Item? in
                let indexPath = IndexPath(item: itemIndex, section: sectionIndex)
                let object = itemListObservable.resultsController.object(at: indexPath)

                return itemListObservable.converter(object)
            }

            let section = ItemListSection(name: sectionInfo.name,
                                          indexTitle: sectionInfo.indexTitle,
                                          items: items)

            try sections.checkInsert(at: sectionIndex)
            sections.insert(section, at: sectionIndex)
        }

        // Insert items
        for (sectionIndex, itemIndexes) in insertedItems {
            for itemIndex in itemIndexes.sorted(by: <) {
                let indexPath = IndexPath(item: itemIndex, section: sectionIndex)
                let object = itemListObservable.resultsController.object(at: indexPath)
                let item: Item? = itemListObservable.converter(object)

                try sections.checkAccess(at: sectionIndex)
                try sections[sectionIndex].items.checkInsert(at: itemIndex)
                sections[sectionIndex].items.insert(item, at: itemIndex)
            }
        }

        return sections
    }
}

// MARK: - Utils

private extension Array {
    enum ArrayError: Error {
        case outOfRange
    }

    func checkAccess(at index: Int) throws {
        guard index < count else {
            throw ArrayError.outOfRange
        }
    }

    func checkInsert(at index: Int) throws {
        guard index <= count else {
            throw ArrayError.outOfRange
        }
    }
}

Обновление # 1 Я не могу использовать Diffable Data Source, потому что мне нужно поддерживать iOS 12.

...