NSFetchedResultsController 每个部分的不同排序描述符
Posted
技术标签:
【中文标题】NSFetchedResultsController 每个部分的不同排序描述符【英文标题】:NSFetchedResultsController different sort descriptors for each section 【发布时间】:2016-01-28 11:17:32 【问题描述】:我有具有 NSDate 属性的对象,我需要将它们分成两部分(第一部分 - 未来事件,第二部分 - 历史事件),然后对于第一部分,我需要按日期属性按升序对它们进行排序,第二部分部分按降序排列。知道如何进行排序吗?
【问题讨论】:
【参考方案1】:假设您使用的是NSFetchedResultsController
,则基础提取必须以一种或另一种方式排序。我可以想到两种不同的解决方案:
使用两个具有互补谓词的独立 FRC,以便一个处理过去的事件,而另一个处理未来的事件。一个将按升序排序,另一个将按降序排序。问题是两个 FRC 都会为第 0 部分生成 indexPaths
。因此,您需要重新映射第二个 FRC 的 indexPaths
以使用 tableView 的第 1 部分。例如,在cellForRowAtIndexPath
中,您需要这样的内容:
if (indexPath.section == 0)
objectToDisplay = self.futureFetchedResultsController.objectAtIndexPath(indexPath)
else // use second FRC
let frcIndexPath = NSIndexPath(forRow: indexPath.row, inSection: 0)
objectToDisplay = self.pastFetchedResultsController.objectAtIndexPath(frcIndexPath)
或者,坚持使用单个 FRC,按升序排序。然后为第二部分重新映射indexPath
,使该部分中的最后一个对象显示在第 0 行,依此类推:
if (indexPath.section == 0)
objectToDisplay = self.fetchedResultsController.objectAtIndexPath(indexPath)
else // use remap to reverse sort order FRC
let sectionInfo = self.fetchedResultsController.sections[1] as! NSFetchedResultsSectionInfo
let sectionCount = sectionInfo.numberOfObjects
let frcIndexPath = NSIndexPath(forRow: (sectionCount - 1 - indexPath.row), inSection:indexPath.section)
objectToDisplay = self.fetchedResultsController.objectAtIndexPath(frcIndexPath)
我个人认为第二种选择更可取。在每种情况下,所有 tableView 数据源/委托方法都需要相同的重新映射,而 FRC 委托方法需要反向映射。
【讨论】:
我正在研究如何使用多个 FRC 完成 tableview 单元格更新,根据 tableview 文档,关于beginUpdates
和 endUpdates
的好消息是 these method pairs can be nested.
【参考方案2】:
为了解决一般问题——希望每个部分有不同的排序——你可以将多个 NSFetchedResultsController
对象包装到一个对象中,这会将部分数组扁平化为一个数组,并将像 func object(at: IndexPath)
这样的函数的结果重新映射为以及NSFetchedResultsControllerDelegate
通知中的索引路径。
这将允许您解决希望显示以不同方式排序的任意数量的部分的一般问题。
我尝试过创建这个包装器对象(CompoundFetchedResultsController),它似乎运行良好:
/**
A CompoundFetchedResultsController is a wrapper of a number of inner NSFetchedResultsControllers.
The wrapper flattens the sections produced by the inner controllers, behaving as if all sections
were fetched by a single controller. Additionally, change notifications are mapped before being
passed to the optional NSFetchedResultsControllerDelegate property, so that the section indices
in the notifications reflect the flattened section indicies.
Example use case: a table where sections should be ordered in mutually opposing ways. E.g., if
section 1 should be ordered by propery A ascending, but section 2 should be ordered by property A
descending. In this case, two controllers can be created - one ordering ascending, the other de-
scending - and wrapped in a CompoundFetchedResultsController. This will maintain the ease of use
in a UITableViewController, and the functionality provided by a NSFetchedResultsControllerDelegate.
*/
class CompoundFetchedResultsController<T: NSFetchRequestResult>: NSObject, NSFetchedResultsControllerDelegate
// The wrapperd controllers
let controllers: [NSFetchedResultsController<T>]
// A delegate to notify of changes. Each of the controllers' delegates are set to this class,
// so that we can map the index paths in the notifications before forwarding to this delegate.
var delegate: NSFetchedResultsControllerDelegate?
didSet controllers.forEach$0.delegate = self
init(controllers: [NSFetchedResultsController<T>]) self.controllers = controllers
func performFetch() throws controllers.forEachtry? $0.performFetch()
var sections: [NSFetchedResultsSectionInfo]?
// To get the flattened sections array, we simply reduce-by-concatenation the inner controllers' sections arrays.
get return controllers.flatMap$0.sections.reduce([], +)
private func sectionOffset(forController controller: NSFetchedResultsController<T>) -> Int
// Determine the index of the specified controller
let controllerIndex = controllers.index(of: controller)!
// Count the number of sections present in all controllers up to (but not including) the supplied controller
return controllers.prefix(upTo: controllerIndex).map$0.sections!.count.reduce(0, +)
func object(at indexPath: IndexPath) -> T
// Sum the section counts of the controllers, in order, until we exceed the section of the supplied index path.
// At that point, we have identifiers the controller which should be used to obtain the object, and just
// adjust the supplied index path's section accordingly.
var sectionCount = 0
for controller in controllers
if sectionCount + controller.sections!.count <= indexPath.section
sectionCount += controller.sections!.count
else
return controller.object(at: IndexPath(row: indexPath.row, section: indexPath.section - sectionCount))
fatalError("Could not find index path \(indexPath).")
func indexPath(forObject object: T) -> IndexPath?
// Given an object, to determine which controller it is in, we just query each controller in turn.
for controller in controllers
if let indexPath = controller.indexPath(forObject: object)
return IndexPath(row: indexPath.row, section: sectionOffset(forController: controller) + indexPath.section)
return nil
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)
// Forward on the willChange notification
delegate?.controllerWillChangeContent?(controller)
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)
// Forward on the didlChange notification
delegate?.controllerDidChangeContent?(controller)
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?)
let sectionOffset = self.sectionOffset(forController: controller as! NSFetchedResultsController<T>)
// Index Paths should be adjusted by adding to the section offset to the section index
func adjustIndexPath(_ indexPath: IndexPath?) -> IndexPath?
guard let indexPath = indexPath else return nil
return IndexPath(row: indexPath.row, section: indexPath.section + sectionOffset)
// Forward on the notification with the adjusted index paths
delegate?.controller?(controller, didChange: anObject, at: adjustIndexPath(indexPath), for: type, newIndexPath: adjustIndexPath(newIndexPath))
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType)
let sectionOffset = self.sectionOffset(forController: controller as! NSFetchedResultsController<T>)
// Forward on the notification with the adjusted section index
delegate?.controller?(controller, didChange: sectionInfo, atSectionIndex: sectionIndex + sectionOffset, for: type)
更新(2021 年 10 月) 虽然上述解决方案当时很方便,但如果您的目标是 ios 14 或更高版本(或同等版本),您可能更适合使用具有可区分数据源和部分快照的更简单的解决方案。
【讨论】:
【参考方案3】:首先在 fetchRequest 中设置排序描述符
func itemFetchRequest() -> NSFetchRequest
let fetchRequest = NSFetchRequest(entityName: "Events")
let primarySortDescription = NSSortDescriptor(key: "futureEvents", ascending: true)
let secondarySortDescription = NSSortDescriptor(key: "historicEvents", ascending: false)
fetchRequest.sortDescriptors = [primarySortDescription, secondarySortDescription]
return fetchRequest
然后设置你的部分数量
func numberOfSectionsInTableView(tableView: UITableView) -> Int
let numberOfSections = frc.sections?.count
return numberOfSections!
最后是你的部分标题
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String?
let sectionHeader = Future Events
let sectionHeader1 = Historic Events
if (section== "0")
return sectionHeader
else
return sectionHeader1
else
return nil
【讨论】:
futureEvents 和historyEvents 键是什么? Event 实体只有一个属性——日期:NSDate 对不起,我假设你有两个属性,因为你希望它们作为节标题。如果您希望它们位于不同的部分,您可能需要将它们分开。以上是关于NSFetchedResultsController 每个部分的不同排序描述符的主要内容,如果未能解决你的问题,请参考以下文章