0

I am an extreme rookie with CoreData and I am attempting to section my tableView using fetchedResultsController using a custom function. My current implementation did not manage to section the tableView and I am still given just 1 section.

I refer to the following posts in my implementation: here and here. I also adopted the transient property.

I first create the NSManagedObject subclass using Xcode (Editor -> Create NSMangedObject Subclass) and added the var sectionIdentifier to return a custom string. Unfortunately, my frc returns only 1 section.

// from the Conversation+CoreDataProperties.swift file automatically generated by Xcode
import Foundation
import CoreData


extension Conversation {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Conversation> {
        return NSFetchRequest<Conversation>(entityName: "Conversation")
    }

    @NSManaged public var conversationStartTime: Double
    @NSManaged public var profileImage: NSData?
    @NSManaged public var recipientID: String?
    @NSManaged public var recipientUsername: String?
    @NSManaged public var shoutoutID: String?
    @NSManaged public var unreadMessagesCount: Int32

    var sectionIdentifier: String? {
        let presentTimestamp = NSDate().timeIntervalSince1970

        if conversationStartTime < presentTimestamp - Double(Constants.PermissibleDurationInMinutes * 60) {
            return "Expired Conversations"
        } else {
            return "Active Conversations"
        }
    }
}

//at VC
lazy var fetchedResultsController: NSFetchedResultsController<Conversation> = {
    let context = CoreDataManager.shared.persistentContainer.viewContext

    let request: NSFetchRequest<Conversation> = Conversation.fetchRequest()
    request.sortDescriptors = [NSSortDescriptor(key: "conversationStartTime", ascending: true)]

    let frc = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: "sectionIdentifier", cacheName: nil)
    frc.delegate = self

    do {
        try frc.performFetch()
    } catch let err {
        print(err)
    }

    return frc
}()

Printing out the conversation entity in console returns this

<Conversation: 0x604000e93100> (entity: Conversation; id: 0xd000000003e00000 <x-coredata://91BC90B2-9A0C-45A7-9B82-844BE88BAFE0/Conversation/p248> ; data: {
    conversationStartTime = "1521359598.88445";
    profileImage = <ffd8ffe0 00104a46 49460001 02000001 00010000 ffed009c 50686f74 6f73686f 7020332e 30003842 494d0404 00000000 0080>;
    recipientID = "-L7rvH71i-KUXvLQVDOh";
    recipientUsername = Angemon;
    sectionIdentifier = nil;
    shoutoutID = "-L7rvH71i-KUXvLQVDOh";
    unreadMessagesCount = 0; })

Somehow sectionIdentifier is always nil. Any advice why is this happening? At the end of the day, I want to divide my list of conversations into two sections, first section "Active Conversations" and second section "Expired Conversations" depending how long ago that conversation is.

UPDATED CODE:

// At Conversation+CoreDataProperties.swift
import Foundation
import CoreData


extension Conversation {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Conversation> {
        return NSFetchRequest<Conversation>(entityName: "Conversation")
    }

    @NSManaged public var conversationStartTime: Double
    @NSManaged public var profileImage: NSData?
    @NSManaged public var recipientID: String?
    @NSManaged public var recipientUsername: String?
    @NSManaged public var shoutoutID: String?
    @NSManaged public var unreadMessagesCount: Int32

    @objc var sectionIdentifier: String {
        willAccessValue(forKey: "sectionIdentifier")

        let presentTimestamp = NSDate().timeIntervalSince1970
        var text = ""
        if conversationStartTime < presentTimestamp - Double(Constants.PermissibleDurationInMinutes * 60) {
            text = "Expired Conversations"
        } else {
            text = "Active Conversations"
        }

        didAccessValue(forKey: "sectionIdentifier")
        return text
    }
}

//At VC
lazy var fetchedResultsController: NSFetchedResultsController<Conversation> = {
    let context = CoreDataManager.shared.persistentContainer.viewContext

    let request: NSFetchRequest<Conversation> = Conversation.fetchRequest()
    request.sortDescriptors = [
                               NSSortDescriptor(key: "conversationStartTime", ascending: false)
                                    ]

    let frc = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: "sectionIdentifier", cacheName: nil)
    frc.delegate = self

    do {
        try frc.performFetch()
    } catch let err {
        print(err)
    }

    return frc
}()

At my CoreData model screen: enter image description here

Koh
  • 2,687
  • 1
  • 22
  • 62
  • Add @objc to the definition of sectionIdentifier. – pbasdf Mar 24 '18 at 08:11
  • @pbasdf u mean at the `Conversation+CoreDataProperties` file? Tried, makes no difference. – Koh Mar 24 '18 at 08:32
  • I didn't see this before and I am not sure if it matters but you're executing the fetch inside the code block that creates the fetch controller which I've never done. I would try to do that outside of the block for instance in the viewDidLoad method. Also set the delegate property in viewDidLoad instead. – Joakim Danielson Apr 14 '18 at 06:26
  • @JoakimDanielson I've just tried this approach but its still giving me the exact same result. It makes no difference if i'm executing the fetch in the code block or not. – Koh Apr 14 '18 at 06:53

1 Answers1

0

This is a continuation on the answer from @Sandeep since I wanted to see if it was possible to group by a transient property without having a sort descriptor with it since one of your links implied it should be possible to do so.

After some experimentation I managed to conclude that it is possible if you fulfil two conditions:

  • You need a sort descriptor (and it needs to be the first one) on the persistent attribute you use for calculating your transient attribute
  • The sort order of the sort descriptor needs to match the transient attribute.

(I assume this applies for when you access multiple persistent attributes as well for your transient attribute, sort descriptors for all of them and sort order needs to match but I haven't tested)

As I see it you fulfil the first one but not the second one since your sort descriptor has ascending = true but since "Active" comes before "Expired" you have an implicit descending sort order for sectionIdentifier.

Here is a silly example I made with some existing code since I had some test data available for it. As you can see I divide the days of the month into three sections so that the will show up in reverse chronological order.

@objc var silly: String {
    willAccessValue(forKey: #keyPath(silly))
    var result: String = "In between"

    let component = Calendar.current.component(Calendar.Component.day, from: date)
    if component <= 13 {
        result = "Last"
    } else if component > 20 {
        result = "At the Top"
    }
    didAccessValue(forKey: #keyPath(silly))
    return result
}

And when setting up my fetched result controller I do

...
fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(Location.date), ascending: false)]  

let fetchedResultController = NSFetchedResultsController(fetchRequest: fetchRequest,
                                                             managedObjectContext: managedObjectContext,
                                                             sectionNameKeyPath: #keyPath(Location.silly),
                                                             cacheName: "Locations")

The will/didAccessValue(...) was necessary to handle insert and updates of new objects after the first execute of the request, otherwise the attribute is set to nil.

Joakim Danielson
  • 43,251
  • 5
  • 22
  • 52
  • I tried your solution but somehow it didnt work for me... I am still getting nil for sectionIdentifier – Koh Mar 25 '18 at 04:32
  • One problem could be that you have defined your attribute as optional which isn't necessary since you're always returning a string – Joakim Danielson Mar 25 '18 at 07:59
  • @Koh, did you try with not declaring sectionIdentifier as optional? – Joakim Danielson Mar 26 '18 at 08:49
  • Yes, I've tried declaring not as optional, but I'm still stuck with the same results. Its still not splitting it into two sections. – Koh Apr 14 '18 at 05:04
  • And I also noticed that I cannot uncheck the "optional" field else I am unable to create the coredata object. It will throw an error – Koh Apr 14 '18 at 06:04
  • I meant in code where I see you have changed it to to optional, so that's fine. – Joakim Danielson Apr 14 '18 at 06:27