In Objective-C, if you want to make use of one of Apple's internal classes, all you have to do is declare the class's interface with the methods you want to use. From there you can create instances of the class by using the NSClassFromString
function to obtain a reference to the class object, like so:
[[NSClassFromString(@"_NSSomeClass") alloc] initWithFoo:5]
In Swift, this is not so easy. Referencing the type of the "hollow" Objective-C class interface anywhere in Swift code makes the compiler query the class for type information that is not known at compile time. This presents a huge problem. You cannot dynamically initialize a type either. Though, it seems the following code used to work at one point:
let cls: NSObject.Type = NSClassFromString("_NSSomeClass") as NSObject.Type
let instance = cls()
Of course, Swift now requires an explicit cast using as!
and that type objects be initialized with an explicit call to init
like this: cls.init()
, but it still doesn't compile anymore.
The good news is, there is a workaround. We can make a hollow Objective-C interface to return an instance of the internal class and use it just like we would in Objective-C as long as this hollow class has the same interface methods as the private class we wish to use.
Make a new Objective-C class with the interface of the class you want to use, hereby referred to as the facade class. For the sake of this example, lets say we want to use _UITapticEngine
, and that we want to use its actuateFeedback:
method. Let's call our facade TapticEngine
. Override init
in your facade class to return an instance of the desired internal class.
// TapticEngine.h
@interface TapticEngine : NSObject
- (void)actuateFeedback:(NSInteger)type;
@end
// TapticEngine.m
#pragma clang diagnostic ignored "-Wincomplete-implementation" // Silence that warning if you want
@implementation TapticEngine
- (id)init {
return [NSClassFromString(@"_UITapticEngine") new];
}
@end
In Swift, you can now use the _UITapticEngine
class through our new TapticEngine
class:
let engine = TapticEngine()
engine.actuateFeedback(3) // phone should vibrate
Our TapticEngine
class is never actually initialized, instead forwarding the init
call to the internal class, hence the term "facade." Any method calls will work as long as your facade class uses method names and types identical to that of the internal class.
How about this for initialization?
let tapticEngine = (objc_getClass("_UITapticEngine") as! NSObject).perform(NSSelectorFromString("new")).takeRetainedValue() as! NSObject
This way, you can simply put the
_UITapticEngine
interface in your bridging header and use it from Swift (just replaceas! NSObject
withas! _UITapticEngine
).