Michele Titolo | Blog | Speaking

The Project File Part 2: Schemes and Targets

Welcome to part 2 of my Project file series! Before reading this post, I suggest familiarizing yourself with part 1.

Schemes and targets are the building blocks of apps in Xcode. Every app has at least one scheme and one target. Just like everything else in Xcode’s build system, these have representations on disk that are used to keep track of the settings needed to build.

Terminology

Before digging into some of the details on how schemes and targets work, there are a number of terms that need to be defined.

  • Product: Apple’s term for the output of a compilation. There are set pre-defined target types: application, test, static library, and framework to name a few.
  • NativeTarget: a set of instructions for building a product. These usually go un-qualified because they are much more frequent than the other kinds of target.
  • Aggregate Target: a set of instructions for building multiple targets. This is flexible and can be used to do pieces of work that don’t output a product. Example: mogenerator is run with an aggregate target but outputs no built product.
  • Legacy Target: a set of instructions that calls a command-line system for building. If a project needs a dependency that needs to run make to build, this is the kind of target to use.
  • Scheme: a set of instructions for building one or more targets. This is the top-most instruction set that Xcode uses and only one can be active at a time. Schemes also have the ability to be private or public. By default, every Xcode project is created with 1 target and 1 public scheme, which is the minimum requirement for building in Xcode.

Targets

All target information is saved in the .pbxproj with the type PBXNativeTarget or PBXAggregateTarget. Here’s what a simple target looks like:

D9B6428F176A2E17003D8169 /* Catstagrame */ = {
			isa = PBXNativeTarget;
			buildConfigurationList = D9B642C5176A2E17003D8169 /* Build configuration list for PBXNativeTarget "Catstagrame" */;
			buildPhases = (
				5886B8DBD5B540EDADB3C9A3 /* Check Pods Manifest.lock */,
				D9B6428C176A2E17003D8169 /* Sources */,
				D9B6428D176A2E17003D8169 /* Frameworks */,
				D9B6428E176A2E17003D8169 /* Resources */,
				291A5E171FA24E5AA4E0C13F /* Copy Pods Resources */,
				D9847C20177F8CA400C3F95B /* Copy Key */,
			);
			buildRules = (
			);
			dependencies = (
			);
			name = Catstagrame;
			productName = Catstagrame;
			productReference = D9B64290176A2E17003D8169 /* Catstagrame.app */;
			productType = "com.apple.product-type.application";
		};

The keys within this object should look fairly familiar–they correspond with information we see within the project view in Xcode. $(TARGET_NAME) is retrieved from the name field. The buildConfigurationList populates the Build Settings tab, buildRules populates the Build Rules tab, and buildPhases and dependencies are shown in the Build Phases tab. The productReference is the UID used to refer to the built product, not the target itself even though they can appear to be very similar.

Product Types

Even though we only have 3 kinds of targets, only PBXNativeTarget has multiple product types. When adding a Legacy or Aggregate product, productReference and productType are missing from the entity. The most common types of products are fairly self-explanatory:

com.apple.product-type.framework  
com.apple.product-type.library.static
com.apple.product-type.application.watchapp2
com.apple.product-type.watchkit2-extension
com.apple.product-type.bundle.unit-test

tvOS

One really interesting tidbit about tvOS targets is that they are listed exactly the same as iOS targets. The main tvOS application is a com.apple.product-type.application. All of the entries are the same as an iOS app, with a few different build settings set in the XCBuildConfiguration section:

SDKROOT = appletvos;
TARGETED_DEVICE_FAMILY = 3;
TVOS_DEPLOYMENT_TARGET = 9.0;

Schemes

Schemes are saved in a separate xml file, within xcuserdata or xcshareddata. Most repositories ignore xcuserdata since Xcode also put things in there like breakpoint settings that shouldn’t be shared between developers. These .xcscheme files are standard xml, not plists like many other files used by Xcode internals. Like the .pbxproj this is not a full representation of scheme data–there is an internal-to-Xcode set of flags that it overrides.

At a quick glance, this file appears to be fairly generic. In reality it is tightly coupled to the project file and targets therein. Every target that is explicitly included in the scheme is referenced within the .xcscheme by UID. Because of this, these cannot simply be copy-pasted between different Xcode projects and “just work.”

This file is auto-generated by Xcode, and just like the project file will be overwritten on a regular basis. Edits to this file should only be made while the corresponding Xcode project is not open.

The root of this file includes information for performing all of the different actions within the project: build, test, launch, profile, analyze, and archive. Unlike the project file, there are no key/value pairs that reference a particular entity so as you read through it, you will notice many references that look the same. The name of the scheme is the name of the file.

<?xml version="1.0" encoding="UTF-8"?>
<Scheme
   LastUpgradeVersion = "0500"
   version = "1.7">
   <BuildAction>
   	...
   </BuildAction>
   <TestAction>
   ...
   </TestAction>
   <LaunchAction>
   ...
   </LaunchAction>
   <ProfileAction>
   ...
   </ProfileAction>
   <AnalyzeAction>
   ...
   </AnalyzeAction>
   <ArchiveAction>
   ...
   </ArchiveAction>
</Scheme>

All of these Actions are named the same as they are used in Xcode except for LaunchAction which is actually Run.

References

Dependencies are referenced within each of the sections within a BuildableReference entry. The presence of one of these means that action is dependent upon Build happening first, which is the case for all other Actions. Unlike in the project file, the same BuildableReference is included several times to refer to the same target. There is also a reference to the project file that contains the target. The UID is referenced by the BlueprintIdentifier key. The project file the target comes from is also included.

<BuildableReference
   BuildableIdentifier = "primary"
   BlueprintIdentifier = "3AA4FE2703EA4DC3A89C1CCC"
   BuildableName = "libPods.a"
   BlueprintName = "Pods"
   ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>

Actions

The Build Action is the only action that refers to multiple targets. As such, it includes a list of BuildableReferences within BuildActionEntries that refer to the explicit dependencies specified by the contained targets. These come from Target Dependencies or Link Binary With Library where the library is also a target within the workspace. All other Actions nest the BuildableReference inside whatever needs to reference the product.

<BuildableProductRunnable
   runnableDebuggingMode = "0">
   <BuildableReference
      BuildableIdentifier = "primary"
      BlueprintIdentifier = "D9B6428F176A2E17003D8169"
      BuildableName = "Catstagrame.app"
      BlueprintName = "Catstagrame"
      ReferencedContainer = "container:Catstagrame.xcodeproj">
   </BuildableReference>
</BuildableProductRunnable>

Another difference between schemes and the project file is that each action has a different set of configuration options. All actions have pre- and post- actions that can be performed before the main action, but that is the only consistency between action types. Analyze and Archive have the fewest options whereas Launch has the most. This mirrors the Edit Scheme dialogue in Xcode.

<AnalyzeAction
   buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
   buildConfiguration = "Release"
   revealArchiveInOrganizer = "YES">
</ArchiveAction>

Options

All of the actions have options associated with them. These are included in the opening tag of the Action as attributes. Not all of the options that show in the Xcode Edit Scheme menu are listed, and some only appear when they are turned on.

<LaunchAction
   buildConfiguration = "Debug"
   selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
   selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
   launchStyle = "0"
   useCustomWorkingDirectory = "NO"
   ignoresPersistentStateOnLaunch = "NO"
   debugDocumentVersioning = "YES"
   debugServiceExtension = "internal"
   allowLocationSimulation = "YES">

For example, turning on Localization Debugging adds showNonLocalizedStrings = "YES" but when turned off the attribute is simply missing. On the other hand, allowLocationSimulation is present regardless of whether it is enabled or not.

Run options screenshot

Pre- and Post- Actions

There are only 2 kinds of pre- and post- actions available: send email and run script.

The Send Email action will open up Mail.app on your computer and send an email from your default email account. There’s no way to add any variables or conditionals. Oh and attachLogToEmail doesn’t appear to do anything.

<ExecutionAction
   ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.SendEmailAction">
   <ActionContent
      title = "Send Email"
      emailRecipient = "thedoctor@tardis.space"
      emailSubject = "Sup"
      emailBody = "Bowties are cool"
      attachLogToEmail = "NO">
   </ActionContent>
</ExecutionAction>

The Script action is much more useful. It can run in any shell in any language you have installed. However, just like with the PBXShellScriptBuildPhase the script will be contained within the scheme file. Thankfully it will automatically escape quotes to &quot;. By default, this is run from within the build directory. So if your goal is to modify contents of the workspace, you can cd ${SRCROOT}.

So for this script:

cd ${SRCROOT}

pod install #>> /tmp/output.txt

It gets saved as:

<ExecutionAction
   ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
   <ActionContent
      title = "Run Script"
      scriptText = "cd ${SRCROOT}&#10;&#10;pod install #&gt;&gt; /tmp/output.txt"
      shellToInvoke = "/usr/bin/env bash">
      <EnvironmentBuildable>
         <BuildableReference
            BuildableIdentifier = "primary"
            BlueprintIdentifier = "D9B6428F176A2E17003D8169"
            BuildableName = "Catstagrame.app"
            BlueprintName = "Catstagrame"
            ReferencedContainer = "container:Catstagrame.xcodeproj">
         </BuildableReference>
      </EnvironmentBuildable>
   </ActionContent>
</ExecutionAction>

All of the flags passed into the build are available for use within these actions, including things like ${PRODUCT_NAME}. Note: Any sort of echo or print will not show up in the build logs, console etc. Send output to a file in order to see what’s going on.

One other nifty fact: changing the titles does also update them in Xcode. So you can send a REALLY COOL email for instance.
title is Send a REALLY COOL email image

In Conclusion

Schemes and targets are the building blocks of our applications, and it turns out they are fairly complicated. Thankfully the naming conventions used in the project and scheme files is fairly explicit and easily tied into Xcode’s views of the same information. Configuring these at a deer level than the defaults that Xcode provide can simplify many processes and automate common tasks. Xcode is a powerful tool. But we all know: with great power, comes great responsibility, so edit wisely.

© 2023 Michele Titolo