We recently underwent an effort to improve our PSPDFKit for iOS API documentation — which is generated by an open source tool — so as to provide a more Swift-first experience.
As part of it, we wanted to audit and modernize all our documentation beforehand to make sure all of our public APIs, which still use Objective-C in a lot of places, were correctly translated to Swift. This is because our goal was to have all our symbols and documentation comments visible for both Swift and Objective-C — including all the custom refinements and renames we do specifically to provide a first-class Swift API.
However, while doing this, we identified a few issues in the generation of Swift API, especially when generated from Objective-C code, that had an impact on the resulting documentation. Some APIs were simply not shown to be available in Swift documentation at all, while some symbols used the wrong name for types. This resulted in invalid Swift APIs, as those types would have a different name in Swift, which was usually changed from the Objective-C type name via the
So now, let’s look at the actual issues we faced and how they impacted our Swift API documentation.
The first issue we came across was for Objective-C headers that used forward declarations. For example, when forward declaring a protocol in a header file and using this protocol as a parameter, return, or property type, the resulting API didn’t show up in the Swift documentation, and it was seemingly only available in Objective-C. However, using the API from Swift in the actual compiled framework worked without any problems. This meant the issue must have been somewhere in the API documentation generation workflow.
Additionally, there are different variations of this issue happening, depending on which types you’re dealing with and how you use them. For example, this issue affects not only protocol forward declarations, where the API that uses them is completely missing, but also class forward declarations. If you add a forward declaration for an Objective-C class that uses the same name in Objective-C and Swift, it creates the correct output, and the API using the class is shown correctly in both languages.
However, if the class uses a custom Swift name via the
NS_SWIFT_NAME macro, the issue mentioned above occurs again, although in a more subtle way. If the forward declaration was made with the Objective-C name, which is the supported way, the generated Swift API shows up in Swift code as the incorrect original Objective-C name of the symbol, which isn’t a valid API.
A header file that exhibits this issue looks like the following:
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @protocol PSPDFClassDelegate; @interface PSPDFClass: NSObject - (nullable id<PSPDFClassDelegate>)delegate; @end NS_ASSUME_NONNULL_END
When generating the API documentation for this class, the
delegate symbol is missing in Swift. Note that if we import the file containing the
PSPDFClassDelegate protocol instead of forward declaring it — for example, via
#import "PSPDFClassDelegate.h — the
delegate API correctly shows up in the generated Swift API.
delegate from actual Swift code works without any issues, and therefore isn’t a problem for the actual framework we provide.
Missing Nested Types
When declaring a nested Swift type name for Objective-C APIs, such as
NS_SWIFT_NAME(Annotation.Tool), in some cases, the entire type is missing from the generated Swift API, and it only shows up as being available through Objective-C. This is the case when the top-level nested type of the symbol name (as in the aforementioned case of
Annotation) isn’t available in the context of the header file of the nested type.
There are two approaches to fix this issue. Forward declaring the Objective-C type name makes the type show up, but only if the name isn’t adapted for Swift. The second option is to import the header containing the type. This is the preferred option, since it also works when renaming the type for Swift.
Curiously, forward declaring the renamed Swift type works. However, this is a code smell; it’s using a Swift type name in an Objective-C context, so it’s not something we recommend using in production. Additionally, it might only be useful for post-processing code for a tool so as to generate the correct API documentation output.
One example producing no output in a Swift API is the following:
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN NS_SWIFT_NAME(Annotation.Tool) @interface PSPDFAnnotationTool: NSObject - (NSString *)toolType; @end NS_ASSUME_NONNULL_END
Annotation.Tool class shows up as not having a Swift API at all. As with the issue above, there are workarounds to get a Swift API to show up again: You can either import the header declaring the
Annotation type, or you forward declare
Diving Into Source Code Analysis
We had to dive into the source code to figure out the origin of these issues, as we wanted to fix them before migrating our documentation to be Swift-first.
We currently use the tool jazzy to generate our API documentation. The first step was looking at the underlying code analysis tool used by jazzy, SourceKitten, and examining its parsed output to further understand the origin of our problems.
SourceKitten is actually an open source Swift layer around Apple’s source code analysis product, SourceKit, which is also used in Xcode to provide syntax highlighting and code completion, along with generating Swift interfaces from Objective-C headers.
Looking at the output of SourceKitten, we could see that the issue was also exhibited there, which told us the issues went deeper. Since SourceKitten heavily relies on SourceKit, the next step was trying to figure out if it also showed these same problems.
Inspecting the Generated Interface
To verify this, we created an Objective-C header with the above-mentioned code, and we looked at the Generated Interface in Xcode. This feature can be found when viewing an Objective-C header file and clicking the Navigate to Related Items button. Look for the four squares located in the top-left corner of the Xcode editor view, and select the Generated Interface > Header.h (Swift X Interface) option. This shows the Swift file that will be generated from your Objective-C code — including all the available APIs.
When we did this, voilà, the issue was also present there. APIs using any of the two above-mentioned issues related to forward declarations and nested types also either were not shown in the Generated Interface, or were using the wrong symbol names.
In the image below, you can see the Swift APIs are missing.
And when using one of the workarounds mentioned, like importing the header, the APIs show up again.
Since SourceKit seemingly only ever parses a single file to provide the generated Swift interface, it doesn’t resolve the forward declarations, and the APIs turn out incorrect compared to what the actual API looks like when compiling code. That’s one of the reasons why, in some cases, the Generated Interface stays completely empty as well, making it look like there are no Swift APIs at all.
All the above-mentioned issues don’t have any effect on the actual compiled product. All of the APIs, even if they aren’t shown in the Generated Interface, are still available and working. This only affects SourceKit and all the tools using it, as well as the Generated Interface in Xcode.
Once we figured this out, we could report an issue to Apple, but at the time of writing this post, we haven’t yet received an answer.
Alternative API Documentation Tools
Since the root cause of issues is located deep in the default development tool and not in the open source tool we’re using, there’s unfortunately no way we can fix those issues ourselves. As such, we’re looking into using different documentation tools, as we can’t know when, or even if, SourceKit might resolve these problems.
The most obvious choice would be using DocC as the API documentation tool. It was introduced at WWDC 2021, and because it added support to Objective-C in Xcode 14, it’d be the perfect candidate to switch to.
Both the issues mentioned above aren’t present in DocC, as it doesn’t use SourceKit under the hood, but rather a custom symbol graph generation for building the available API. DocC also provides the ability to directly ship a documentation catalog to SDK users to view the documentation directly in Xcode, which is another benefit. However, we need to evaluate DocC further before we can make a decision to switch to it at some point in the future.
Throughout all of this, I learned that even though it might look like an issue is present in a tool, it doesn’t always mean that it’s actually the tool’s fault, and it can be worth it to spend some time understanding the internals of a project and investigating more deeply.