Удаление из отфильтрованных результатов поиска UISearchController

У меня есть tableView, получающий содержимое своей ячейки от CoreData, и я заменил SearchDisplayController (устаревший) новым SearchController. Я использую один и тот же контроллер tableView для представления как полного списка объектов, так и отфильтрованных/искомых объектов.

Мне удалось заставить поиск/фильтрацию работать нормально, и я могу перейти от отфильтрованного списка к подробным представлениям для этих элементов, а затем успешно отредактировать и сохранить изменения в отфильтрованном tableView. Моя проблема заключается в том, что удаление ячеек из отфильтрованного списка вызывает ошибку времени выполнения. Раньше с SearchDisplayController я мог сделать это легко, так как у меня был доступ к таблице результатов SearchDisplayController's, поэтому следующий (псевдо) код работал бы нормально:

func controllerDidChangeContent(controller: NSFetchedResultsController) {
    // If the search is active do this
          searchDisplayController!.searchResultsTableView.endUpdates()
    // else it isn't active so do this
          tableView.endUpdates()
    }
}

К сожалению, для UISearchController такого tableView нет, и я в растерянности. Я попытался сделать tableView.beginUpdates() и tableView.endUpdates() условными для tableView, не являющегося поисковым tableView, но безуспешно.

Для записи это мое сообщение об ошибке:

Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-3318.65/UITableView.m:1582

* ИЗМЕНИТЬ *

Мой tableView использует FetchedResultsController для заполнения себя из CoreData. Этот tableViewController также используется SearchController для отображения отфильтрованных результатов.

var searchController: UISearchController!

Затем в ViewDidLoad

searchController = UISearchController(searchResultsController: nil)
searchController.dimsBackgroundDuringPresentation = false
searchController.searchResultsUpdater = self
searchController.searchBar.sizeToFit()
self.tableView.tableHeaderView = searchController?.searchBar
self.tableView.delegate = self
self.definesPresentationContext = true

а также

func updateSearchResultsForSearchController(searchController: UISearchController) {
    let searchText = self.searchController?.searchBar.text
    if let searchText = searchText {
        searchPredicate = searchText.isEmpty ? nil : NSPredicate(format: "locationName contains[c] %@", searchText)
        self.tableView.reloadData()
    }
}

Что касается сообщения об ошибке, я не уверен, что могу добавить. Приложение зависает сразу после нажатия красной кнопки удаления (которая продолжает отображаться), которую можно открыть, проводя пальцем по экрану. Это журнал ошибок потока для 1–5. Приложение, кажется, зависает на номере 4.

#0  0x00000001042fab8a in objc_exception_throw ()
#1  0x000000010204b9da in +[NSException raise:format:arguments:] ()
#2  0x00000001027b14cf in -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] ()
#3  0x000000010311169a in -[UITableView _endCellAnimationsWithContext:] ()
#4  0x00000001019b16f3 in iLocations.LocationViewController.controllerDidChangeContent (iLocations.LocationViewController)(ObjectiveC.NSFetchedResultsController) -> () at /Users/neilmckay/Dropbox/Programming/My Projects/iLocations/iLocations/LocationViewController.swift:303
#5  0x00000001019b178a in @objc iLocations.LocationViewController.controllerDidChangeContent (iLocations.LocationViewController)(ObjectiveC.NSFetchedResultsController) -> () ()

Я надеюсь, что это поможет.

* ИЗМЕНИТЬ 2 *

override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
    if editingStyle == .Delete {
        let location: Location = self.fetchedResultsController.objectAtIndexPath(indexPath) as Location
        location.removePhotoFile()

        let context = self.fetchedResultsController.managedObjectContext
        context.deleteObject(location)

        var error: NSError? = nil
        if !context.save(&error) {
            abort()
        }
    }
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if self.searchPredicate == nil {
        let sectionInfo = self.fetchedResultsController.sections![section] as NSFetchedResultsSectionInfo
        return sectionInfo.numberOfObjects
    } else {
        let filteredObjects = self.fetchedResultsController.fetchedObjects?.filter() {
            return self.searchPredicate!.evaluateWithObject($0)
        }
        return filteredObjects == nil ? 0 : filteredObjects!.count
    }
}

// MARK: - NSFetchedResultsController methods

var fetchedResultsController: NSFetchedResultsController {
    if _fetchedResultsController != nil {
        return _fetchedResultsController!
    }

    let fetchRequest = NSFetchRequest()
    // Edit the entity name as appropriate.
    let entity = NSEntityDescription.entityForName("Location", inManagedObjectContext: self.managedObjectContext!)
    fetchRequest.entity = entity

    // Set the batch size to a suitable number.
    fetchRequest.fetchBatchSize = 20

    // Edit the sort key as appropriate.
    if sectionNameKeyPathString1 != nil {
        let sortDescriptor1 = NSSortDescriptor(key: sectionNameKeyPathString1!, ascending: true)
        let sortDescriptor2 = NSSortDescriptor(key: sectionNameKeyPathString2!, ascending: true)
        fetchRequest.sortDescriptors = [sortDescriptor1, sortDescriptor2]
    } else {
        let sortDescriptor = NSSortDescriptor(key: "firstLetter", ascending: true)
        fetchRequest.sortDescriptors = [sortDescriptor]
    }

    var sectionNameKeyPath: String
    if sectionNameKeyPathString1 == nil {
        sectionNameKeyPath = "firstLetter"
    } else {
        sectionNameKeyPath = sectionNameKeyPathString1!
    }

    // Edit the section name key path and cache name if appropriate.
    // nil for section name key path means "no sections".
    let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext!, sectionNameKeyPath: sectionNameKeyPath, cacheName: nil /*"Locations"*/)
    aFetchedResultsController.delegate = self
    _fetchedResultsController = aFetchedResultsController

    var error: NSError? = nil
    if !_fetchedResultsController!.performFetch(&error) {
        fatalCoreDataError(error)
    }

    return _fetchedResultsController!
}

var _fetchedResultsController: NSFetchedResultsController? = nil

func controllerWillChangeContent(controller: NSFetchedResultsController) {
    if searchPredicate == nil {
        tableView.beginUpdates()
    } else {
        (searchController.searchResultsUpdater as LocationViewController).tableView.beginUpdates()
    }

// tableView.beginUpdates() }

func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
    var tableView = UITableView()
    if searchPredicate == nil {
        tableView = self.tableView
    } else {
        tableView = (searchController.searchResultsUpdater as LocationViewController).tableView
    }

    switch type {
    case .Insert:
        tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
    case .Delete:
        tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
    default:
        return
    }
}

func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath) {
    var tableView = UITableView()
    if searchPredicate == nil {
        tableView = self.tableView
    } else {
        tableView = (searchController.searchResultsUpdater as LocationViewController).tableView
    }

    switch type {
    case .Insert:
        println("*** NSFetchedResultsChangeInsert (object)")
        tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)

    case .Delete:
        println("*** NSFetchedResultsChangeDelete (object)")
            tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
    case .Update:
        println("*** NSFetchedResultsChangeUpdate (object)")
        if searchPredicate == nil {
            let cell = tableView.cellForRowAtIndexPath(indexPath) as LocationCell
            let location = controller.objectAtIndexPath(indexPath) as Location
            cell.configureForLocation(location)
        } else {
            let cell = tableView.cellForRowAtIndexPath(searchIndexPath) as LocationCell
            let location = controller.objectAtIndexPath(searchIndexPath) as Location
            cell.configureForLocation(location)
        }
    case .Move:
        println("*** NSFetchedResultsChangeMove (object)")
        tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
        tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
    }
}

func controllerDidChangeContent(controller: NSFetchedResultsController) {
    if searchPredicate == nil {
        tableView.endUpdates()
    } else {
        (searchController.searchResultsUpdater as LocationViewController).tableView.endUpdates()
    }
}

person Magnas    schedule 26.12.2014    source источник
comment
Не могли бы вы поделиться более подробной информацией о конфигурации вашего SearchController, методе updateSearchResultsForSearchController и дополнительной информации о сообщении об ошибке? Спасибо.   -  person pbasdf    schedule 04.01.2015
comment
Я добавил дополнительную информацию к своему первоначальному ответу. Надеюсь это поможет.   -  person Magnas    schedule 05.01.2015
comment
Спасибо. Хотя ошибка возникает при вызове tableView.endUpdates(), я думаю, что проблема в другом. Я подозреваю, что проблема в том, что после удаления число, возвращаемое numberOfRowsInSection, не соответствует предыдущему значению (если вы удалили одну строку, она должна быть (предыдущее значение - 1). Проверьте (и/или добавьте к вашему вопросу) ваш код в commitEditingStyle и numberOfRowsInSection и другие методы делегата FRC.   -  person pbasdf    schedule 05.01.2015
comment
Добавлено больше кода. Я думаю, что вы правы насчет несоответствия количества строк, но сейчас я чувствую, что тону в этом коде.   -  person Magnas    schedule 07.01.2015


Ответы (1)


Проблема возникает из-за несоответствия между indexPath, используемым контроллером извлеченных результатов, и indexPath для соответствующей строки в tableView.

Пока контроллер поиска активен, существующий tableView повторно используется для отображения результатов поиска. Следовательно, ваша логика различает два tableViews:

if searchPredicate == nil {
    tableView = self.tableView
} else {
    tableView = (searchController.searchResultsUpdater as LocationViewController).tableView
}

не нужно. Это работает, потому что вы устанавливаете searchController.searchResultsUpdater = self при инициализации searchController, поэтому нет необходимости его менять, но в любом случае используется один и тот же tableView.

Разница заключается в том, как заполняется tableView, когда searchController активен. В этом случае это выглядит (из кода numberOfRowsInSection), как будто все отфильтрованные результаты отображаются в одном разделе. (Я предполагаю, что cellForRowAtIndexPath работает аналогично.) Предположим, вы удалили элемент в разделе 0, строке 7, в отфильтрованных результатах. Затем будет вызван commitEditingStyle с indexPath 0-7 и следующей строкой:

let location: Location = self.fetchedResultsController.objectAtIndexPath(indexPath) as Location

попытается получить объект с индексом 0-7 из FRC. Но элемент с индексом 0-7 FRC может быть совершенно другим объектом. Следовательно, вы удаляете не тот объект. Затем срабатывают методы делегата FRC и говорят tableView удалить строку с индексом 0-7. Теперь, если действительно удаленный объект НЕ был в отфильтрованных результатах, то количество строк не изменится, даже если строка была удалена: отсюда и ошибка.

Итак, чтобы исправить это, измените свой commitEditingStyle, чтобы он находил правильный объект для удаления, если searchController активен:

override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
    if editingStyle == .Delete {
        var location : Location
        if searchPredicate == nil {
            location = self.fetchedResultsController.objectAtIndexPath(indexPath) as Location
        } else {
            let filteredObjects = self.fetchedResultsController.fetchedObjects?.filter() {
                return self.searchPredicate!.evaluateWithObject($0)
            }
            location = filteredObjects![indexPath.row] as Location
        }
        location.removePhotoFile()

        let context = self.fetchedResultsController.managedObjectContext
        context.deleteObject(location)

        var error: NSError? = nil
        if !context.save(&error) {
            abort()
        }
    }
}

Я не смог проверить вышеизложенное; приносим свои извинения, если допустили некоторые ошибки. Но это должно, по крайней мере, указывать в правильном направлении; Надеюсь, поможет. Обратите внимание, что аналогичные изменения могут потребоваться в некоторых других методах делегата/источника данных tableView.

person pbasdf    schedule 07.01.2015
comment
Большое спасибо, что нашли время и усилия, чтобы объяснить это так ясно. Это очень ценится. - person Magnas; 11.01.2015