iosswiftgenericsuuidnspredicate

Swift Generics: Explicitly declared parameter Set<UUID> converts to CVarArg but generic Set<S> with UUID's does not


I'm building a generic Realm Swift query function. I started by getting it to work with everything declared explicitly. Now I'm moving parameters into generic types.

This is my function and it works like this:

func queryFromCollection<T: Object>(of type: T.Type, query set: Set<UUID>, for key: String) async -> Results<T>? {

    let objects = await RealmReadService.instance.readObjects(of: type)

    var predicates: [NSPredicate] = []
    for item in set {
        let query = NSPredicate(format: "%K == %@", key, item as CVarArg)
        predicates.append(query)
    }
    let compoundPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: predicates)
    
    return objects!.filter(compoundPredicate)
}

The Set of UUIDs converts to CVarArg just fine.

Now, when I use this code to make my Set generic:

func queryFromCollection<T: Object, S>(of type: T.Type, query set: Set<S>, for key: String) async -> Results<T>?

and in my loop I have to change item as! CVarArg. This compiles and runs until I do the query.

Once I run the query it fails and I get this: "Could not cast value of type 'Foundation.UUID' (0x1df22b1e0) to 'Swift.CVarArg' (0x13b14e7b8)"

I'm struggling to understand why it can convert UUIDs to CVarArg when the Set is declared as a Set<UUID> but not with a generic Set.


Solution

  • I think this is because UUID is bridged to NSUUID in the case of Set<UUID>. NSUUID inherits NSObject, which does implement CVarArg.

    However, in the generic case, this bridging does not happen, because it is not necessarily the case that the generic type can be bridged.

    Other than making your function only take CVarArgs,

    func queryFromCollection<T: Object, S: NSObject>(of type: T.Type, query set: Set<S>, for key: String) async -> Results<T>?
    

    which would require the caller to do a lot of explicit conversion to NSUUID, I can only think of one other rather ugly way - make a CVarArgConvertible protocol

    protocol CVarArgConvertible {
        func toCVarArg() -> CVarArg
    }
    
    extension UUID: CVarArgConvertible {
        func toCVarArg() -> CVarArg {
            self as NSUUID
        }
    }
    
    // other bridgable types you want to use...
    

    Then queryFromCollection would look like:

    func queryFromCollection<T: Object, S: CVarArgConvertible>(of type: T.Type, query set: Set<S>, for key: String) async -> Results<T>? {
        // in here you would call "toCVarArg" on an element of the set to get a CVarArg.
    }
    

    Or, as Larme reminded me in the comments, you can use the other overload of NSPredicate, that doesn't take CVarArgs, but an [Any]? instead:

    let query = NSPredicate(format: "%K == %@", argumentArray: [key, item])