The Problem
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[ valueForUndefinedKey:]: this class is not key value coding-compliant for the key active.'
Thanks to @GrahamPerks answer to this SO question, I inserted an exception breakpoint in my code, which now pauses execution at this line...
static var entityActive: Bool {
return entity.value(forKey: "active") as! Bool // <-- PAUSES AT THIS LINE
}
This obviously needs further explanation...
Background
I'm writing a Core Data app that uses a generic table view data source and delegate, more or less set up per Florian Kugler's book "Core Data", published in 2017 by objc.io.
I have successfully linked three separate UITableViewController
s to this generic data source / delegate. I am using a single main storyboard and these three controllers are linked to three UISplitViewController
master/detail views.
I have deleted the automatically generated dataSource and delegate connections within the storyboard UITableView
s (although whether I leave or remove these connections, seems to make no difference).
If I comment out the code for the static var entityActive
above, the project will successfully Build and Run.
My code uses the UITableViewDelegate
method tableView(_, willDisplay:, forRowAt:)
to change the .textColor
of text and .backgroundColor
of cells, based on an attribute "active", stored as a boolean Scalar Type value with each entity.
For clarity, I'm trying to get the "active" attribute (every entity in my data model has a common attribute "active") for each NSManagedObject
for the entity. The value of that "active" attribute (Bool
true
or false
) for each entity is then used to format the cell in the if...else statement in the following code.
To attempt to maximise code reuse and minimise code repetition, I have also placed this delegate method in my generic data source / delegate class.
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let entityObjectActive = T.entityActive
if entityObjectActive == true {
cell.textLabel?.textColor = UIColor.black
cell.detailTextLabel?.textColor = UIColor.darkGray
cell.backgroundColor = UIColor.white
} else if entityObjectActive == false {
cell.textLabel?.textColor = UIColor.lightGray
cell.detailTextLabel?.textColor = UIColor.lightGray
cell.backgroundColor = UIColor.clear
}
}
The compiler doesn't complain.
I seem to be able to use the generic type T
(representing Core Data entities) to associate the static property entityActive
with my instance of entityObjectActive
- so this seems to work...
let entityObjectActive = T.entityActive
To confirm, I have the following:
generic data source class....
class MyDataSource<T: Managed,
Delegate: TableViewDataSourceDelegate>:
NSObject,
UITableViewDataSource,
UITableViewDelegate,
NSFetchedResultsControllerDelegate {
// lots of code...
}
protocol...
protocol Managed: class, NSFetchRequestResult {
static var entity: NSEntityDescription { get }
static var entityActive: Bool { get } }
}
and extension...
extension Managed where Self: NSManagedObject {
static var entity: NSEntityDescription { return entity() }
static var entityActive: Bool {
return entity.value(forKey: "active") as! Bool
}
}
Attempts at Problem Solving
I've done some reading attempting to resolve my problem, including a review of many blogs (on how to set up generics in general and generic table view data sources) and a lot of SO Q&A, in particular...
How to fix Error: this class is not key value coding-compliant for the key tableView.'
Uncaught exception: This class is not key value coding-compliant
setValue:forUndefinedKey: this class is not key value coding-compliant for the key
It seems that all the SO Q&A are directly related to an issue with IB connections within storyboards. Maybe this is my issue too - but if that is the case it seems to me to be very well hidden.
Any assistance or advice please?
PS: I'm learning Swift after being a long time coder in Obj-C and I'm really struggling with the Generics Protocols Extensions paradigm shift, so it could be that my use of generic types is incorrect?
Thanks to those whose comments pointed me towards this solution...
My REVISED protocol Managed
...
protocol Managed: class, NSFetchRequestResult {
static var entity: NSEntityDescription { get }
var attributeActive: Bool { get } // <-- REMOVED static
}
My REVISED extension of Managed
...
UPDATE - added var attributeActive
to Managed
extension...
extension Managed where Self: NSManagedObject {
static var entity: NSEntityDescription { return entity() }
var attributeActive: Bool {
guard let attribute = self.value(forKey: "active") as? Bool else {
return false // in case key "active" is not set
}
return attribute
}
}
UPDATE - deleted var attributeActive
from managed object extensions as no longer required...
My REVISED extensions for each of my three Core Data Entities (the data of which is displayed in each of the three separate UITableViewController
s)...
extension <<DataModelEntity>>: Managed {
public var attributeActive: Bool {
return self.active
}
@NSManaged public var active: Bool
@NSManaged public var <<OTHER DATA MODEL ENTITY ATTRIBUTES>> //...
// ...etc.
}
Maybe it is worth noting here for clarity that the Codegen value for each entity in the data model is set to Manual/None, so I have manually1 prepared classes and extensions for each entity.
1 When I write manually, I mean that I used the Create Managed Object Subclass... function under the Editor menu in Xcode and then manually entered my Managed
protocol stubs.
Finally, my REVISED generic data source delegate class...
class MyDataSource<T: Managed,
Delegate: TableViewDataSourceDelegate>:
NSObject,
UITableViewDataSource,
UITableViewDelegate,
NSFetchedResultsControllerDelegate {
// lots of code...
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let object = fetchedResultsController.object(at: indexPath)
let objectActive = object.attributeActive
if objectActive == true {
cell.textLabel?.textColor = UIColor.black
cell.detailTextLabel?.textColor = UIColor.darkGray
cell.backgroundColor = UIColor.white
} else if entityObjectActive == false {
cell.textLabel?.textColor = UIColor.lightGray
cell.detailTextLabel?.textColor = UIColor.lightGray
cell.backgroundColor = UIColor.clear
}
}
// lots more code...
}
If anyone is still reading this essay, maybe you're interested in the reasons for this solution?
Frankly I'm still figuring out the details myself and plan to add a more concise / accurate reason in the future as my understanding improves regarding swift generics, protocols and extensions, but for now I provide the following...
As pointed out in comments, I was incorrectly attempting to get an entity property based on a data model attribute from an entity description. This is like trying to get the colour or size of a Lego brick by asking the Lego box for the characteristics of one of it's bricks. "Which brick?" might the box ask, if Lego boxes could ask such a thing.
Essentially I made a couple of errors. I didn't understand the effect the static definition had on my variables and I didn't properly understand how my Managed
protocol and extension interacted with any classes that adopted that protocol.
Collected from the Internet
Please contact [email protected] to delete if infringement.
Comments