Update note: This tutorial has only been tested with Xcode 6.3.2 — if you’re working with another version of Xcode, your experience might not match up perfectly with this tutorial.
The “one size fits all” paradigm that Apple extends to its products can be a tough pill to swallow. Although Apple has forced its workflows onto iOS/OS X developers, it’s still possible to bend Xcode to your will through plugins!
There isn’t any official Apple documentation on how to create an Xcode plugin, but the development community has put a tremendous amount of work into some pretty useful tools to help aid developers.
From autocompletion for images, to nuking your Derived Data to a vim editor, the Xcode plugin community has pushed the boundaries of what was originally thought capable.
In this epic three-part tutorial, you’ll create an Xcode plugin to prank your co-workers, featuring none other than the best prankster in these parts — Ray himself! And although the plugins are lighthearted in nature, you’ll still learn a lot about tracing through Xcode, how to find the elements you want to modify, and how to swizzle in your own functionality!
You’ll be inspecting undocumented frameworks using your x86 assembly knowledge, code navigating skills, and LLDB skills while exploring private APIs and injecting code using method swizzling. Since there’s a lot to cover, this tutorial will move very quickly. Make sure you’re comfortable in iOS or OS X development before proceeding!
Plugin development with Swift severely complicates an already tricky topic, and the Swift debugging tools are just not up to par with Objective-C yet. For now, that means the best choice for plugin development (and this tutorial!) is Objective-C.
Getting Started
To celebrate Prank Your Co-Worker Day (aka every day!), your Xcode plugin will Rayroll your victim. Wait, what’s Rayrolling? It’s the copyright & royalty free version of Rickrolling, where you bait-and-switch your victims with content that’s different than what was expected. When you’ve completed this series, your plugin will modify Xcode so that it will:
- Replace Ray’s face on the Xcode alerts (i.e. Build Succeeded/Build Failed).
- Replace Xcode’s titlebar contents with lyrics from Ray’s hit song, Never Gonna Live You Up.
- Replace all Xcode documentation requests to a Rayroll’d video.
In this first part of the tutorial, you’ll focus on hunting down the class responsible for displaying the “Build Succeeded” alert and modify the image it displays with a good ol’ pic of Ray.
Installing the Alcatraz Plugin
Before you do anything, you’ll need to install Alcatraz. The Xcode plugin Alcatraz acts as a Xcode plugin manager; it’s from the talented developers @kattrali, @_supermarin, and @JurreTweet.
Type the following command into Terminal to install Alcatraz:
curl -fsSL https://raw.github.com/supermarin/Alcatraz/master/Scripts/install.sh | sh |
Restart Xcode once the script has finished. You might see an alert warning you of the Alcatraz bundle; go ahead and click Load Bundle to continue. You do want to power-up your Xcode, right?
defaults delete com.apple.dt.Xcode DVTPlugInManagerNonApplePlugIns-Xcode-6.3.2 |
You’ll see a new option in the Xcode Window menu section called Package Manager. Creating an Xcode plugin requires you to muck around with the Build Settings to launch and attach Xcode to another instance of Xcode. Fortunately, @kattrali has already done the work for you and created a plugin, which creates a template…which creates a plugin.
Open the Alcatraz plugin by navigating to Window\Package Manager. In the Alcatraz search dialog, search for Xcode Plugin. Make sure that you have the All and Templates attributes selected on the search window. Once you’ve located the Xcode Plugin Template, click the Install button on the left to install it:
Once Alcatraz has downloaded the plugin, create a new project by navigating to File\New\Project…. Select the new OS X\Xcode Plugin\Xcode Plugin template option and click Next.
Name the Product Rayrolling, set the Organization Identifier as com.raywenderlich (this is important), and choose Objective-C for the Language.. Save the project to whatever directory you desire.
The Hello World Plugin Template
Build and run your new Rayroll project; you’ll see a new child instance of Xcode appear. This child instance has a new option in the Edit menu named Do Action:
Selecting this item will launch a modal alert:
Plugins are tagged to work with specific versions of Xcode. This means that when a new Xcode version comes out, all 3rd party plugins created by the community will fail until they add the UUID specific to that version. If this particular template doesn’t work and you don’t see the new menu action, you may need to add support for your version of Xcode.
To add this, run the following command in Terminal:
defaults read /Applications/Xcode.app/Contents/Info DVTPlugInCompatibilityUUID |
This command will output a UUID for your current Xcode. Open the Info.plist of the plugin and navigate to the DVTPlugInCompatibilityUUID key to add the value to the array:
Note: Throughout this tutorial, you’ll be running and making changes to an installed Xcode plugin. This will change the behavior of Xcode, and potentially make Xcode crash! If you want to disable a plugin, you’ll need to manually remove it using Terminal:
cd ~/Library/Application\ Support/Developer/Shared/Xcode/Plug-ins/ rm -r Rayrolling.xcplugin/ |
…and then restart Xcode.
Finding the Features to Modify
One tried and true way to get a basic grounding of what’s going on behind the scenes is to have a NSNotification observer that listens to all events fired by Xcode. By using Xcode and observing these notifications as they fire, you will be able to take a peek at the underlying classes.
Open Rayrolling.m and add the following property to the class:
@property (nonatomic, strong) NSMutableSet *notificationSet; |
This NSMutableSet
stores all the NSNotification
names the Xcode console spits out.
Next, add the following code to initWithBundle:
, after the if (self = [super init]) {
line:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:nil object:nil]; self.notificationSet = [NSMutableSet new]; |
Using nil
in the name
parameter, indicates you want to listen for all the notifications that Xcode passes around.
Now implement handleNotification:
as shown below:
- (void)handleNotification:(NSNotification *)notification { if (![self.notificationSet containsObject:notification.name]) { NSLog(@"%@, %@", notification.name, [notification.object class]); [self.notificationSet addObject:notification.name]; } } |
handleNotification:
checks that the notification name is in the notificationSet
; if it’s not, print the notification’s name
and class
and add it to the set. This way, you’ll only see each notification reported once.
Next, find and replace the added action menu item declaration to update its title text:
NSMenuItem *actionMenuItem = [[NSMenuItem alloc] initWithTitle:@"Reset Logger" action:@selector(doMenuAction) keyEquivalent:@""]; |
This is a minor modification to the NSMenuItem
title so you know that it will reset the NSNotification
set when you click on the menu action.
Finally, replace the implementation of doMenuAction
with the following:
- (void)doMenuAction { [self.notificationSet removeAllObjects]; } |
The menu item will now reset all the notifications in the notificationSet
property. This will let you examine notifications of interest with less “console noise.”
Build and run the plugin again to relaunch the child Xcode. Make sure you clearly separate the parent Xcode that is running the instance of the debugged child Xcode, because the parent will not have your most recent plugin changes incorporated until you relaunch Xcode into memory.
Play around with the child Xcode; click on buttons, open windows and explore the application while keeping an eye on the parent Xcode’s console as the notifications fire.
Finding and Inspecting the Build Status Alert
Now that you have your basic grounds for inspecting the NSNotification
names triggered in Xcode, you need to turn your attention to figuring out the class associated with displaying the build alert.
Launch the Xcode plugin. In the child Xcode, open any project. Make sure you have bezel notifications enabled – in your Xcode settings, enable them for when builds succeed and fail. Again, make sure you’re changing the settings in your child Xcode instance!
Reset the notificationSet
using the Reset Logger menu item you created, then run the child Xcode project.
As the child Xcode’s build succeeds (or fails), keep an eye on the console messages. Skim through the console and see if there is anything of interest. Can you spot any notifications that look like they’re worth inspecting further? The solution is below in case you need a little help…
You’ll pick one of these and explore it further to see what information you can dig out of it.
What about NSWindowWillOrderOffScreenNotification? Good choice! You’ll explore that one.
Still in Rayrolled.m, navigate to handleNotification:
and add a breakpoint on the first line as shown by the image and corresponding steps below:
- Hover over the breakpoint, right-click the breakpoint and select Edit Breakpoint.
- In the condition section, paste
[notification.name isEqualToString:@"NSWindowWillOrderOffScreenNotification"]
- In the Action Section, add
po notification.object
- If the parent Xcode is not already running the child Xcode, launch the build, then launch a build in the child Xcode. The breakpoint will stop on the NSWindowWillOrderOffScreenNotification notification. Observe the
-[notification object]
printed out. This isDVTBezelAlertPanel
, the first of many private classes that you’ll be exploring.
You now have a potential lead. You have a class named DVTBezelAlertPanel
, and more importantly, you have an instance of this class in memory. Unfortunately, you don’t have any headers for this file to determine if this is the instance responsible for displaying the Xcode alert. Hmm.
Actually…it is possible to obtain this information. Although you don’t have the headers for this class, you do have a debugger attached to the child Xcode, and memory can tell you as good of a story as any header file could.
While still paused in the debugger of the parent Xcode, enter the following in the parent Xcode’s LLDB Console:
(lldb) image lookup -rn DVTBezelAlertPanel 17 matches found in /Applications/Xcode.app/Contents/SharedFrameworks/DVTKit.framework/Versions/A/DVTKit: ... |
This searches for the name DVTBezelAlertPanel
within Xcode, as well as in any frameworks, libraries, and plugins loaded into the Xcode process, and spits out any matching contents. Look at the listed methods. Are there any methods within the DVTBezelAlertPanel
image dump that could help correlate this class to this error message? I’ve provided some help below if you need it.
image lookup
LLDB command will list methods that are implemented in memory. When applying this to a particular class, this does not take into account subclassing where other methods inherit from superclasses. That is, the lookup of this command will omit any methods only declared in superclasses provided the subclasses do not override the super
method.Without moving or stepping in the LLDB console, inspect the contentView
property in LLDB with the following command:
(lldb) po [notification.object controlView] <nil> |
The console spits out nil
. Darn. Maybe this is because the controlView
isn’t set at this time. Time to try a different tactic.
initWithIcon:message:parentWindow:duration
and initWithIcon:message:controlView:duration:
looked somewhat juicy. Since you know that the DVTBezelAlertPanel
instance is already alive, one of these twp method calls must have already occurred. You’ll need to attach a breakpoint to both of these methods through the LLDB console and try and trigger this class’s initialization again.
While remaining paused in the LLDB console, type the following:
(lldb) rb 'DVTBezelAlertPanel\ initWithIcon:message:' Breakpoint 1: 2 locations. (lldb) br l ... |
This sticks a regular expression breakpoint on both of DVTBezelAlertPanel
‘s initializers referenced above. Since both methods have the same starting text in their initialization, the regex breakpoint will match both items. Make sure you have a \ before the space and surround the expression in single quotes so LLDB knows how to properly parse the regex.
Resume the child application, then re-build the child project. You’ll hit the initWithIcon:message:parentWindow:duration
breakpoint in the debugger.
If you didn’t hit it, make sure you added the breakpoint to the parent Xcode, with the child Xcode running a project. Xcode will break on the assembly for this method since it doesn’t have the corresponding sourcefile for it.
Now that you’ve arrived in a method for which you don’t have the source code, you’ll need a way to print out the function parameters sent to the method. This is perhaps a good time as any to talk about…ASSEMBLY! :]
A Whirlwind Detour of Assembly
When working with private APIs, you’ll need to inspect registers instead of using the debug symbols typically available to you when working with source code. Knowing how registers behave on the x86-64 architecture can be tremendously helpful.
Although not a required read, this article is a good resource for catching up on x86 Mach-0 assembly. In Part 3 of this tutorial series, you’ll rip apart a method in the disassembler to see what it’s doing, but for now, you’ll take the easy road.
It’s worth noting the following registers and how they function:
- $rdi: This register references the
self
parameter passed into a method; this is the first parameter passed in. - $rsi: Refers to the Selector passed. This is the second parameter.
- $rdx: The third parameter passed into a function, and the first parameter of an Objective-C method.
- $rcx: The fourth parameter passed into a function, and the second parameter of an Objective-C method.
- $r8: The fifth parameter passed into a function. $r9 is used for the 6th param followed by the stack frame used, if there are any more parameters required for the function call.
- $rax: Return values are passed in this register. For example, when stepping out of –
[aClass description]
, $rax will contain anNSString
of theaClass
instance’s description.
doubles
using the $xmm register family. Use the above quick reference as a guide.
Applying theory in practice to a real world example, take the method below.
@interface aClass : NSObject - (NSString *)aMethodWithMessage:(NSString *)message; @end @implementation aClass - (NSString *)aMethodWithMessage:(NSString *)message { return [NSString stringWithFormat:@"Hey the message is: %@", message]; } @end |
If you executed it like this:
aClass *aClassInstance = [[aClass alloc] init]; [aClassInstance aMethodWithMessage:@"Hello World"]; |
When compiled, the call to aMethodWithMessage:
would be passed to objc_msgSend
and would look roughly like this:
objc_msgSend(aClassInstance, @selector(aMethodWithMessage:), @"Hello World") |
aMethodWithMessage:
found in, aClass
, would yield the following results in the set of registers:
Immediately upon calling aMethodWithMessage:
:
- $rdi: Would contain an instance of
aClass
. - $rsi: Would contain the SEL
aMethodWithMessage:
, which is basically achar *
(trypo (SEL)$rsi
in lldb). - $rdx: Would contain the contents of
message
, which will be the reference to the instance of:@"Hello World"
.
Immediately upon leaving the method:
- $rax: Would contain the return value, which would be an instance of
NSString
. For this particular case, it would be an instance of:@"Hey the message is: Hello World"
.
x86 Register Dumpster Diving
Now that you’re an official assembly register wizard, it’s time to revisit DVTBezelAlertPanel
‘s initWithIcon:message:parentWindow:duration:
. Hopefully you haven’t moved from the break on this method. If you have, re-run the child Xcode to get there again. Remember, you’re searching for a clue that this class is the class responsible for showing Xcode’s Build Succeeded alert.
While stopped in initWithIcon:message:parentWindow:duration
, type the following in LLDB:
(lldb) re re |
This command is an abbreviated way of saying register read
, which will print the significant registers available on your machine.
Using what you’ve learned about reading x86_64 registers, examine the register responsible for the message:
parameter and the 4th objc_msgSend param. Do the contents match the expected alert string?
Augment the $rcx register with a new string and see if the alert changes to be 100% sure:
(lldb) po [$rcx class] __NSCFConstantString (lldb) po id $a = @"Womp womp!"; (lldb) p/x $a (id) $a = 0x000061800203faa0 (lldb) re w $rcx 0x000061800203faa0 (lldb) c |
The application then resumes. Note the augmented alert message that you changed while in the debugger. You can now safely make the assumption that this class is associated with the build alerts. Took a bit to figure it out, didn’t it?
Code Injection
You’ve found the class that you’re interested in. Now it’s time to inject code to augment DVTBezelAlertPanel
‘s behavior to display a lovely Rayrolling face when an alert occurs.
Time to use method swizzling!
Since you could potentially swizzle numerous methods from different classes, it would be best to use a Category on NSObject
to create a convenience method to perform setup logic.
Select File\New\File… and select the OS X\Source\Objective-C File template. Name the file MethodSwizzler and make it of file type Category and class NSObject.
Open NSObject+MethodSwizzler.m and replace its contents with the code below:
#import "NSObject+MethodSwizzler.h" // 1 #import <objc/runtime.h> @implementation NSObject (MethodSwizzler) + (void)swizzleWithOriginalSelector:(SEL)originalSelector swizzledSelector:(SEL) swizzledSelector isClassMethod:(BOOL)isClassMethod { Class cls = [self class]; Method originalMethod; Method swizzledMethod; // 2 if (isClassMethod) { originalMethod = class_getClassMethod(cls, originalSelector); swizzledMethod = class_getClassMethod(cls, swizzledSelector); } else { originalMethod = class_getInstanceMethod(cls, originalSelector); swizzledMethod = class_getInstanceMethod(cls, swizzledSelector); } // 3 if (!originalMethod) { NSLog(@"Error: originalMethod is nil, did you spell it incorrectly? %@", originalMethod); return; } // 4 method_exchangeImplementations(originalMethod, swizzledMethod); } @end |
Taking each numbered comment in turn:
- This is the magical header responsible for declaring the functions used for method swizzling.
isClassMethod
indicates if the methods are class methods or instance methods.- When you don’t have the help of the compiler to autocomplete your methods, it’s easy to misspell them. This is a check to make sure that you are declaring your
SEL
s accurately. - This is the function that will switch your implementations around.
Declare swizzleWithOriginalSelector:swizzledSelector:isClassMethod
in NSObject+MethodSwizzler.h like so:
#import <Foundation/Foundation.h> @interface NSObject (MethodSwizzler) + (void)swizzleWithOriginalSelector:(SEL)originalSelector swizzledSelector:(SEL) swizzledSelector isClassMethod:(BOOL)isClassMethod; @end |
Now it’s time to actually swizzle! Create another new Category called Rayrolling_DVTBezelAlertPanel which inherits from NSObject.
Replace the contents of NSObject+Rayrolling_DVTBezelAlertPanel.m with the following:
#import "NSObject+Rayrolling_DVTBezelAlertPanel.h" // 1 #import "NSObject+MethodSwizzler.h" #import <Cocoa/Cocoa.h> // 2 @interface NSObject () // 3 - (id)initWithIcon:(id)arg1 message:(id)arg2 parentWindow:(id)arg3 duration:(double)arg4; @end // 4 @implementation NSObject (Rayrolling_DVTBezelAlertPanel) // 5 + (void)load { static dispatch_once_t onceToken; // 6 dispatch_once(&onceToken, ^{ // 7 [NSClassFromString(@"DVTBezelAlertPanel") swizzleWithOriginalSelector:@selector(initWithIcon:message:parentWindow:duration:) swizzledSelector:@selector(Rayrolling_initWithIcon:message:parentWindow:duration:) isClassMethod:NO]; }); } // 8 - (id)Rayrolling_initWithIcon:(id)icon message:(id)message parentWindow:(id)window duration:(double)duration { // 9 NSLog(@"Swizzle success! %@", self); // 10 return [self Rayrolling_initWithIcon:icon message:message parentWindow:window duration:duration]; } @end |
Broken down, the code is relatively straightforward:
- Make sure to import the method that can enable swizzling.
- You forward declare all the methods that you intend on using. Although this is not required, it makes the compiler play nice by autocompleting your code. In addition, this trick suppresses any warnings about undeclared methods.
- This is the actual private method you will be swizzling with.
- Since you don’t want to redeclare a private class, you’re opting for a category instead.
- This is the heart of the code injecting “trick”. You’ll perform the injecting in
load
.load
is unique in that it has a “to-many relationship”. That is, multiple categories of the same class can all implement aload
command and have them all execute. - Since
load
can be called multiple times, you usedispatch_once
. - This uses the
NSObject
category method you implemented earlier. Note that you retrieve the private class dynamically at runtime usingNSClassFromString
. - This is the replacement method for the original one. It’s good practice to use a unique namespace convention that only you could have come up with.
- This is a basic test to see if the swizzling worked by printing out to the console.
- Since you’re swizzling this method with the original one, when you call the swizzled method, it will still call the original method. This means you could add code before or after the original method is called, or even go so far as to change the parameters passed to the original function… which you’ll do in just a moment.
Congratulations! You’ve now successfully injected code into a private method of a private class! Build the parent Xcode, and then use the build of the child Xcode to see the added console message which was swizzled in.
Now it’s time to replace all alert images with the Rayrolling face. Download the lovely image created by our resident image swizzler Crispy from here and add the image into the Xcode project. Make sure to select Copy Items if Needed.
Navigate back to Rayrolling_initWithIcon:message:parentWindow:duration
and change its content to the following:
- (id)Rayrolling_initWithIcon:(id)arg1 message:(id)arg2 parentWindow:(id)arg3 duration:(double)arg4 { if (arg1) { NSBundle *bundle = [NSBundle bundleWithIdentifier:@"com.raywenderlich.Rayrolling"]; NSImage *newImage = [bundle imageForResource:@"IDEAlertBezel_Generic_Rayrolling.pdf"]; return [self Rayrolling_initWithIcon:newImage message:arg2 parentWindow:arg3 duration:arg4]; } return [self Rayrolling_initWithIcon:arg1 message:arg2 parentWindow:arg3 duration:arg4]; } |
This method now checks that an image was passed to the original method, and replaces it with the Rayrolling image. Note that you had to use the +[NSBundle bundleWithIdentifier:]
to load the image because it’s not contained in your mainBundle
.
Build and run the project; quit out of all instances Xcodes and restart fresh.
Beautiful! :]
Toggling and Persisting the Rayroll
You’re designing this plugin to be annoying; no doubt you’d like to toggle it on and off as you work on it, while having your selection persist across the various instances of Xcode using NSUserDefaults
.
Navigate to Rayrolling.h and add the following property to the header file:
+ (BOOL)isEnabled; |
Now go to Rayrolling.m and add the following methods:
+ (BOOL)isEnabled { return [[NSUserDefaults standardUserDefaults] boolForKey:@"com.raywenderlich.Rayrolling.shouldbeEnable"]; } + (void)setIsEnabled:(BOOL)shouldBeEnabled { [[NSUserDefaults standardUserDefaults] setBool:shouldBeEnabled forKey:@"com.raywenderlich.Rayrolling.shouldbeEnable"]; } |
You have the logic to persist your selection; now you need to enable a toggle in the GUI.
Back in Rayrolling.m, modify -(void)doMenuAction
to look like the following:
- (void)doMenuAction:(NSMenuItem *)menuItem { [Rayrolling setIsEnabled:![Rayrolling isEnabled]]; menuItem.title = [Rayrolling isEnabled] ? @"Disable Rayrolling" : @"Enable Rayrolling"; } |
This will simply toggle the boolean to either enable or disable Rayrolling.
Finally, change the menu setup code in didApplicationFinishLaunchingNotification:
to look like the following:
NSMenuItem *menuItem = [[NSApp mainMenu] itemWithTitle:@"Edit"]; if (menuItem) { [[menuItem submenu] addItem:[NSMenuItem separatorItem]]; NSString *title = [Rayrolling isEnabled] ? @"Disable Rayrolling" : @"Enable Rayrolling"; NSMenuItem *actionMenuItem = [[NSMenuItem alloc] initWithTitle:title action:@selector(doMenuAction:) keyEquivalent:@""]; [actionMenuItem setTarget:self]; [[menuItem submenu] addItem:actionMenuItem]; } |
You now have an NSMenuItem
which will persist across Xcode launches and retain the setting to to enable or disable the logic.
Navigate back to NSObject+Rayrolling_DVTBezelAlertPanel.m, and add the following import:
#import "Rayrolling.h" |
Finally, open Rayrolling_initWithIcon:message:parentWindow:duration:
and replace this line:
if (arg1) { |
…with:
if ([Rayrolling isEnabled] && arg1) { |
Build and run the program so the changes propagate to the plugin.
Boom! You now have a plugin which modifies the Xcode alert and can be toggled on and off. Pretty nice for a day’s work, eh?
Where to Go From Here?
You can download the completed Rayrolling project from this part of the tutorial.
You’ve made a ton of progress — but there’s still a lot more to do! In part 2, you’ll learn the basics of Dtrace and explore some advanced LLDB features to look into running processes such as Xcode.
If you want to get ahead, there’s some homework to do before you get to part 3, where you’ll see a lot of assembly code. Make sure you start learning now and have a decent understanding of x86_64 assembly by checking out Part I and Part II of Mike Ash’s series on disassembling assembly. These two articles will greatly aid in what’s to come.
Good luck and have fun exploring! If you had any comments or questions from this tutorial, feel free to join the forum discussion below!
How To Create an Xcode Plugin: Part 1/3 is a post from: Ray Wenderlich
The post How To Create an Xcode Plugin: Part 1/3 appeared first on Ray Wenderlich.