1

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.

Community
  • 1
  • 1
rmaddy
  • 314,917
  • 42
  • 532
  • 579
  • What is the Swift version? – Muzahid May 11 '17 at 23:43
  • Compare [How could I type check CGColor / CGPath?](http://stackoverflow.com/q/38252330/2976878) – you cannot type-check a Core Foundation type with `is`/`as?` because it's really just an opaque pointer, you can however use `CFGetTypeID`. – Hamish May 11 '17 at 23:44
  • @Md.MuzahidulIslam Swift 3.1 (Xcode 8.3.2) – rmaddy May 11 '17 at 23:59
  • @Hamish Thanks. I didn't find that one in my searching. After seeing that I came up with the same code that Grzegorz posted below. – rmaddy May 12 '17 at 00:00
  • The answers you've been given are interesting, but I would simply use a wrapper struct-or-class. – matt May 12 '17 at 00:09

3 Answers3

13

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")
}
Hamish
  • 78,605
  • 19
  • 187
  • 280
  • Thanks for the good explanation of the underlying issue. I really like the suggestion of `CGColor` extension. That makes the conditional binding code much simpler. – rmaddy May 12 '17 at 00:06
4

The other answers are interesting but I would just use a wrapper. As this code shows, you can assign to an Any and get it back again with an is or as? test:

struct ColorWrapper {
    let color:CGColor
}
let c = ColorWrapper(color:UIColor.red.cgColor)
let any : Any = c
if let c2 = any as? ColorWrapper {
    let result = c2.color
}
matt
  • 515,959
  • 87
  • 875
  • 1,141
2

For Swift 3:

    if let val = things["color"], CFGetTypeID(val as CFTypeRef) == CGColor.typeID {
        let color = val as! CGColor
        print(color)
    }

You could also keep it simpler and store UIColor in your Dictionary instead of CGColor - this will allow you to use standard as? UIImage methods

Grzegorz Krukowski
  • 18,081
  • 5
  • 50
  • 71
  • Perfect (well, as good as it gets in this case). I came up with the same thing after seeing the link posted by Hamish. – rmaddy May 12 '17 at 00:02
  • BTW - as stated in my question, I need `CGColor` since the code will be in a common framework used by both iOS and macOS. So using `UIColor` is not an option. – rmaddy May 12 '17 at 00:03
  • Ugh. That's ugly, but I see why it's required. rmaddy, you should accept it since it's the first answer that solves your problem. – Duncan C May 12 '17 at 00:04