How to convert your Xcode plugins to Xcode extensions

Khoa Pham
Khoa Pham
Dec 11, 2018 · 19 min read
Image for post
Image for post
Source: Imgur

Table of Contents

My first Xcode plugin: XcodeWay

I choose a lazy person to do a hard job. Because a lazy person will find an easy way to do it

Image for post
Image for post
XcodeWay works by creating a menu under Editor with lots of options to navigate to other places right from Xcode. It looks simple but there was some hard work required.

What are Xcode plugins?

Image for post
Image for post
class Xmas: NSObject {  var bundle: NSBundle  init(bundle: NSBundle) {
self.bundle = bundle
super.init()
}
}
<key>NSPrincipalClass</key>
<string>Xmas</string>
<key>XCPluginHasUI</key>
<false/>

What Xcode plugins can do

Image for post
Image for post

Xvim

Image for post
Image for post

SCXcodeMiniMap

Image for post
Image for post

FuzzyAutocompletePlugin

Image for post
Image for post

KSImageNamed-Xcode

Image for post
Image for post

ColorSense-for-Xcode

Image for post
Image for post

LinkedConsole

Image for post
Image for post

The hard work behind Xcode plugins

Swizzling DVTBezelAlertPanel framework in Xmas

Image for post
Image for post
class func swizzleMethods() {
guard let originalClass = NSClassFromString("DVTBezelAlertPanel") as? NSObject.Type else {
return
}
do {
try originalClass.jr_swizzleMethod("initWithIcon:message:parentWindow:duration:",
withMethod: "xmas_initWithIcon:message:parentWindow:duration:")
}
catch {
Swift.print("Swizzling failed")
}
}

Interacting with DVTSourceTextView in XcodeColorSense

Image for post
Image for post
func listenNotification() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(handleSelectionChange(_:)), name: NSTextViewDidChangeSelectionNotification, object: nil)
}
func handleSelectionChange(note: NSNotification) {
guard let DVTSourceTextView = NSClassFromString("DVTSourceTextView") as? NSObject.Type,
object = note.object where object.isKindOfClass(DVTSourceTextView.self),
let textView = object as? NSTextView
else { return }
self.textView = textView
}
public struct HexMatcher: Matcher {func check(line: String, selectedText: String) -> (color: NSColor, range: NSRange)? {
let pattern1 = "\"#?[A-Fa-f0-9]{6}\""
let pattern2 = "0x[A-Fa-f0-9]{6}"
let ranges = [pattern1, pattern2].flatMap {
return Regex.check(line, pattern: $0)
}
guard let range = ranges.first
else { return nil }
let text = (line as NSString).substringWithRange(range).replace("0x", with: "").replace("\"", with: "")
let color = NSColor.hex(text)
return (color: color, range: range)
}
}

Using NSTask and IDEWorkspaceWindowController in XcodeWay

Image for post
Image for post
@objc protocol Navigator: NSObjectProtocol {
func navigate()
var title: String { get }
}
self.IDEWorkspaceWindowControllerClass = objc_getClass("IDEWorkspaceWindowController");NSArray *workspaceWindowControllers = [self.IDEWorkspaceWindowControllerClass valueForKey:@"workspaceWindowControllers"];id workSpace = nil;for (id controller in workspaceWindowControllers) {
if ([[controller valueForKey:@"window"] isEqual:[NSApp keyWindow]]) {
workSpace = [controller valueForKey:@"_workspace"];
}
}
NSString * path = [[workSpace valueForKey:@"representingFilePath"] valueForKey:@"_pathString"];
~/Library/Developer/CoreSimulator/Devices/1A2FF360-B0A6-8127-95F3-68A6AB0BCC78/data/Container/Data/Application/

Security and freedom

Source Editor Extension

protocol XCSourceEditorCommand {  func perform(with invocation: XCSourceEditorCommandInvocation, 
completionHandler: @escaping (Error?) -> Void)
}
Image for post
Image for post

Unless you resign Xcode

Image for post
Image for post
codesign -f -s MySelfSignedCertificate /Applications/Xcode.app

Moving to Xcode extension

Color literal in XcodeColorSense2

Image for post
Image for post
func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {
guard let selection = invocation.buffer.selections.firstObject as? XCSourceTextRange else {
completionHandler(nil)
return
}
let lineNumber = selection.start.lineguard lineNumber < invocation.buffer.lines.count,
let line = invocation.buffer.lines[lineNumber] as? String else {
completionHandler(nil)
return
}
guard let hex = findHex(string: line) else {
completionHandler(nil)
return
}
let newLine = process(line: line, hex: hex)invocation.buffer.lines.replaceObject(at: lineNumber, with: newLine)completionHandler(nil)
}
}
Image for post
Image for post

How to debug Xcode extensions

Image for post
Image for post

How to install Xcode extensions

Image for post

AppleScript in XcodeWay

Image for post
Image for post
tell application "Spotify"
set trackId to id of current track as string
set trackName to name of current track as string
set artworkUrl to artwork url of current track as string
set artistName to artist of current track as string
set albumName to album of current track as string
return trackId & "---" & trackName & "---" & artworkUrl & "---" & artistName & "---" & albumName
end tell
Image for post
Image for post
on myOpenFolder(myPath)
tell application "Finder"
activate
open myPath as POSIX file
end tell
end myOpenFolder
func eventDescriptior(functionName: String) -> NSAppleEventDescriptor {
var psn = ProcessSerialNumber(highLongOfPSN: 0, lowLongOfPSN: UInt32(kCurrentProcess))
let target = NSAppleEventDescriptor(
descriptorType: typeProcessSerialNumber,
bytes: &psn,
length: MemoryLayout<ProcessSerialNumber>.size
)
let event = NSAppleEventDescriptor(
eventClass: UInt32(kASAppleScriptSuite),
eventID: UInt32(kASSubroutineEvent),
targetDescriptor: target,
returnID: Int16(kAutoGenerateReturnID),
transactionID: Int32(kAnyTransactionID)
)
let function = NSAppleEventDescriptor(string: functionName)
event.setParam(function, forKeyword: AEKeyword(keyASSubroutineName))
return event
}
on myGitHubURL()
set myPath to myProjectPath()
set myConsoleOutput to (do shell script "cd " & quoted form of myPath & "; git remote -v")
set myRemote to myGetRemote(myConsoleOutput)
set myUrl to (do shell script "cd " & quoted form of myPath & "; git config --get remote." & quoted form of myRemote & ".url")
set myUrlWithOutDotGit to myRemoveSubString(myUrl, ".git")
end myGitHubURL
use scripting additions
use framework "Foundation"
property NSString : a reference to current application's NSString
on myRemoveLastPath(myPath)
set myString to NSString's stringWithString:myPath
set removedLastPathString to myString's stringByDeletingLastPathComponent
removedLastPathString as text
end myRemoveLastPath
on myOpenDocument()
set command1 to "cd ~/Library/Developer/CoreSimulator/Devices/;"
set command2 to "cd `ls -t | head -n 1`/data/Containers/Data/Application;"
set command3 to "cd `ls -t | head -n 1`/Documents;"
set command4 to "open ."
do shell script command1 & command2 & command3 & command4
end myOpenDocument

App Sandbox

Image for post
Image for post
Image for post
Image for post
Image for post
Image for post

How to install custom scripts

Image for post
Image for post
#!/bin/bashset -euo pipefailDOWNLOAD_URL=https://raw.githubusercontent.com/onmyway133/XcodeWay/master/XcodeWayExtensions/Script/XcodeWayScript.scpt
SCRIPT_DIR="${HOME}/Library/Application Scripts/com.fantageek.XcodeWayApp.XcodeWayExtensions"
mkdir -p "${SCRIPT_DIR}"
curl $DOWNLOAD_URL -o "${SCRIPT_DIR}/XcodeWayScript.scpt"

More security in macOS Mojave

unable to load info.plist exceptions (egpu overrides)

Image for post
Image for post
Source: https://www.felix-schwarz.org/blog/2018/08/new-apple-event-apis-in-macos-mojave
<key>NSAppleEventsUsageDescription</key>
<string>Use AppleScript to open folders</string>

Where to go from here


Fantageek

Simple apps that make sense

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store