Explorations into the Xcode Source Editor Extensions underbelly: Part 1

Xcode source extensions are wildly exciting, surprisingly limited, and infuriatingly frustrating to work with. So I thought I’d share some experiences so you don’t have to suffer through some of my issues.

There are bunches of posts around the web on how to create extensions and I really don’t want to dupe their effort. In a nutshell, create a new Cocoa app. Then create a new macOS > Application Extension > Xcode Source Editor Extension target in that app. You’re ready to code.

If you watched the WWDC video, you’ll have seen the whole process of “run the target in Xcode and a new version of Xcode pops up with a darkened label and then you can test your extension”. I’m here to tell you that this approach is a huge steaming pile of crap doesn’t work as well as I’d hoped. What you really want to do is this:

In AppDelegate.swift of your extension/app project, add the following method. It lets you launch the app, install the latest extension and get the hell out of Dodge without using the “Xcode with the darkened label”.

func applicationDidFinishLaunching(_ aNotification: Notification) {
    let alert = NSAlert(); alert.messageText = "Extension installed! Click OK to quit."
    alert.beginSheetModal(for: window) { _ in
        exit(0)
    }
}

In your main extension Swift file (the class defined for XCSourceEditorExtensionPrincipalClass), add this. It lets you watch the OS X console and ensure that your extensions loaded the way you expected them to. Notice that I use NSLog and not print. Always use NSLog, so your output and debug info goes to the system console.

func extensionDidFinishLaunching() {
    NSLog("NAMEOFMYTARGET extensions did finish launching")
}

Set up your Info.plist in a separate editor pane or even a separate window. If you intend to create a multi-part extension (and most people will), you’ll want to build that extension incrementally and slowly add items to your XCSourceEditorCommandDefinitions dictionary. A typical entry looks like this:

Screen Shot 2016-07-21 at 10.20.38 AM

Also, don’t do what I did here. I used “sadun.ExtensionTestbed” because I know this is a throwaway. Assume you may write multiple extension groups and reverse namespace them properly, e.g. “org.sadun.ExtensionName.CommandName”.

Each time you want to test do this:

  1. Build your target.
  2. Open Products, right-click the .app file and show in Finder.
  3. Quit Xcode and run the app.
  4. Launch Xcode.
  5. Open any project that isn’t a playground because Xcode hates me and doesn’t want to properly load extensions for playgrounds. Always start with a non-playground project and *then* open a playground if you want to work with playgrounds.
  6. If the extension is greyed out (and if you follow these instructions they usually aren’t because this is the sequence for El Capitan that appears to be super reliable), check the OS X console. Look for assertion failures, XPC connection failures, and invalidated connections. If you see these, quit Xcode and relaunch. What you want to see there is the “did finish launching” log line.

I know this workflow is a super pain, especially given Apple’s promotion about in-Xcode testing without quitting and relaunching (let alone running the standalone app), but it’s transformed my source editor extension duty cycle from impossible to annoying. A huge win.

Notes:

  • Don’t “clean” your Xcode extension project while running the extension. Ooops.
  • Once the extension crashes, you need to re-run the installer app.
  • I can’t seem to grab a single cursor position, there has to be an actual selection, so my extensions for adding and removing “/// ” for doc comment markup mean you have to select a line, not just move the cursor there. Or maybe there’s a bug that’s screwing this up. I haven’t quite figured this out yet.
  • A lot of my code is super redundant: operate on all lines, operate on all selection lines, operate on selected text. I really need to re-architect a common subclassable system because the actual logic for most plugins is only a few lines or a call-out to another class.
  • Any extension that works on all lines rather than individual selections seems to be more stable in my experience.
  • My experiments trying to create interactive UI elements with extensions hasn’t been going very well, they run in the original application and not in Xcode, the window is behind xcode, they can’t be interacted with while Xcode is running, and you inevitably have to force quit something.

Screen Shot 2016-07-21 at 10.39.39 AM

Here are some things I’ve been messing with. Let me know how your extensions are going for you.

9 Comments

  • Get same results as with WWDC demo, the extension doesn’t show up in the Editor menu — not even greyed out. Why would the fact the extension shows up as installed in the surrounding app started from Finder imply that it would also install in Xcode next time you start it?
    Btw in AppDelegate you need to define “window” — I got it to work with
    let window = NSApplication.shared().windows.first!

    • With beta 5, clean the build, quit xcode, removed derived data, open xcode, build, restart xcode, and check the system console to see that the plugin has been installed, and that your did load method has been called.

  • Thanks for the notes section, this information should really go to Apple documentation. Some more tips about the grayed extension problem:
    * run sudo /usr/libexec/xpccachectl and reboot might help
    * kill the com.apple.dt.Xcode.AttachToXPCService process might help as well

  • I have to disagree with a lot of what is posted on here.

    First, NEVER RUN XCODE BETA AND EXPECT IT TO WORK. As much as I like Apple, like most businesses, beta software is not for the weak hearted. In my case, I rarely touch beta software because a) something I like and use regularly may be removed and b) something I like may work horribly, horribly wrong. When they say NOT FOR PRODUCTION WORK they pretty much mean it

    Second, as much as the writer believes his is the best solution, I actually PREFER Apple’s solution. It really beats having to open and close Xcode every freaking time (and really, it does work). In other words ”doesn’t work as well as I’d hoped” works better than I expected.

    Third, I discovered that when I followed the directions, yes the directions, yes every single stupid sounding direction, it actually worked like Apple said it would.

    Fourth, I have NEVER RUN sudo /usr/libexec/xpccachectl and you shouldn’t have to either.

    Fifth, DO KILL com.apple.dt.Xcode.AttachToXPCService – that goes a long way to solving the greyed out problem.

    Sixth, You actually have to be editing SOMETHING for your extension to show up. Yeah, I know, I think some people don’t think you have to be editing anything.

    Seventh, IF YOU NEED A UI, START UP THE CONTAINER APP – there are a lot of games you can play here, but the main one to remember is that the container app is only loosely connected to the extension. How do you start up the container app? Read about other extensions, they tell you how and it does work (don’t try to use NSWorkspace – you are just asking for trouble).

    Eighth, UI’s in extensions WILL NOT WORK… EVER This appears to be by design. So if you can’t use the container app for your gui, then you are pretty much left high and dry.

    Ninth, seriously important: ALWAYS KEEP A FINDER WINDOW TO YOUR EXTENSIONS because if you restart Xcode and your extension is still crashing, you’ll have to delete the extension before you can rebuild it. Make sense? Clear as mud. But it will make sense when you discover Xcode is crashing on you.

    Tenth: DO NOT START YOUR CONTAINER APP USING NSWorkspace Why? Because you’ll get things stuck where you have a nice version of your extension THAT YOU CAN’T KILL! If you do kill it, it will regenerate. I spent a day before I realized you had to start up the container app using the following documentation: https://developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html – yes, it looks scary, but it is pretty trivial.

    Okay. That should make y’alls life easier.

    • Forgot one other: DO NOT MAKE INFO.PLIST part of any project. If you do it trashes things. No, it doesn’t make sense. No, I don’t know why it doesn’t make sense. Yes, I redid the freaking project four times before I realized what I was doing that made a bunch of sad.

      • Let me explain the Tenth one again. The link is too damn scary. First, in your container apps info.plist you will have something like the following:


        CFBundleURLTypes

        CFBundleURLName
        com.yourcompanyname.yourproductname
        CFBundleURLSchemes

        urlthatstartsupcontainerapp

        Okay, that is the hard part.

        NOW in your extension code you will have something like the following to start up the container app:


        if (invocation.commandIdentifier == “com.yourcompanyname.yourproductname.extensionname.commandId") {
        let customurl = NSURL.init(string: "urlthatstartsupcontainerapp://")
        NSWorkspace.shared().open(customurl as! URL)
        }

        Yes, it is that easy. And best of all, THIS IS HOW APPLE WANTS YOU TO DO IT! A win/win.

        • Okay, that didn’t work out so well. Meh. Google. Better yet, I answer it on Stack Overflow:

          http://stackoverflow.com/questions/39413889/xcode-8-extension-executing-nstask

          • When I say “Don’t start with NSWorkspace” what I mean is don’t use the following:

            NSWorkspace.sharedWorkspace().launchApplication()

            It will cause you more pile of hurt than you can imagine (and I can imagine quite a lot). It will make you sad. It will make you angry. Worse of all it will unleash the fury that is OS X turning it from the docile workmate to the Beast mentioned in all dark lore.

            That and it makes your app unkillable (not a zombie… WORSE than a zombie).

  • Did anyone try / succeed using Alamofire (via CocoaPods) in the Xcode extension?
    Everything works with an empty podfile.
    After I add the following:
    pod ‘Alamofire’, ‘~> 4.0’
    the extension gets greyed out and I get the following error:

    Hub connection error Error Domain=NSCocoaErrorDomain Code=4097 "connection to service named com.Swiftify.Xcode.Extension" UserInfo={NSDebugDescription=connection to service named com.Swiftify.Xcode.Extension}