25

Any advice on implementing calculated attributes when using Core Data in Swift?

with the generated ManagedObject class, I tried to override the getter but I get the error:

'NSManaged' not allowed on computed properties

which implies you cannot override the getter for a transient (calculated) attribute.

In the code sample below, dateDue is defined as a transient attribute in my model.

Please note that the @NSManaged lines were generated by Xcode - not added by me.

@NSManaged var timeStamp: NSDate
@NSManaged var dateDue: String { 
    get {

        self.willAccessValueForKey("dateDue")
        var ddtmp  = self.primitiveValueForKey("dateDue") as String?
        self.didAccessValueForKey("dateDue")

        if (ddtmp == nil)
        {

            let calendar = NSCalendar.currentCalendar()

            let components = calendar.components((NSCalendarUnit.YearCalendarUnit | NSCalendarUnit.MonthCalendarUnit ) , fromDate: self.timeStamp)
            ddtmp = "\(components.year * 1000 + components.month)"
            self.setPrimitiveValue(ddtmp, forKey: "dateDue")

        }



        return ddtmp!
    }

}
Tomm P
  • 761
  • 1
  • 8
  • 19

4 Answers4

52

First, in the data model create a transient attribute (section). Because it is transient, it is not physically stored and thus not stored in the managed object context.

The section attribute is shown here:

enter image description here

The entity is shown here:

enter image description here

The class NSManagedObject subclass should have computed 'section' attribute. The NSManagedObject subclass demonstrating how to accomplish this is shown here:

class Number: NSManagedObject {

    @NSManaged var number: NSNumber

    var section: String? {
        return number.intValue >= 60 ? "Pass" : "Fail"
    }
}

Then you must set sectionForKeyPath in the NSFetchedResultsController initializer to be the transient attribute key in the data model and the cache name if desired.

override func viewDidLoad() {
        super.viewDidLoad()

        fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest(), managedObjectContext: managedObjectContext!, sectionNameKeyPath: "section", cacheName: "Root")
        fetchedResultsController?.delegate = self
        fetchedResultsController?.performFetch(nil)

        tableView.reloadData()
}

func fetchRequest() -> NSFetchRequest {

    var fetchRequest = NSFetchRequest(entityName: "Number")
    let sortDescriptor = NSSortDescriptor(key: "number", ascending: false)

    fetchRequest.predicate = nil
    fetchRequest.sortDescriptors = [sortDescriptor]
    fetchRequest.fetchBatchSize = 20

    return fetchRequest
}

The result is a UITableViewController with grades sorted by pass or fail dynamically:

enter image description here

I made a sample project that can be found on GitHub.

Ian
  • 12,538
  • 5
  • 43
  • 62
  • 1
    Worked for me. :) The bad part of this solution is that you have to edit the auto-generated Core Data class. Each time you regenerate you have to redo the modifications. I put the section in an extension class, but I still have to remove section from the generated class after regenerating the class. – Glauco Neves Apr 25 '15 at 19:22
  • 1
    This answer should be accepted! Worked perfectly for me with dates instead of numbers. A suggestion for everyone trying to implement section separation by date starting from "DateSectionTitles" Apple sample project: follow this answer instead! Thanks a lot @Bluehound – cdf1982 Apr 29 '15 at 16:55
  • 2
    I can't get it to work with swift 4/ iOS 11. Does anyone has similar problems? – webventil Jan 24 '18 at 15:40
  • 1
    @webventil Yes. I have similar Problems with Swift 4 and iOS 11. Did you find a solution? – Maurice Feb 09 '18 at 15:58
  • 5
    @webventil found the solution. Need to add @ objc in front of the property declaration – Maurice Feb 09 '18 at 16:22
  • Thanks @webventil ! – Fulkron Apr 05 '18 at 09:33
  • 2
    I think this answer misses the point. The point is that transient Core Data properties still have a getter and setter generated for you. You could edit the machine-generated Core Data entity file make the property read-only instead of copy (i.e. read-write), and then write your own getter in the user-editable generated file for the entity, but anything that involves editing the machine generated file is not a good solution. A better answer might be: forget about have a transient property. Just make a getter in the human-editable CD generated file the calculates the value. – occulus Jun 03 '18 at 11:14
  • 1
    "it is not physically stored and thus not stored in the managed object context." -- this is wrong btw. Transients are not persisted by the Persistent store coordinator, but they *are* in the MOC. – occulus Jun 03 '18 at 11:14
8

"Transient" and "computed" in the sense you mean are different things and are mutually exclusive.

Transient means that the value is stored in memory on the object graph. Computed means that the value is stored nowhere and is calculated in the getter. Both are distinct from the classic non-transient attribute which is stored on the object graph and is saved to disk.

@NSManaged can only be applied to attributes that have a slot in your managed object model.

iluvcapra
  • 9,436
  • 2
  • 30
  • 32
  • It is an attribute in the managed object model. Juts to expand, I am trying to copy a technique used by one of the apple sample programs - however the apple sample was written in Objective C - in fact its the template that is delivered in Xcode (master detail application). – Tomm P Sep 21 '14 at 17:35
  • Yeah but ObjC doesn't do `@NSManaged` -- if you're trying to wrap the underlying attribute you shouldn't use `@NSManaged` here. `@NSManaged` tells the compiler that the property implementation will be provided late, at runtime. Using `@NSManaged` while explicitly providing implementation is a contradiction. – iluvcapra Sep 21 '14 at 17:50
  • OK thanks. I'll revisit removing the NSManaged. Let you know how I get on :) – Tomm P Sep 21 '14 at 18:20
8

I have solved this in Swift with an extension, so there is no need to subclass the NSManagedObject and I don't have to generate class files for my models.

So for the above example with class Number, create a file Number+Section.swift you can load the transient property value like this in awakeFromFetch

import Foundation    
extension Number {
    public override func awakeFromFetch() {
        super.awakeFromFetch()
        section = number.intValue >= 60 ? "Pass" : "Fail"
    }
}

I found this way of loading transient fields in Apple's Core Data programming guide.

awakeFromFetch is invoked when an object is reinitialized from a persistent store (during a fetch). You can override awakeFromFetch to, for example, establish transient values and other caches.

  • This approach works if you have set Codegen to "Class Definition". Thanks for the solution Core Data guide explains this clearly – Nikhil Muskur Dec 13 '19 at 11:59
  • While it basically works, it is no good solution when using an NSFetchedResultsController in conjunction with a table view: The initial table sections are correct, but when you update the underlying data, the awakeFromFetch apparently is not called again. – LPG Feb 25 '20 at 07:11
  • This is a great answer, especially because allows to avoid class generation just to have a transient property! Much easier than in the past, thanks! – cdf1982 Jul 05 '20 at 13:54
2

Remove the NSManaged attribute.

Forge
  • 6,538
  • 6
  • 44
  • 64
Daij-Djan
  • 49,552
  • 17
  • 113
  • 135
  • Nope. Sorry, that's not it. That part of the code was generated by Xcode so I presume is correct (or at least as apple intended). – Tomm P Sep 21 '14 at 17:40
  • 1
    dont assume ;)if it is transient, it isn't managed. so either apply or you made a mistake – Daij-Djan Sep 21 '14 at 17:42
  • 2
    Xcode generated the code but you're writing a method that hides the underlying attribute, you and the code generator are thinking different things. – iluvcapra Sep 21 '14 at 17:53
  • See what you mean. I'll try without the NSManaged - guess I have to be a bit more sceptical of what Xcode generates. – Tomm P Sep 21 '14 at 18:21
  • @Daij-Djan is completely right. Should be removed in this case. – Bartłomiej Semańczyk Aug 30 '16 at 20:06
  • Just add a little tip from [another question related to CoreData](https://stackoverflow.com/questions/43533078/semantics-of-optional-in-the-context-of-transient-attributes-in-coredata) Transient attributes can't be "optional" in the model. – LShi Sep 11 '17 at 11:03