How to create an Xcode plugin

How to create an Xcode plugin

[[163627]]

  • Original text: How To Create an Xcode Plugin: Part 1/3
  • Original author: Derek Selander
  • Translator: @yohunl

Apple's "one size fits all" strategy makes its products more and more like a hard pill to swallow. Although Apple has brought some workflows to iOS/OS X developers, we still hope to make Xcode more convenient through plugins!

Although Apple does not provide any official documentation to guide us on how to create an Xcode plugin, the developer community has done a lot of work to develop some very useful tools that can be used to help developers.

From plugins that autocomplete image names, to plugins that clear cache, to plugins that turn Xcode into a vim editor, Xcode's plugin community has expanded our thinking and we can make Xcode smarter.

In this not-so-short three-part tutorial, you'll create an Xcode plugin to entertain your coworkers by showing them something they don't expect to see! Even though this plugin is lightweight, you'll still learn a lot, such as how to debug Xcode, find the element you're interested in and modify it, and how to replace system functions with your own (via swizzling)!

You will use x86 assembly knowledge, code definition skills, and LLDB debugging skills to view undisclosed private frameworks, explore private APIs in these private frameworks, and use method swizzleing to inject code. Because there is so much content, this tutorial will be explained very quickly. Before continuing, please make sure you have mastered the relevant iOS/OS X development.

Developing plugins in Swift is still a complex topic, and Swift's debugging tools are still much weaker than Objective-C. For now, this means that the best choice for plugin development (for this tutorial!) is Objective-C.

start

To celebrate Prank Your Coworkers Day, your Xcode plugin will Rayroll your victims. Wait… What is Rayrolling? It’s a free and royalty-free Rayrolling Rick Shake – where you see something other than what you expect. When you complete this series, your plugin will change what Xcode displays:

Replace some Xcode alert boxes with Ray's head (for example, Xcode prompt boxes for build success or failure)

Replace the Xcode title with a line from Ray's hit song, Never Gonna Live You Up

Replace all Xcode search document content with a video Rayroll'd video

In the first part of the tutorial, we will focus on finding the class responsible for displaying the "Build Success" alert box and changing its image to Ray's head image.

Install Plugin Manager Plugin Alcatraz

Before you begin, you need to install Alcatraz, the Xcode plugin management tool.

The typical way to install Alcatraz is through the command line

  1. curl -fsSL https: //raw.github.com/supermarin/Alcatraz/master/Scripts/install.sh | sh  

When this command is finished, restart Xcode. You may see an alert box prompting the Alcatraz bundle; click Load Bundle to continue so that Xcode can load the Alcatraz plugin so that the Alcatraz plugin can work.

NOTE: If you accidentally click "skip bundle", you can always get it back by typing the following command from the command line!

  1. defaults delete com.apple.dt.Xcode DVTPlugInManagerNonApplePlugIns-Xcode- 7.2 . 1  

The above Xcode 7.2.1 is the Xcode version number on your machine. If yours is not 7.2.1, just change it to the corresponding version number.

You will see a new menu item under the Window menu of Xcode: Package Manager. To create an Xcode plugin, you need to set Build Settings to run another new instance of Xcode to load it. This is a boring and tedious process (if you want to know this boring process, you can refer to my article Xcode7 Plugin Making Primer). Fortunately, someone has already done this for you. Someone has developed an Xcode project template that allows you to easily create a plugin project.

Open Alcatraz (Window->Package Manager). Type Xcode Plugin in the Alcatraz search box. Make sure you check both All and Templates in the search box. Once you find it, click Install to the left to install it!!

If you can't find it, it doesn't matter. You can go to https://github.com/kattrali/Xcode-Plugin-Template and download it yourself to load it. For specific installation methods, see the project instructions.

Once Alcatraz has downloaded the Xcode Plugin, you can create a plugin project (File->New -> Project…), select the new OS X ->Xcode Plugin ->Xcode Plugin template, and click Next.

Name the project Rayrolling, set the organization identifier to com.raywenderlich (this step is very important), and select Objective-C as the language. Save the project in any directory you want.

Hello World plugin template

Compile and run the Rayroll project, and you will see a new Xcode instance appear. This Xcode instance has an additional menu item Do Action under the Edit menu bar:

Selecting this menu item will cause a modal popup to appear:

Starting from Xcode5, plugins can only run in a specific version of Xcode. This means that when a new Xcode update is installed, all third-party plugins will be invalid unless you add the UUID of that version of Xcode. If some templates do not work and you do not see a new menu item, one of the possible reasons is that there is no UUID for the corresponding version, and you need to add support for the corresponding version of Xcode.

To add the UUID, first run the following command in the command line

  1. defaults read /Applications/Xcode.app/Contents/Info DVTPlugInCompatibilityUUID

This command will output the UUID of the current version of Xcode. Open the Info.plist file of the Rayroll project. Navigate to DVTPlugInCompatibilityUUID and add it

Note: Throughout this tutorial, you will run and modify installed plugins. This will change Xcode's default behavior, and of course, it may also cause Xcode to crash! ! If you want to disable a plugin, you can manually delete it through the terminal.

  1. cd ~/Library/Application\ Support/Developer/Shared/Xcode/Plug-ins/
  2. rm -r Rayrolling.xcplugin/

Then restart Xcode

Find a feature of Xcode that we want to modify

The most direct and effective way to get what's happening behind the scenes is to register an NSNotification observer to listen to all Xcode events. By listening to these message notifications through Xcode, you will go deep into the internals of some internal classes.

Open Rayrollling.m and add the following properties to the class:

  1. @property (nonatomic,strong) NSMutableSet *notificationSet;

This NSMutableSet is used to store all the NSNotification names printed out by the Xcode console

Next, in initWithBundle:, after if (self = [super init]) {, add the following code:

  1. [[NSNotificationCenter defaultCenter] addObserver:self selector: @selector (handleNotification:) name:nil object:nil];
  2.    
  3. self.notificationSet = [NSMutableSet new ];

Passing nil to the name parameter indicates that we need to listen to all NSNotifications of Xcode.

Now, implement the handleNotification: method:

  1. - ( void )handleNotification:(NSNotification *)notification {
  2. if (![self.notificationSet containsObject:notification.name]) {
  3. NSLog(@ "%@, %@" , notification.name, [notification.object class ]);
  4. [self.notificationSet addObject:notification.name];
  5. }
  6. }

handleNotification: Checks if the obtained notification name is in notificationSet. If not, prints its notification name and the corresponding class of the notification in the console. Then add it to notificationSet. In this way, you will only see each type of notification once in the console.

Next, find the declaration for adding a menu item and replace it with the following code:

  1. NSMenuItem *actionMenuItem = [[NSMenuItem alloc] initWithTitle:@ "Reset Logger"  
  2. action: @selector (doMenuAction) keyEquivalent:@ "" ];

This code simply changes the title of the NSMenuItem to let you know when you click it, and it resets the set object that holds the NSNotification.

***, replace the implementation code of doMenuAction with the following

  1. - ( void )doMenuAction {
  2. [self.notificationSet removeAllObjects];
  3. }

This menu item will reset all notifications stored in the notificationSet property. The purpose of this is to make it easy for you to observe the notifications you are interested in in the console, without being overwhelmed by repeated messages in the console, so that you can focus more.

Compile and run again, please make sure you can distinguish which is your project's Xcode (the parent Xcode) and which is the Xcode instance you debugged (the child Xcode). Why do you need to distinguish them? Because every time you debug again, the changes you make will take effect in the debugged Xcode, while the parent Xcode will only take effect after you restart it.

In the child Xcode, just click some buttons, open some windows, browse around in the program, and you will see messages triggered in the console of the parent Xcode.

Find and monitor compiled prompts

Now that you have learned the basics of viewing notifications (NSNotification) raised by Xcode itself, you now need to find out specifically which class the tooltip that shows the compile status is associated with.

Run the Xcode plugin, open any project in the child Xcode, and make sure bezel notifications are turned on in Xcode settings, both Succeeds and Fails bezel notifications. Of course, make sure you are operating the child Xcode instance!

Reset notificationSet through the menu item Reset Logger you created in the edit menu of Xcode, and then run your sub-code (the above allows you to open any project in the sub-Xcode, now you run the opened project in the sub-Xcode)

When the child Xcode project compiles (either successfully or unsuccessfully), pay attention to the console output in the parent Xcode. Take a quick look to see if there are any notifications that you can pay attention to. Can you find some notifications that deserve your further attention? The following may help you (In the original text, the following list is hidden, you can click to display it, the author encourages you to look for it yourself first, and if you can't find it, then open the following prompt. Due to the limitations of the markdown editor I use, I can't do this, so I just put it here)

The following items deserve your further attention:

  • NSWindowWillOrderOffScreenNotification, DVTBezelAlertPanel

  • NSWindowDidOrderOffScreenNotification, DVTBezelAlertPanel

  • NSWindowDidOrderOffScreenAndFinishAnimatingNotification, DVTBezelAlertPanel

You should pick one of these and explore it further to see if you can get some important information from it. For example, what does NSWindowWillOrderOffScreenNotification do? Great, you chose to explore NSWindowWillOrderOffScreenNotification further.

Go back to the parent Xcode file Rayrolled.m, locate the handleNotification: method, add a breakpoint to the first line of the method, and set the breakpoint as follows:

  1. Place the mouse on this breakpoint, right-click on it, and select Edit Breakpoint.
  2. In the condition input box in the pop-up breakpoint edit box, add [notification.name isEqualToString:@"NSWindowWillOrderOffScreenNotification"]
  3. In the Action section, add po notification.object
  4. If the parent Xcode is already running, restart it to debug, and then compile and run a project in the generated child Xcode. The breakpoint in the parent Xcode should stop at the NSWindowWillOrderOffScreenNotification notification. Observe the value of -[notification object] in the console output DVTBezelAlertPanel, which is also one of the private classes that deserves your in-depth attention.

You now know that there is a class called DVTBezelAlertPanel, and more importantly, you know that there is an instance of this class in memory. Unfortunately, you can't find any header file about this class to tell you whether this class is the one that displays the Xcode alert box.

In fact, you can still get this information. Although we don't have a header file for this class, if you have a debugger connected to the child Xcode, the information in memory can still tell you relevant information about this class, just like you read its header file.

Note: Throughout this series of tutorials, LLDB output is usually accompanied by the standard console output. Any line beginning with (lldb) is considered an input line, where you can enter commands. Three dots... output in the console means that the console cannot print enough and ignores some of it. If the console displays too many print logs, you can simply press ? + K to clear the current output and re-accept the output

Make sure your parent Xcode is in debugging mode, the program stops at the breakpoint, and enter the following lldb command into the parent Xcode's lldb console:

  1. (lldb) image lookup -rn DVTBezelAlertPanel
  2. 17 matches found in /Applications/Xcode.app/Contents/SharedFrameworks/DVTKit.framework/Versions/A/DVTKit:
  3. ...(The ... here means that the output of the command above is omitted because there is too much)

This command searches any frameworks, libraries, and plugins loaded into the Xcode process for information about a class named DVTBezelAlertPanel, and then outputs the information found. Observe the methods listed in the search results. Have you been able to find some methods that can be used to associate the DVTBezelAlertPanel class with the compilation success/failure alert box that appears in the child Xocde? Below I provide a list of some methods that may help you. (In the original text, the following list is hidden, you can click to display it, the author encourages everyone to look for it first, and if you can't find it, then open the prompt below. Due to the limitations of the Markdown editor I use, I can't do this, so I just put it here).

Helpful methods

The following methods of the DVTBezelAlertPanel class are worth further exploration:

  • initWithIcon:message:parentWindow:duration
  • initWithIcon:message:controlView:duration:
  • controlView

Either of the two initialization methods above can basically help you verify whether the class DVTBezelAlertPanel is associated with the content of the prompt box that appears.

Note: LLDB's image lookup command only lists methods that are implemented in memory. When you use this to look up certain classes, it does not include methods that are inherited from the parent class, but the subclasses do not override them, that is, it only lists the methods that they implement.

Make sure you are still at the breakpoint in the parent Xcode and enter the following command in the parent class’s LLDB console to inspect contentView:

  1. (lldb) po [notification.object controlView]
  2. [nil] (The angle brackets are replaced with square brackets) (This is the output of the previous sentence, yohunl note)

The console output is nil.(⊙o⊙)..., probably because the contentView has not been initialized at this time. It doesn't matter, let's try the next one: initWithIcon:message:parentWindow:duration and initWithIcon:message:controlView:duration:, because you already know that there is an instance of the DVTBezelAlertPanel class in the memory, which means that these two initialization methods have been called. You need to add debugging breakpoints to these two methods, because we don't have its implementation file, so here we use the LLDB console to add breakpoints. Then trigger the initialization of this class again.

The parent Xcode is still stuck at the breakpoint, enter the following command

  1. (lldb) rb 'DVTBezelAlertPanel\ initWithIcon:message:'  
  2. Breakpoint 1 : 2 locations.
  3. (lldb) br l
  4. ...

Yohunl Note: In Xcode 7.2.1, it is displayed as

  1. (lldb) rb 'DVTBezelAlertPanel\ initWithIcon:message:'  
  2. Breakpoint 2 : 4 locations.
  3. (lldb) br l
  4. ........

This regular expression breakpoint will add a breakpoint to both initialization methods above, because both methods have the same starting character, and the regular expression will match both of them. Don't forget the \ symbol before the space in the regular expression above, and the single quotes ' to enclose the entire expression so that LLDB knows how to parse it.

Switch to the child Xcode and recompile the child project (ctrl+B). The parent Xcode will hit the initWithIcon:message:parentWindow:duration breakpoint.

If there is no *** breakpoint, check whether the breakpoint is set in the parent Xcode (if you set it in the child Xcode, it will not work), and whether a project is compiled in the child Xcode. Because the corresponding source code file cannot be found, Xcode will break in the assembly code of the method.

Now that you have a breakpoint into a method without the source code, you need a way to print out the arguments passed to that method. It's time to talk about that. . . assembly... :]

[[163630]]

Compilation Tour

When you are dealing with private APIs, you often need to analyze registers instead of using debug symbols like you would when you have the source code. Understanding the behavior of registers in the x86-64 architecture will help you a lot.

Although not a must-read, this is a very good article about x86 Mach-0 assembly. In the third part of this tutorial, you will go through some disassembly code of the method to understand what the method does. But for now, you just need a brief understanding.

The following registers and how they work are worth your attention:

  • $rdi: This register represents the parameter self passed to the method, which is also the first parameter passed.
  • $rsi: represents Selector, which is the second parameter passed to
  • $rdx: The third parameter passed to the function, which is also the first parameter we see in Objective-C (because self and Selector are implicit parameters)
  • $rcx: The fourth parameter passed to the function, which is also the second parameter of Objective-C (because self and Selector are implicit parameters)
  • $r8: The fifth parameter passed to the function. If more parameters need to be passed, $r9 will be used as the stack frame following the sixth parameter.
  • $rax: The return value of the function is stored in this register. For example, when we execute the method -[aClass description], $rax will store the description NSString of the aClass object.

Note: The above description is not absolute. In some binaries, different registers are used to store different types of parameters, for example, doubles use the $xmm register bank. The above is just a quick reference!

Below we use the following method to apply the above theory to practice

  1. @interface aClass : NSObject
  2. - (NSString *)aMethodWithMessage:(NSString *)message;
  3. @end  
  4.    
  5. @implementation aClass
  6.    
  7. - (NSString *)aMethodWithMessage:(NSString *)message {
  8. return [NSString stringWithFormat:@ "Hey the message is: %@" , message];
  9. }
  10.    
  11. @end  

Use the following code to execute it:

  1. aClass *aClassInstance = [[aClass alloc] init];
  2. [aClassInstance aMethodWithMessage:@ "Hello World" ];

After compilation, the call to the method aMethodWithMessage will be replaced by the Runtime layer with a call to objc_msgSend, which is basically similar to the following:

  1. objc_msgSend(aClassInstance, @selector (aMethodWithMessage:), @ "Hello World" )

The call of aMethodWithMessage of aClass will change the contents of some registers:

Before calling method aMethodWithMessage

  • $rdi: stores an instance variable of type aClass
  • $rsi: stores aMethodWithMessage: of SEL type, which is actually a string of type chra * (you can verify this by typing po (SEL)$rsi ​​in lldb)
  • $rdx: contains the message passed to the method, in this case, a string @"Hello World"

When the calling method is finished

  • $rax: stores the return value after the method is executed, which is an NSString in this case. In this particular example, it stores the string @"Hey the message is: Hello World"

x86 Registers

Now that you have a guide to registers, it's time to revisit the initialization method initWithIcon:message:parentWindow:duration: of DVTBezelAlertPanel. Hopefully, your parent Xcode breakpoint is still at this method. Of course, if not, it's okay, rerun the child Xcode and stop at the parent class breakpoint initWithIcon:message:parentWindow:duration: again. Remember, you are looking for clues between the class DVTBezelAlertPanel and the display of the Xcode compilation success/failure prompt box.

When the program breakpoint is at initWithIcon:message:parentWindow:duration, enter the following in the LLDB console

  1. (lldb) re re

This command is the abbreviation of register read, which is used to output the contents of important registers currently visible on your machine.

Use what you have learned about x86 registers to check which register is used to store the message parameter and the fourth parameter of the objc_msgSend method. Is this the content we want to get in the alert box? (In the original text, the following list is hidden. You can click to show it. The author encourages you to find it yourself first. If you can't find it, open the prompt below. Due to the limitations of the markdown editor I use, I can't do this, so I just put it here)

Yes, you should check the register $rcx, and you will see that its content is the content of the message parameter, which is the prompt message displayed in the Xcode compilation prompt box.

Enter the following command to drill down further:

  1. (lldb) po $rcx
  2. Build Failed

Note: Xcode outputs register contents in the default AT&T assembly format, in which the source and target operands are swapped, meaning that the first operand in AT&T syntax is the source operand, and the second is the destination operand, from left to right, which is the opposite of Intel's assembly format. (Translator's note: For more information about AT&T assembly, see http://blog.csdn.net/bigloomy/article/details/6581754)

It looks like this is the register we are looking for!

Try changing the content of $rcx to a new string and see if the content of the alert box changes:

  1. (lldb) po [$rcx class ]
  2. __NSCFConstantString //The output on my xcode7.2.1 shows __NSCFString  
  3.    
  4. (lldb) po id $a = @ "Womp womp!" ;
  5. (lldb) p/x $a
  6. (id) $a = 0x000061800203faa0   //yohunl's note, here is the output of the previous sentence p/x $a. On my Xcode7.2.1, the output is (__NSCFString *) $a = 0x00006080026379c0 @"Womp womp!". The address output on your machine may also be different. The address in the next sentence should be replaced with the address value displayed here on your machine.  
  7. (lldb) re w $rcx 0x000061800203faa0  
  8. (lldb) c

The application will resume running. Pay attention to whether the content of the prompt box showing the success/failure of the compilation has changed to the string we modified. You will see that it has indeed become the new string we set, which also verifies our assumption that DVTBezelAlertPanel is used to display this prompt information.

Code Injection

Now that you have found the class you need, it is time to extend the behavior of DVTBezelAlertPanel through code injection to display the lovely Rayrolling (name) avatar in the compilation prompt box.

We use the method swizzling technology.

You may want to swizzle a lot of methods from many different classes, so the best advice is to create a category of NSObject and provide a convenience method in it to establish all the swizzle logic.

In Xocde, select File\New\File…, then select OS X\Source\Objective-C File. Create a file called MethodSwizzler and make sure it is of the NSObject category.

Open NSObject+MethodSwizzler.m and replace it with the following code:

  1. # import   "NSObject+MethodSwizzler.h"  
  2.    
  3. // 1  
  4. # import    
  5. @implementation NSObject (MethodSwizzler)
  6.    
  7. + ( void )swizzleWithOriginalSelector:(SEL)originalSelector swizzledSelector:(SEL) swizzledSelector isClassMethod:(BOOL)isClassMethod
  8. {
  9. Class cls = [self class ];
  10.    
  11. Method originalMethod;
  12. Method swizzledMethod;
  13.    
  14. // 2  
  15. if (isClassMethod) {
  16. originalMethod = class_getClassMethod(cls, originalSelector);
  17. swizzledMethod = class_getClassMethod(cls, swizzledSelector);
  18. } else {
  19. originalMethod = class_getInstanceMethod(cls, originalSelector);
  20. swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);
  21. }
  22.    
  23. // 3  
  24. if (!originalMethod) {
  25. NSLog(@ "Error: originalMethod is nil, did you spell it incorrectly? %@" , originalMethod);
  26. return ;
  27. }
  28.    
  29. // 4  
  30. method_exchangeImplementations(originalMethod, swizzledMethod);
  31. }
  32. @end  

The key codes are numbered and explained below:

  1. This is the header file required to use method swizzle
  2. isClassMethod indicates whether this method is an instance method or a class method
  3. If you don't use the compiler's method hints, it's easy to misspell the above method. This code is used to check to make sure your spelling is correct.
  4. This is the key function used to implement the exchange method.

Add the declaration of the method swizzleWithOriginalSelector:swizzledSelector:isClassMethod to the header file NSObject+MethodSwizzler.h as follows:

  1. # import    
  2. @interface NSObject (MethodSwizzler)
  3. + ( void )swizzleWithOriginalSelector:(SEL)originalSelector swizzledSelector:(SEL) swizzledSelector isClassMethod:(BOOL)isClassMethod;
  4.    
  5. @end  

Next, we can do the actual swizzling. Create a new category called Rayrolling_DVTBezelAlertPanel, which is also a category of NSObject.

Replace the created NSObject+Rayrolling_DVTBezelAlertPanel.m with the following code

  1. # import    
  2. // 2  
  3. @interface NSObject ()
  4.    
  5. // 3  
  6. - (id)initWithIcon:(id)arg1 message:(id)arg2 parentWindow:(id)arg3 duration:( double )arg4;
  7. @end  
  8.    
  9. // 4  
  10. @implementation NSObject (Rayrolling_DVTBezelAlertPanel)
  11.    
  12. // 5  
  13. + ( void )load
  14. {
  15. static dispatch_once_t onceToken;
  16.    
  17. // 6  
  18. dispatch_once(&onceToken, ^{
  19.    
  20. // 7  
  21. [NSClassFromString(@ "DVTBezelAlertPanel" ) swizzleWithOriginalSelector: @selector (initWithIcon:message:parentWindow:duration:) swizzledSelector: @selector (Rayrolling_initWithIcon:message:parentWindow:duration:) isClassMethod:NO];
  22. });
  23. }
  24.    
  25. // 8  
  26. - (id)Rayrolling_initWithIcon:(id)icon message:(id)message parentWindow:(id)window duration:( double )duration
  27. {
  28. // 9  
  29. NSLog(@ "Swizzle success! %@" , self);
  30.    
  31. // 10  
  32. return [self Rayrolling_initWithIcon:icon message:message parentWindow:window duration:duration];
  33. }
  34.    
  35. @end  

The above code is relatively simple, let's analyze it:

  1. Import header files for swizzling
  2. Forward declare all the methods you plan to use. Although this is not required, it allows the compiler to complete your code with IntelliSense. In addition, this also eliminates the compiler warning that it cannot find the method declaration.
  3. This is the method you actually need to swizzle
  4. Since we don't want to redeclare a private class, the alternative is to declare a category.
  5. This method is where code injection is triggered. You should put all code injections in load. This load is the only method with a "one-to-many relationship", that is, if multiple categories have load methods, then all category load methods can be executed.
  6. Because load may be executed multiple times, use dispatch_once to ensure that it is executed only once.
  7. Swizzle the method declared earlier to your own implementation. Of course, it uses NSClassFromString to dynamically get the class in memory!
  8. This is the method you write to replace the original method. It is recommended that it use a unique command method so that you can immediately know what it does from the name.
  9. Output to make sure the swizzle is successful
  10. Because you have swizzled the original method, when you call the swizzled method (here [self Rayrolling_initWithIcon:icon message:message parentWindow:window duration:duration];), it will call the original method. This means you can add any code you want before or after the original method is executed, and even change the parameters passed to the original method... Of course, you are already doing this here

Congratulations, you have successfully injected code into a private method of a private class! Compile the parent Xcode, then compile and run a project in the child Xcode, and check the console output of the parent Xcode to see if it is successfully wswizzled.

  1. Swizzle success! [DVTBezelAlertPanel: 0x11e42d300] (Due to recognition issues, the angle brackets are replaced with square brackets here)

Next, you can replace the icons on the compilation success/failure prompt box with the Rayrolling avatar. Download the avatar resource Crispy from here, then add it to the project, making sure to select Copy Items if Needed.

Now, navigate to the method Rayrolling_initWithIcon:message:parentWindow:duration and change its code to the following:

  1. - (id)Rayrolling_initWithIcon:(id)arg1 message:(id)arg2 parentWindow:(id)arg3 duration:( double )arg4
  2. {
  3. if (arg1) {
  4. NSBundle *bundle = [NSBundle bundleWithIdentifier:@ "com.raywenderlich.Rayrolling" ];
  5. NSImage *newImage = [bundle imageForResource:@ "IDEAlertBezel_Generic_Rayrolling.pdf" ];
  6. return [self Rayrolling_initWithIcon:newImage message:arg2 parentWindow:arg3 duration:arg4];
  7. }
  8. return [self Rayrolling_initWithIcon:arg1 message:arg2 parentWindow:arg3 duration:arg4];
  9. }

This method first checks if an image parameter is passed to the original method, and then replaces it with our custom image. Note: Here you use [NSBundle bundleWithIdentifier:@"com.raywenderlich.Rayrolling"]; to load the image, because Xcode's MainBundle does not contain our resources.

Recompile the parent Xcode, then compile a project in the child Xcode, you will see

Add a switch and persistence

This plugin is designed for entertainment, so you definitely need a switch to make it work or not. We use NSUserDefaults to persist the variables that make it work or not.

Navigate to Rayrolling.h and add the following code

  1. + (BOOL)isEnabled;

Add in Rayrolling.m file

  1. + (BOOL)isEnabled {
  2. return [[NSUserDefaults standardUserDefaults] boolForKey:@ "com.raywenderlich.Rayrolling.shouldbeEnable" ];
  3. }
  4.    
  5. + ( void )setIsEnabled:(BOOL)shouldBeEnabled {
  6. [[NSUserDefaults standardUserDefaults] setBool:shouldBeEnabled forKey:@ "com.raywenderlich.Rayrolling.shouldbeEnable" ];
  7. }

Now that you have the logic to persist your selections, it's time to hook it up to the GUI.

Go back to Rayrolling.m and modify the code in -(void)doMenuAction to the following:

  1. - ( void )doMenuAction:(NSMenuItem *)menuItem {
  2. [Rayrolling setIsEnabled:![Rayrolling isEnabled]];
  3. menuItem.title = [Rayrolling isEnabled] ? @ "Disable Rayrolling" : @ "Enable Rayrolling" ;
  4. }

This is a bool value used to switch, enable or disable Rayrolling

***, change the initialization code of the menu item in didApplicationFinishLaunchingNotification: to the following:

  1. NSMenuItem *menuItem = [[NSApp mainMenu] itemWithTitle:@ "Edit" ];
  2. if (menuItem) {
  3. [[menuItem submenu] addItem:[NSMenuItem separatorItem]];
  4. NSString *title = [Rayrolling isEnabled] ? @ "Disable Rayrolling" : @ "Enable Rayrolling" ;
  5. NSMenuItem *actionMenuItem = [[NSMenuItem alloc] initWithTitle:title action: @selector (doMenuAction:) keyEquivalent:@ "" ];
  6. [actionMenuItem setTarget:self];
  7. [[menuItem submenu] addItem:actionMenuItem];
  8. }

This menu item will retain the logic you choose to enable or disable, even if Xcode is restarted, because your choice is already persistently stored.

Navigate to the file NSObject+Rayrolling_DVTBezelAlertPanel.m and add a line to the header file

  1. # import   "Rayrolling.h"  

***, open the method Rayrolling_initWithIcon:message:parentWindow:duration:, and set

  1. if (arg1) {

Replace with

  1. if ([Rayrolling isEnabled] && arg1) {

Build and run the program to change the behavior of the plugin.

Now, you have created a plugin that can be used to change the icon and content of the Xcode build success/failure prompt box, and it can also be turned on or off. This is a pretty good day's work, isn't it? ? ?

What to do next?

You can download the complete demo project from here.

You’ve made a lot of progress, but there’s still a lot to do! In the second part of this tutorial, you’ll learn the basics of DTrace and dive into some advanced features of LLDB, such as finding running processes, such as the running Xcode process.

If you want to go further, you still have some work to do before you move on to Tutorial 3. In Tutorial 3, you will see a lot of assembly code. Make sure you have started to understand the relevant x86_64 assembly knowledge before that. Here are 2 articles in Mike Ash's series of articles on assembly analysis, Article 1 and Article 2, which can provide you with relevant help.

<<:  Email inventor Tom Linson dies at 74

>>:  Google Photos now supports Apple's Live Photos feature

Recommend

A complete and effective event planning plan!

Event planning refers to the planning of differen...

How can brands come up with new ideas for Spring Festival marketing?

The Spring Festival marketing war is about to beg...

A guide to live streaming marketing techniques!

Live broadcast script is a key factor affecting t...

Wang Yuquan_QianShao Technology Training Camp Baidu Cloud Download

Wang Yuquan · Qianshao Technology Training Camp R...

China Automobile Dealers Association: July 2023 Automobile Market Pulse Report

In July 2023, the retail sales of passenger cars ...

How to do competitive product analysis report as a workplace rookie!

Recently, a netizen complained to Clippings that ...

Yuting 2021 clothing live broadcast operation implementation course

The course comes from Yuting’s 2021 clothing live...

How to process charges for 400 calls? How much are 400 calls charged?

Nowadays, the application of 400 telephones in do...

Galaxy S4 exploded while charging at night: Woman almost disfigured

If you think that cell phone explosions only happe...