0
votes

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

1
Add @objc to the definition of sectionIdentifier.pbasdf
@pbasdf u mean at the Conversation+CoreDataProperties file? Tried, makes no difference.Koh
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
@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

1 Answers

0
votes

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.