You have probably defined sectionName()
as a free function,
and not as a property or method of your managed object subclass that is being
fetched. Also formatting the "current date" as a string does
not make much sense, as you probably want to group the objects
according to some date property of your entity.
Apple's sample project Custom Section Titles with NSFetchedResultsController demonstrates how to group a table view into
sections based on the month and year of a "timeStamp" property.
That project is written in Objective-C.
Here is a short recipe how the same can be achieved in Swift.
In the following, I assume that the entity and the managed object
subclass is called Event
, and that the events should be grouped
according to the month and year of the timeStamp
property.
First, add a transient property "sectionIdentifier" of type "String"
to the "Event" entity.
Next, define the sectionIdentifier
property of the Event
class.
This can be done directly in the class Event { ... }
definition
or as an extension:
extension Event {
var sectionIdentifier : String? {
// Create and cache the section identifier on demand.
self.willAccessValueForKey("sectionIdentifier")
var tmp = self.primitiveValueForKey("sectionIdentifier") as? String
self.didAccessValueForKey("sectionIdentifier")
if tmp == nil {
if let timeStamp = self.valueForKey("timeStamp") as? NSDate {
/*
Sections are organized by month and year. Create the section
identifier as a string representing the number (year * 1000) + month;
this way they will be correctly ordered chronologically regardless
of the actual name of the month.
*/
let calendar = NSCalendar.currentCalendar()
let components = calendar.components(.CalendarUnitYear | .CalendarUnitMonth, fromDate: timeStamp)
tmp = String(format: "%ld", components.year * 1000 + components.month)
self.setPrimitiveValue(tmp, forKey: "sectionIdentifier")
}
}
return tmp
}
}
In the table view controller, you have to override the
titleForHeaderInSection
method to compute a proper title from
the section identifier:
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
/*
* Creating a date formatter is "expensive". Use a static property so that
* this is done only once.
*/
struct Formatter {
static let formatter : NSDateFormatter = {
let fmt = NSDateFormatter()
let dateFormat = NSDateFormatter.dateFormatFromTemplate("MMMM yyyy", options: 0,
locale: NSLocale.currentLocale())
fmt.dateFormat = dateFormat
return fmt
}()
}
/*
Section information derives from an event's sectionIdentifier, which is a string
representing the number (year * 1000) + month.
To display the section title, convert the year and month components to a string representation.
*/
if let theSection = fetchedResultsController.sections?[section] as? NSFetchedResultsSectionInfo,
let numericSection = theSection.name?.toInt() {
let components = NSDateComponents()
components.year = numericSection / 1000
components.month = numericSection % 1000
if let date = NSCalendar.currentCalendar().dateFromComponents(components) {
let titleString = Formatter.formatter.stringFromDate(date)
return titleString
}
}
return nil
}
Finally, create the fetched results controller with "timeStamp"
as first sort descriptor and "sectionIdentifier" as sectionNameKeyPath
:
let fetchRequest = NSFetchRequest(entityName: "Event")
let timeStampSort = NSSortDescriptor(key: "timeStamp", ascending: false)
fetchRequest.sortDescriptors = [timeStampSort]
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: context,
sectionNameKeyPath: "sectionIdentifier",
cacheName: nil)
If the time stamp of an object can be modified then things become
slightly more complicated. The corresponding "sectionIdentifier"
must be invalidated so that it is computed again on demand.
In Objective-C, that is fairly simple by overriding the getter method
of the "timeStamp" property only (see DateSectionTitles/APLEvent.m). In Swift this seems
to require that you define "timeStamp" as a normal computed property (without @NSManaged), as discussed in https://devforums.apple.com/thread/262118?tstart=0:
class Event: NSManagedObject {
//@NSManaged var timeStamp : NSDate
var timeStamp: NSDate? {
get {
self.willAccessValueForKey("timeStamp")
let tmp = self.primitiveValueForKey("timeStamp") as? NSDate
self.didAccessValueForKey("timeStamp")
return tmp
}
set {
self.willChangeValueForKey("timeStamp")
self.setPrimitiveValue(newValue, forKey: "timeStamp")
self.didChangeValueForKey("timeStamp")
// If the time stamp changes, the section identifier become invalid.
self.setPrimitiveValue(nil, forKey: "sectionIdentifier")
}
}
override class func keyPathsForValuesAffectingValueForKey(key: String) -> Set<NSObject> {
var keyPaths = super.keyPathsForValuesAffectingValueForKey(key)
if key == "sectionIdentifier" {
keyPaths.insert("timeStamp")
}
return keyPaths
}
}
Update: As of Swift 4 you have to make the custom Core Data properties explicitly visible to the Objective-C runtime by adding
the @objc
attribute, e.g.
@objc var sectionIdentifier : String? { ... }
@objc var timeStamp: NSDate? { ... }
For more information about this change, see