Dealing with an `id` defined as Optional

I'm in a slight pickle, and probably overthinking things a lot here. Technically, this is SwiftUI-meets-a Core Data with very legacy decisions, but the underlying issue I'm facing is theoretically able to be worked around in pure Swift. Sorry in advance if it dips a bit too much into specific technologies.

While SwiftUI's List type is happy to be provided any form of identity you give it with a key path (via the id: parameter, so you can i.e. pass \.self), SwiftUI's Table type does not. It demands YourType.ID and the id variable it returns.

Unfortunately, the type I want to represent in a table is a Core Data entity... where it has an attribute in the model called id... as a String?. And it is being used as an optional string, where things sourced from a server have their from-the-server ID, things sourced locally did not (because the attribute is basically the server's ID for that object, and objects can be sourced from local resources too). This is pretty weird, but it didn't pose much of a problem when this schema was designed in ~2010 (and when the application was written in Objective-C with nibs instead of increasingly Swift), but it does pose one for SwiftUI using presumably Identifiable. (Hilariously, defining an id attribute on an entity immediately gives it the ID typealias with that attribute's type.)

This actually poses a problem because it confuses the hell out of Core Data. Obviously, the fact it can be nil is no good. For those local items with all nil in id, trying to derive identity is from that is broken - the table will just have the same item. But if I do override id, so it returns a String? that can't be null:

    @objc dynamic public override var id: String? {
        get {
            if let value = primitiveValue(forKey: "id") as! String? {
                return value
            } else {
                return objectID.uriRepresentation().absoluteString
            }
        }
        set {
            setPrimitiveValue(newValue, forKey: "id")
        }
    }

(I realize this could just return that managed object ID directly, but this just attempts to deal with the already mentioned local-remote sourced cases.)

You get some fascinating behaviour. You try to select an item, and it immediately gets deselected, but doesn't actually get removed from the Set<SBTrack.ID> that the table selection is bound to. Instead, it keeps adding items, and if you select the same item again, you get a really funny error:

Fatal error: Duplicate elements of type 'Optional<String>' were found in a Set.
This usually means either that the type violates Hashable's requirements, or
that members of such a set were mutated after insertion.

I think this is because the actual items getting inserted are the Optional wrappers, and that really confuses the hell out of things again. If the SBTrack.ID was able to be set to non-nil, that would be ideal.


The obvious solution is "rename the attribute in Core Data, refactor everything from model to code to bindings in nibs, and admit defeat", but I'm wondering if there's anything I could do to work around the model's definition colliding with Identifiable's.

I tried applying violence, like in this post in reverse (where I want an optional ID type to become non-optional), resulting in this:

    public typealias ID = String

    @_disfavoredOverload
    @_implements(Identifiable, id)
    var stableId: ID
    {
        get {
            if let value = primitiveValue(forKey: "id") as! String? {
                return value
            } else {
                return objectID.uriRepresentation().absoluteString
            }
        }
        set {
            setPrimitiveValue(newValue, forKey: "id")
        }
    }

This seems too good to be true, and it unfortunately confuses the selection (defined as Set<SBTrack.ID>, with @State) with:

Cannot convert value of type 'Binding<Set<SBTrack.ID>>' (aka 'Binding<Set<String>>') to expected argument type 'Binding<SBTrack.ID?>'

I think it's somehow pulling the old String? ID type alias at differently from when it pulls the new String alias, resulting in this confusing message. Which makes the whole attempt moot. And using underscored attributes for this feels gross.


Alternatively, there are two other options I can think of. Maybe there's something else better too.

One is changing the root entity that this one inherits from's Swift class definition and basically put the overriden id there as replacing the @NSManaged one. I've tried this and it does work in SwiftUI, but it changes the semantics of that attribute subtly that I'm a little cautious with going this way immediately.

One is changing the Swift side name for the Core Data attribute would reduce the blast damage for a refactor by reducing it to only the Swift parts needing to be changed, not the entity in the model nor ObjC code using bindings still. It would use whatever the hell default typealias ID is (ObjectIdentifier I believe, which I'm surprised works well w/ Core Data objects?), which seems to work better. I'm not sure of the best way to do that though.