Last evening a colleague asked whether it was possible to test private APIs in Swift playgrounds. Answer? Sort of.
Swift is not, as you will probably be unshocked to learn, not particularly friendly to API hacking. Playgrounds make it worth the bother by offering a perfect fit for exploration, testing, and prototyping.
Here’s what you do.
Set up a module workspace (File > New Project > Framework & Library > Cocoa Touch Framework, select Language > Swift) and add a playground to the workspace.
UIView’s recursiveDescription API is unpublished. By default, it’s not visible to the playground.
Playground execution failed: MyPlayground.playground:4:1: error: 'UIView' does not have a member named 'recursiveDescription' v.recursiveDescription() ^ ~~~~~~~~~~~~~~~~~~~~
You access unpublished methods by calling performSelector at runtime or adding an @interface declaration for compile time use. Swift doesn’t support either approach. You work around Swift by adding the interface approach to your workspace module’s primary .h file.
The extension lives on the Objective-C side of the house. You must compile the module and then possibly quit and restart Xcode before the update becomes visible to your playground. (This latter thing is a known Xcode bug and hopefully gets fixed soon.)
The recursiveDescription method is now ready for playing.
A few things to keep in mind:
- You may want to enable the simulator for any APIs that require OpenGL, Animations, core AVFoundation support, etc.
- Add extended execution via the XCPlayground module, enabling your app to establish a runloop beyond a playground’s normal top-to-bottom execution.
- You’re limited to Simulator-ready APIs, so don’t expect to use the playground for features like Siri dictation (UIDictationController)
- For obvious reasons, unpublished API use isn’t App Store safe.
- When in doubt, just use Objective-C. It’s a lot easier even if you don’t have access to the playground. (See this github project for Objective-C playgrounds)
Like the hacks? Read the book.
Update: A little more.
I decided to see whether I could access framework components through Objective-C and dlfcn/dlopen/dlsym. The playground’s sandbox prevents this while Objective-C modules work around the limitation. (The iOS simulator can see your entire disk, a very handy thing when you want to log or save test images to your desktop.)
I added a function to instantiate symbols from XCPlayground (which is private and cannot be imported normally but is otherwise of really limited use to access).
void *InstantiateFrameworkSymbol(NSString *string) { // Path is /Applications/Xcode.app/Contents/etc... void *xcp = dlopen(PATH_TO_FRAMEWORK, RTLD_LAZY); void *x = dlsym(xcp, string.UTF8String); dlclose(xcp); return x; }
I also added a function that would execute a CFunction pointer, in this case one that takes no arguments and returns an NSString.
NSString *ExecuteCFunctionReturningNSString(NSString *(*fptr)()) { return fptr(); }
On the playground side of things, I used these calls to retrieve a string, a float, and executed a simple C function that returned an NSString :
var p1 = UnsafePointer<CChar>.init(InstantiateFrameworkSymbol("XCPlaygroundVersionString")) if let s = String.fromCString(p1) { println("Playground Version \(s)") } var p2 = UnsafePointer<Double>.init(InstantiateFrameworkSymbol("XCPlaygroundVersionNumber")) println("Version Number \(p2.memory)") var p3 = InstantiateFrameworkSymbol("DVTPlaygroundSharedDataDirectory") let p3CPtr : CFunctionPointer = unsafeBitCast(p3, CFunctionPointer<() -> String!>.self) println(ExecuteCFunctionReturningNSString(p3CPtr))
Note that this kind of work is not just fairly pointless, it’s also really frustrating because playground module support is pretty iffy right now. Prepare to quit and restart Xcode a lot, clean out derived data regularly, and hunt down why your code updates aren’t “taking”.
Comments are closed.