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.
- 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. - 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. - 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
.
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 🙏