Building Documentation for Both iOS and macOS Using DocC

April 16, 2023

Earlier this year I started working on a macOS version of Runestone, my open-source framework for building code editors. As is the case with many frameworks that support iOS, iPadOS, and macOS, there will be some platform differences in the public interface offered by the framework.

The challenge

Most often differences in public APIs can be communicated to the reader using Swift's @available(...) attribute. For example, this is how we can show that a function is only available on iOS 16 and newer and not available on macOS:

@available(iOS 16, *)
@available(macOS, unavailable)
func search(for query: SearchQuery) -> [SearchResult]

However, in some cases, we may not want to include a type, parameter, or function when compiling our codebase for a specific platform. This may be the case if we depend on a type that is not available on all platforms we support. In that case, we will use the #if os(...) macros.

#if os(macOS)
final class LineNumberRulerView: NSRulerView {
  // ...
}
#endif

It is important to keep in mind that when building documentation in Xcode by selecting Product → Build Documentation, Xcode is building for the selected destination only.

So in this case the LineNumbersRulerView will not show up in the documentation built by the DocC compiler when building for the iOS platform. Of course, we could build the documentation for macOS to include the symbol in the documentation but that leaves out any iOS-specific symbols. This is a challenge when we wish to publish the documentation online. That documentation should contain the symbols for all platforms.

The build process

Let us have a look at what goes on when building the documentation by selecting Product → Build Documentation in Xcode.

Illustration showing xcodebuild being invoked and outputting a file named Foo-iOS.symbols.json which is then provided as input to the DocC tool that then outputs a file named Foo.doccarchive.
  1. Xcode runs xcodebuild to build your project for the selected platform. This will emit a file named *.symbols.json that contains a description of the symbols, that is, all the types and their parameters and methods.
  2. Xcode then runs the docc command line tool with your documentation catalog and the symbol files as input. docc will output a file with the extension .doccarchive. This is a documentation archive that contains HTML, images, and stylesheets. It is essentially a website.
  3. Finally, Xcode will present the contents of the documentation archive in a new window.

Notice that xcodebuild is only outputting the symbols for the selected platform. To build documentation for both platforms we need the symbols for both iOS and macOS.

At the time of writing, there is no way to have Xcode build a documentation archive that contains the documentation for both iOS and macOS.

The solution

We can use the xcodebuild and docc command line tools included with Xcode to manually build a documentation archive by providing two symbol files to docc.

Illustration showing xcodebuild being invoked twice and outputting files named Foo-iOS.symbols.json and Foo-macOS.symbols.json which are then provided as input to the DocC tool that then outputs a file named Foo.doccarchive

The symbol files can be generated by building the project using xcodebuild build and supplying the -emit-symbol-graph and -emit-symbol-graph-dir Swift flags.

xcodebuild build \
  -scheme Foo \
  -destination "generic/platform=iOS" \
  -derivedDataPath .deriveddata \
  OTHER_SWIFT_FLAGS="-emit-symbol-graph -emit-symbol-graph-dir .build/symbol-graphs/ios"

After building the project for both iOS and macOS, the documentation archive can be built using docc build and supplying the symbol files using the --additional-symbol-graph-dir argument.

$(xcrun --find docc) convert Sources/Foo/Foo.docc \
  --index \
  --fallback-display-name Foo \
  --fallback-bundle-identifier Foo \
  --fallback-bundle-version 0 \
  --output-dir Foo.doccarchive \
  --additional-symbol-graph-dir .build/symbol-graphs

All of this can be put together into the following script that builds the project for iOS and macOS to generate the symbol files and then builds the documentation archive.

# Remember to change these two variables 👇
###########################################

SCHEME="Foo" # Remember to change this
DOCC_BUNDLE_PATH="Sources/Foo/Foo.docc"

###########################################

# Paths used in the script.
DERIVED_DATA_DIR=".deriveddata"
BUILD_DIR=".build"
SYMBOL_GRAPHS_DIR="${BUILD_DIR}/symbol-graphs"
SYMBOL_GRAPHS_DIR_IOS="${SYMBOL_GRAPHS_DIR}/ios"
SYMBOL_GRAPHS_DIR_MACOS="${SYMBOL_GRAPHS_DIR}/macos"
DOCCARCHIVE_PATH="${PWD}/${SCHEME}.doccarchive"

# Generate *.symbols.json file for iOS.
mkdir -p "${SYMBOL_GRAPHS_DIR_IOS}"
xcodebuild build \
  -scheme "${SCHEME}" \
  -destination "generic/platform=iOS" \
  -derivedDataPath "${DERIVED_DATA_DIR}" \
  OTHER_SWIFT_FLAGS="-emit-symbol-graph -emit-symbol-graph-dir ${SYMBOL_GRAPHS_DIR_IOS}"

# Generate *.symbols.json file for macOS.
mkdir -p "${SYMBOL_GRAPHS_DIR_MACOS}"
xcodebuild build \
  -scheme "${SCHEME}" \
  -destination "generic/platform=macOS" \
  -derivedDataPath "${DERIVED_DATA_DIR}" \
  OTHER_SWIFT_FLAGS="-emit-symbol-graph -emit-symbol-graph-dir ${SYMBOL_GRAPHS_DIR_MACOS}"

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

# Clean up.
rm -rf "${DERIVED_DATA_DIR}"
rm -rf "${BUILD_DIR}"

The script assumes that your scheme is named Foo and that your documentation catalog is stored at Sources/Foo/Foo.docc. Remember to change this before running the script.

That's it! The resulting documentation archive will contain symbols for both iOS and macOS.

Thanks a ton to Franklin Schrans for providing the guidance needed for me to put this script together 🙏