2

The Problem

break point screenshot

*** 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 UITableViewControllers 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 UITableViews (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?

andrewbuilder
  • 3,215
  • 1
  • 21
  • 39
  • 1
    Are you sure you can access a static variable using key-value coding? Shouldn't it be accessed as `Managed.entity` when it's static? – Joakim Danielson Nov 13 '18 at 06:33
  • @JoakimDanielson thanks for your question but honestly I'm not sure, I'm new to Swift and generics, self taught on Obj-C and always preferred blocks to protocols in Obj-C, so part of my problem may be a general and basic lack of understanding of static variables, protocols and KVC. (PS tried your suggestion but could not make it work.) – andrewbuilder Nov 13 '18 at 09:53
  • 1
    Per your extension, `entity` is an `NSEntityDescription`: there's no `active` property for NSEntityDescriptions which is why you get the error. Do you in fact mean to get the `active` attribute for the NSManagedObject? – pbasdf Nov 13 '18 at 10:39
  • @pbasdf correct, I'm trying to get the `active` attribute (every entity in my data model has a common `active` attribute) for the `NSManagedObject`. – andrewbuilder Nov 13 '18 at 10:41
  • 1
    I think your `entityActive` should not be static: it's a property of `Managed` objects, not the `Managed` class. Your `willDisplayCell` code therefore needs to identify which `Managed` object is required - presumably using the `indexPath` to look up in the datasource. – pbasdf Nov 13 '18 at 10:55
  • Thanks for your input... after considering your comments, reading more and hacking some trial and error code, I've solved my problem. I'll post an answer. – andrewbuilder Nov 14 '18 at 06:36

1 Answers1

0

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 UITableViewControllers)...

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.

andrewbuilder
  • 3,215
  • 1
  • 21
  • 39