Teabyte

App development for Apple platforms and Swift

DocC for Multi-Platform Documentation

Writing documentation is an essential part of developing and distributing software. It is often the first point of contact for developers interacting with your software. Therefore, it's crucial to deliver complete and reliable documentation. With DocC, we have a powerful tool for distributing documentation. Recently, the DocC SPM plugin gained support for generating documentation for multiple targets, making the tool even more versatile. Unfortunately, there is still one crucial piece missing: the ability to generate documentation for multi-platform packages. At the time of writing, this is not possible with the DocC plugin. However, I recently needed to generate multi-platform documentation for an SPM package, which meant I couldn't use the swift-docc-plugin.

In this brief post, I'll explain how you can manually create multi-platform documentation - so let's dive in.

The basics

Since we cannot use the swift-docc-plugin, we need to use the underlying DocC tool directly. DocC is bundled with Xcode, and you can locate it using the following command:

xcrun --find docc

Generating a documentation for a source code project with docc is a 3-step process:

  1. Building the project and emitting its symbol graph
  2. Converting the .docc bundle to a DocC archive using docc.
  3. Processing the archive to generate HTML files that can be hosted, for example on GitHub Pages.

In addition to these steps, you also need a documentation bundle—a directory with a .docc extension that contains all your additional documentation pages, such as tutorials or standalone pages. In the next section, we'll review the minimum requirements your documentation bundle must meet.

The Bundle

The first step in creating DocC documentation is to set up the DocC bundle. Once that's in place, you can define the documentation's root page. In this example, we'll name it "Overview", though you're free to choose a title that best suits your project. To establish this page as the entry point of your documentation, we'll use the @TechnologyRoot keyword.

Documentation.docc/Overview.md
# Framework Documentation
 
@Metadata {
    @TechnologyRoot
}

To ensure DocC accurately presents the source frameworks for each target in your library in its sidebar, reference them directly on the Overview page. This can be achieved by compiling a concise list of all the frameworks included in your project.

Documentation.docc/Overview.md
# Framework Documentation
 
@Metadata {
    @TechnologyRoot
}
 
## Overview
 
A small description of the framework.
 
## Topics
 
### Frameworks
 
- ``/YourTarget``
- ``/YourSecondTarget``
...
 

The final step in setting up multi-platform documentation is to create an Info.plist file at the root of your documentation bundle. Much like in iOS applications, this file defines configuration options for your documentation. In particular, you'll need to specify the minimum deployment targets for each platform you intend to support.

Documentation.docc/Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
	<dict>
		<key>CDAppleDefaultAvailability</key>
		<dict>
			<key>YourTarget</key>
			<array>
				<dict>
					<key>name</key>
					<string>watchOS</string>
					<key>version</key>
					<string>10.0</string>
				</dict>
				<dict>
					<key>name</key>
					<string>iOS</string>
					<key>version</key>
					<string>16.0</string>
				</dict>
				<dict>
					<key>name</key>
					<string>visionOS</string>
					<key>version</key>
					<string>1.0</string>
				</dict>
				<dict>
					<key>name</key>
					<string>tvOS</string>
					<key>version</key>
					<string>17.0</string>
				</dict>
				<dict>
					<key>name</key>
					<string>macOS</string>
					<key>version</key>
					<string>14.0</string>
				</dict>
			</array>
		</dict>
	</dict>
</plist>
 

In total we have the following structure now:

.Documentation.docc
├── Info.plist
└── Overview.md

With the basic setup of the documentation bundle in place, we can now define a script to handle the building process for the components outlined earlier.

The script

To streamline the process and minimize manual effort, as mentioned earlier, we will create a concise yet effective script to manage the key steps efficiently.

Constants

We start with defining some constants:

build_docs.sh
SCHEME="the_scheme" # The scheme which builds all targets
DOCC_BUNDLE_PATH="./Sources/Documentation.docc" # Path to the .docc bundle

Next, define output paths for the symbol graphs corresponding to each platform you intend to support. Although creating dedicated directories for each platform isn't strictly required, doing so enhances clarity and organization. In my case, since the library supports iOS, watchOS, visionOS, tvOS, and macOS, I have specified the respective directories accordingly.

build_docs.sh
# Paths used in the script.
DERIVED_DATA_DIR=".deriveddata"
BUILD_DIR="${PWD}/.build"
SYMBOL_GRAPHS_DIR="${BUILD_DIR}/symbol-graphs"
SYMBOL_GRAPHS_DIR_IOS="${SYMBOL_GRAPHS_DIR}/ios"
SYMBOL_GRAPHS_DIR_WATCHOS="${SYMBOL_GRAPHS_DIR}/watchos"
SYMBOL_GRAPHS_DIR_VISIONOS="${SYMBOL_GRAPHS_DIR}/visionos"
SYMBOL_GRAPHS_DIR_TVOS="${SYMBOL_GRAPHS_DIR}/tvos"
SYMBOL_GRAPHS_DIR_MACOS="${SYMBOL_GRAPHS_DIR}/macos"
DOCCARCHIVE_PATH="${PWD}/${SCHEME}.doccarchive"
WEBSITE_OUTPUT_PATH="${PWD}/.docs-out"

Building

With all necessary constants defined, we can now tackle the first step: building the required targets and generating their symbol graphs needed for the final documentation. To generalize this process, we define a function to build for a specific platform.

build_docs.sh
build_for_platform() {
  local LOC_SYMBOL_GRAPHS_DIR=$1
  local LOC_DERIVED_DATA_DIR=$2
  local LOC_PLATFORM=$3
 
  mkdir -p "${LOC_SYMBOL_GRAPHS_DIR}"
  xcodebuild build \
    -scheme "${SCHEME}" \
    -destination "${LOC_PLATFORM}" \
    -derivedDataPath "${LOC_DERIVED_DATA_DIR}" \
    DOCC_EXTRACT_EXTENSION_SYMBOLS=YES \
    OTHER_SWIFT_FLAGS="-Xfrontend -emit-symbol-graph -Xfrontend -emit-symbol-graph-dir -Xfrontend ${LOC_SYMBOL_GRAPHS_DIR} -Xfrontend -emit-extension-block-symbols"
}

First, we set three local variables that describe the inputs to our function:

  1. The symbol graph directory
  2. The derived data output
  3. The platform we build for

Then, we create the directory for the symbol graphs. Next, we invoke xcodebuild to build the respective scheme. The key aspect here is the set of flags we supply to xcodebuild:

  • DOCC_EXTRACT_EXTENSION_SYMBOLS and -emit-extension-block-symbols will make sure that Swift extension declarations are included in the documentation
  • -emit-symbol-graph and emit-symbol-graph-dir place the symbol graphs in the correct laction

With a generic method to build our scheme for a specific platform and extract the symbol graphs, we can define the inputs needed for each platform and iterate over them by invoking our generic method.

build_docs.sh
platforms=(
    iOS,"📱","${SYMBOL_GRAPHS_DIR_IOS}"
    watchOS,"⌚","${SYMBOL_GRAPHS_DIR_WATCHOS}"
    visionOS,"🕶️","${SYMBOL_GRAPHS_DIR_VISIONOS}"
    tvOS,"📺","${SYMBOL_GRAPHS_DIR_TVOS}"
    macOS,"💻","${SYMBOL_GRAPHS_DIR_MACOS}"
)
 
# Iterate through each input and unpack its elements
for input in "${platforms[@]}"; do
    # Use IFS to split the input into its elements
    IFS=","
    set -- $input
 
    echo "$2 Building for $1"
    build_for_platform "$3" ${DERIVED_DATA_DIR} "generic/platform=$1"
done

This will print output for each platform in the shell:

📱 Building for iOS
SYMBOL_GRAPHS_DIR: <your_project>/.build/symbol-graphs/ios
DERIVED_DATA_DIR: .deriveddata

Creating the DocC archive

Now, let's move on to part 2: generating the DocC archive. To achieve this, we call the convert command of docc and supply our symbol graphs using the additional-symbol-graph-dir option.

build.sh
# Create a .doccarchive from the symbols.
$(xcrun --find docc) convert "${DOCC_BUNDLE_PATH}" \
  --fallback-display-name "${SCHEME}" \
  --fallback-bundle-identifier <fallback_bundle_identifer>\
  --fallback-bundle-version 1 \
  --output-dir "${DOCCARCHIVE_PATH}" \
  --additional-symbol-graph-dir "${SYMBOL_GRAPHS_DIR}" \

This command creates the archive at DOCCARCHIVE_PATH. By combining the symbol graphs from all platforms, docc incorporates this data into the final HTML, displaying the platform information. It will look similar to this:

Default appearance of a Tip

The final step involves transforming the archive into an HTML bundle that's ready for hosting.

build.sh
# Create a .doccarchive from the symbols.
$(xcrun --find docc) process-archive \
transform-for-static-hosting "${DOCCARCHIVE_PATH}" \
--output-path "${WEBSITE_OUTPUT_PATH}" \
--hosting-base-path "/your_base_path/"

We only need to supply the archive path as input and specify an output path for the generated HTML bundle. Finally, we provide the hosting-base-path option which is used to correctly set links within your documentation when hosted on a server. If you host your HTML on GitHub Pages, this will be the name of your repository as seen in the URL.

Conclusion

In this post, we saw how to utilize a combination of xcodebuild, symbol graphs, and docc to create multi-platform documentation. All you need is a minimal DocC bundle setup and a small shell script. This approach can effectively fill the gap until official support for multi-platform documentation is added to the DocC SPM plugin. If you'd like to see this script in action, check out the production version in my recent library.

Have feedback or suggestions? Feel free to reach out. Thanks for reading, and see you next time! 👋