Search code examples
iosobjective-cswiftuicolorswift-protocols

How do I expose a private class method of an Objective-C object using protocols in Swift?


Consider two private methods on UIColor:

  1. The instance method styleString which returns the RGB string of the color
  2. The class method _systemDestructiveTintColor which returns the red color used by destructive buttons.

UIColor.h private header for reference

For instance methods, I can create an @objc protocol and use unsafeBitCast to expose the private method:

@objc protocol  UIColorPrivate {
    func styleString() -> UIColor
}

let white = UIColor.whiteColor()
let whitePrivate = unsafeBitCast(white, UIColorPrivate.self)
whitePrivate.styleString() // rgb(255,255,255)

However, I'm not sure how this would work for class methods.

First attempt:

@objc protocol UIColorPrivate {
    class func _systemDestructiveTintColor() -> String // Error: Class methods are only allowed within classes
}

Makes sense, I'll change it to static:

@objc protocol UIColorPrivate {
    static func _systemDestructiveTintColor() -> String
}

let colorClass = UIColor.self
let privateClass = unsafeBitCast(colorClass, UIColorPrivate.self) // EXC_BAD_ACCESS

This causes a crash. Well this is going nowhere fast. I could use a bridging header and just expose the class methods as an @interface, but is there a way to expose these private class methods in pure Swift?

I could do this with performSelector, but I'd rather expose the method as an interface or protocol:

if UIColor.respondsToSelector("_systemDestructiveTintColor") {
    if let red = UIColor.performSelector("_systemDestructiveTintColor").takeUnretainedValue() as? UIColor {
        // use the color
    }
}

Solution

  • One way to achieve what you want via protocols is to use a separate protocol for the static method. Static methods in Objective-C are actually instance methods on the metaclass of the class, so you can safely take an approach like below:

    @objc protocol UIColorPrivateStatic {
        func _systemDestructiveTintColor() -> UIColor
    }
    
    let privateClass = UIColor.self as! UIColorPrivateStatic
    privateClass._systemDestructiveTintColor() // UIDeviceRGBColorSpace 1 0.231373 0.188235 1
    

    This will give you both exposure of the private method and usage of protocols, and you get rid of the ugly unsafeBitCast (not that a forced cast would be more beautiful).

    Just note that as always if you are working with private API's your code can break at any time if Apple decides to change some of the internals of the class.