Search code examples
iosswiftmacoscore-graphicscore-foundation

Trouble retrieving a CGColor from a Swift dictionary


I need a Swift dictionary that can store any kind of object. Some of the values will be CGColor references. I have no issue creating the dictionary and storing the CGColor references. The problem is trying to safely get them back.

let color = CGColor(gray: 0.5, alpha: 1)
var things = [String:Any]()
things["color"] = color
things["date"] = Date()
print(things)

That works and I get reasonable output. Later on I wish to get the color (which may or may not exist in the dictionary. So naturally I try the following:

if let color = things["color"] as? CGColor {
    print(color)
}

But this results in the error:

error: conditional downcast to CoreFoundation type 'CGColor' will always succeed

In the end I came up with:

if let val = things["color"] {
    if val is CGColor {
        let color = val as! CGColor
        print(color)
    }
}

This works without any warnings in a playground but in my actual Xcode project I get a warning on the if val is CGColor line:

'is' test always true because 'CGColor' is a Core Foundation type

Is there good solution to this problem?

I'm working with core graphics and layers and the code needs to work with both iOS and macOS so I'm trying to avoid UIColor and NSColor.

I did find Casting from AnyObject to CGColor? without errors or warnings which is related but doesn't seem relevant any more since I don't need the parentheses to eliminate the warning plus I'm trying to use optional binding which isn't covered by that question.


Solution

  • The problem is that Core Foundation objects are opaque, therefore a value of type CGColor is nothing more than an opaque pointer – Swift itself currently doesn't know anything about the underlying object. This therefore means that you cannot currently use is or as? to conditionally cast with it, Swift has to always allow the given cast to succeed (this will hopefully change in the future though – ideally the Swift runtime would use CFGetTypeID to check the type of the opaque pointer).

    One solution, as shown by Martin in this Q&A, is to use CFGetTypeID in order to check the type of the Core Foundation object – which, I would recommend factoring out into a function for convenience:

    func maybeCast<T>(_ value: T, to cfType: CGColor.Type) -> CGColor? {
      guard CFGetTypeID(value as CFTypeRef) == cfType.typeID else {
        return nil
      }
      return (value as! CGColor)
    }
    
    // ...
    
    if let color = maybeCast(things["color"], to: CGColor.self) {
      print(color)
    } else {
      print("nil, or not a color")
    }
    

    And you could even generalise this to other Core Foundation types with a protocol:

    protocol CFTypeProtocol {
      static var typeID: CFTypeID { get }
    }
    
    func maybeCast<T, U : CFTypeProtocol>(_ value: T, to cfType: U.Type) -> U? {
      guard CFGetTypeID(value as CFTypeRef) == cfType.typeID else {
        return nil
      }
      return (value as! U)
    }
    
    extension CGColor : CFTypeProtocol {}
    extension CGPath  : CFTypeProtocol {}
    
    // Some CF types don't have their ID imported as the 'typeID' static member,
    // you have to implement it yourself by forwarding to their global function.
    extension CFDictionary : CFTypeProtocol {
      static var typeID: CFTypeID { return CFDictionaryGetTypeID() }
    }
    
    
    // ...
    
    let x: Any? = ["hello": "hi"] as CFDictionary
    
    if let dict = maybeCast(x, to: CFDictionary.self) {
      print(dict)
    } else {
      print("nil, or not a dict")
    }