问题
Consider two private methods on UIColor
:
- The instance method
styleString
which returns the RGB string of the color - 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
}
}
回答1:
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.
回答2:
unsafeBitCast()
is a terrible way to access private API.
is there a way to expose these private class methods in pure Swift?
There is an answer to that question - use an extension with a computed property. You still have to use perform selector but you get the type safety.
extension UIColor {
static var systemDestructiveTintColor: UIColor {
let privateColor = Selector("_systemDestructiveTintColor")
if UIColor.respondsToSelector(privateColor),
let red = UIColor.performSelector(privateColor).takeUnretainedValue() as? UIColor {
return red
}
return UIColor.redColor()
}
}
Usage
let color = UIColor.systemDestructiveTintColor
print(color)
Update - breaking down unsafebit cast
To address the question in the comments:
then why would they expose the unsafeBitCast API in the first place?
The documentation for unsafeBitCast()
says the following:
- Warning: Breaks the guarantees of Swift's type system; use with extreme care. There's almost always a better way to do anything.
unsafeBitCast
is defined in swift/stdlib/public/core/Builtin.swift as
@_transparent
public func unsafeBitCast<T, U>(_ x: T, to: U.Type) -> U {
_precondition(sizeof(T.self) == sizeof(U.self),
"can't unsafeBitCast between types of different sizes")
return Builtin.reinterpretCast(x)
}
Builtin.reinterpretCast
is defined in swift/include/swift/AST/Builtins.def as
/// reinterpretCast has type T -> U.
BUILTIN_SIL_OPERATION(ReinterpretCast, "reinterpretCast", Special)
ReinterpretCast
is the C++ identifier, "reinterpretCast"
is the string name in Swift and Special
is an attribute of the function which means the following (source):
The builtin has custom proccessing
So where is that custom processing? In swift/lib/AST/Builtins.cpp
case BuiltinValueKind::ReinterpretCast:
if (!Types.empty()) return nullptr;
return getReinterpretCastOperation(Context, Id);
...
...
static ValueDecl *getReinterpretCastOperation(ASTContext &ctx,
Identifier name) {
// <T, U> T -> U
// SILGen and IRGen check additional constraints during lowering.
GenericSignatureBuilder builder(ctx, 2);
builder.addParameter(makeGenericParam(0));
builder.setResult(makeGenericParam(1));
return builder.build(name);
}
Summary
The purpose of unsafe bit cast is to blindly change a type of 1 object to another. The protocol instance method just happens to work because if you treat the bits of @objc protocol UIColorPrivate { func styleString() -> UIColor }
like it was UIColor.styleString() -> UIColor
the correct method is called.
It's not at all strange that it doesn't work with class methods; in fact i'd say it's miraculous that it works for instance methods.
回答3:
but is there a way to expose these private class methods
That's like saying you want a vegan meal consisting of steak. You can have the steak, but that would not be a vegan meal. The words "private" and "expose" are opposites.
Using Objective-C's dynamic messaging solves the problem. You can use it by way of performSelector
and its family of methods. You already know this, so it's hard to see what more can be wanted.
If you prefer to use #selector
syntax, you can create a dummy class
protocol with a static
method containing your target function, to give yourself a way to refer to the method. Your whole unsafeBitCast
route, however, is going nowhere.
EDIT You can send any known message to any Objective-C object by casting to AnyObject, and you can use my dummy class
protocol to make the message known:
@objc protocol Dummy {
func hiddenMethod()
}
(someObject as AnyObject).hiddenMethod()
But I don't see why this is better than the protocol and #selector
syntax.
来源:https://stackoverflow.com/questions/38108312/how-do-i-expose-a-private-class-method-of-an-objective-c-object-using-protocols