In this post I describe the technical setup of a small language experiment I developed to aid myself working on custom Xcode instruments.

The need for a more flexible coding inside Xcode

Last month, I prototyped a Swift IDE and became very interested in enhancing the experience of building with Swift. (Since Apple doesn’t hire remotely, I wonder what other companies are working in that area?)

A little later, I came back to updating my custom Xcode Instrument called Timelane to support debugging new code based on the new Swift modern concurrency APIs:

Unlike previous Timelane versions that supported Combine and RxSwift which are based on very minimal protocols, the new instrument needed to support a ton of different APIs like TaskGroup(), TaskGroup.addTask() Task(), Task.detached(), and more.

Simple custom Xcode instruments are defined in a single XML file, and once I hit the 1,000 line mark developing came to a grinding halt. I tried some creative approaches like including ASCII art in Xcode’s jump menu:

But ultimately thousands of lines of flat XML simply aren’t manageable during active development. So, I took a deep breath and made a detour to create a simple language extension that plugs right into Xcode…

Pre-actions and post-actions in Xcode

I do have some minimal experince with React and Vue.js and I’m a fan of the idea of transpiling: designing cool/powerful APIs that “unwrap” into less elegant/usable code in the target language! It’s a win-win.

So, I looked into possible ways to integrate something quick with Xcode. You can add scripts to automatically run on events like build, run, test, etc. and I found that I needed exactly two things that Xcode offers out of the box:

  1. A “build pre-action” — to parse & transpile my custom code into the target Xcode XML format.
  2. A “run post-action” — to clean any generated, transpiled code from the source.

This way, while editing code, I can work with a clean, minimal version of the code using my own syntax.

Once a batch of changes are completed, I build and that generates the output XML putting Xcode to work on validating it and displaying errors and warnings.

Once the errors are solved, I can run the project to test the custom instrument in Instruments while consulting the full, generated code. When I quit the custom instrument I can work again with the minimal, custom code.

It’s a clean, straigh-forward process that helped me easily work on the instrument that is currently over 2,000 lines of XML when “unwrapped”.

XML Includes

First of all, I wanted to break the monolith by extracting various pieces into a clean hierarchy of folders and files. I added an include command that includes an XML file during build and then removes the included code during cleanup:

Breaking the 1,000s of lines of flat XML into scoped, smaller files of up to a couple of hundred lines made the codebase easy to navigate.

I chose to use commands wrapped in XML comments to make my custom code valid XML — this way the custom syntax doesn’t trigger errors in Xcode’s code editor.

The build phase finds tags looking like this:

1
<!-- include "tasks-instrument.xml" -->

And inserts the target file’s contents after the original tag’s location like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!-- include "tasks-instrument.xml" -->

	<!-- included "tasks-instrument.xml" "982F43E4-59E0-45A4-A199-18C3DED2730E" -->

	  <!-- MARK: - TASKS INSTRUMENT -->
	  <!--
	  	The tasks instrument.
	  -->
	  
	  <augmentation>
	  	<id>${instrumentID}.byname</id>
	  	<title>Show Tasks by Name</title>
	  	<purpose>A view that provides insight into individual tasks on the timeline.</purpose>

    ...

	<!-- / included "tasks-instrument.xml" "982F43E4-59E0-45A4-A199-18C3DED2730E" -->
	  

Once I’m finished debugging the instrument, the cleanup phase removes the code between the opening and closing “included” tags. (I’m not saying this process is bullet-proof but I have to say it works solidly for my use case.)

When the includes worked to my needs I could design a few levels deep structure that made each component in the package easy to work with and reason about. Below is part of the actual hierarchy:

Instrument package

  +- tasks-schema.xml
    +- schema-metadata.xml
    +- region-tracking-partial.xml
    +- [Custom XML]
    -- region-tracking-open-interval-partial.xml

  +- region-tracking-open-interval-partial.xml
    ...  
    
  +- task-groups-schema.xml
    +- schema-metadata.xml
    +- region-tracking-partial.xml
    -- region-tracking-open-interval-partial.xml

  ...

You might’ve already noticed it, but take another look above — both schemas include a) “schema-metadata.xml”, b) “region-tracking-partial.xml” and c) “region-tracking-open-interval-partial.xml”. Some of these includes are reused verbatim and this decreases the total amount of code to take care of drastically.

Other includes weren’t exactly the same but the variation was minimal so (again, inspired by Vue.js) I thought I’d add data binding to the components (in my case the components are the includes).

Data bindings

Since the includes work hierarchically, it made sense to also have hierarchical data bindings, e.g. much like the environment in SwiftUI children inherit the definitions of their parents and merge-in their own.

First, I added a JSON file that contains any global constants that aren’t bound to a specific component:

And extended the powerups to support ${...} syntax in the templates like so:

1
2
<subsystem>"${instrumentID}.time"</subsystem>
<category>"DynamicStackTracing"</category>

This took care of easily fine-tuning data like the instrument identifeir, identifiers of the subsystems and various tables ids, as well as the instrument version, and more.

More interestingly, I added syntax to the include tags to bind data — that really made them feel like proper components. For example:

1
2
3
4
<os-signpost-point-schema>
	<!-- include "schema-metadata.xml" id="async-taskgroupresults-schema" schemaTitle="Task Group Results" entity="taskgroups" -->
	<!-- include "value-tracking-partial.xml" entity="TaskGroup" -->
</os-signpost-point-schema>

All params that aren’t recognized tag properties are bound as data to the template and its children.

This enabled dynamically building identifiers, names and even comments in the generated code:

1
2
3
4
5
<!-- The data table storing ${entity} data -->
<create-table>
	<id>${entity}-table</id>
	<schema-ref>async-${entity}-schema</schema-ref>
</create-table>

This made the complete package code very DRY pushing the 2,000+ lines of XML to less than 1,000 split across 20-ish files.

Conditionals

Before wrapping up, I noticed that I can reuse even more code but some instruments needed it and some didn’t. I had to add conditionals (and by god at this point I considered just switching to XSL but I was having too much fun with this.)

To implement conditional includes I added a where tag property like this:

1
<!-- include "tasks-augmentation.xml" where "${entity}==tasks" -->

This would include the template and bind the data, etc. only if the where condition evaluates to true.

The whole powerups experiment took just some hours to complete but it unblocked me to move forward and complete the instrument itself, which otherwise would probably have been impossible.

Conclusion

Creating custom instruments is such a niche job that my powerups experiment as-is isn’t useful beside my concrete use-case (in all fairness, the XML API should’ve been a Swift DSL in first place if developers were supposed to really use it):

Regardless, I hope that this write-up has been inspiring — your day-to-day workflow in Xcode could be extended in a bunch of ways and writing your own language extensions or a full-blown transpiler is possible even if not very easy to do.

To be honest, I wish I’d looked into building more language extensions in the past instead of spending tons of time on mundain code tasks.

Till next time!

Where to go from here?

If you’d like to support me, get my book on Swift Concurrency:

» swiftconcurrencybook.com «

Interested in discussing the new async/await Swift syntax and concurrency? Hit me up on twitter at https://twitter.com/icanzilb.