Swift has 6 access levels ranging from open to private.
Typically if you do not want consumers of your code to access a function or type, you can mark it private
. However, Apple frameworks written in Swift, particularly SwiftUI,
contain APIs that are meant to be used by other Apple frameworks, but not by 3rd party apps. This is achieved by limiting when the code can be seen at compile time, but still
allowing it to be found at link time. In this post we’ll look at how you can still call these functions in your own code to use features that are not typically available.
Compiling vs. Linking
First, we’ll walk through how a typical external function call is compiled and used, with the SwiftUI function Text.fontWeight
as an example. To compile your code that uses this function, the Swift compiler
first finds it in a .swiftinterface
file provided by SwiftUI. Specifically, it is found in $SDKROOT/System/Library/Frameworks/SwiftUICore.framework/Modules/SwiftUICore.swiftmodule/arm64-apple-ios.swiftinterface
which contains the following:
extension SwiftUICore.Text {
nonisolated public func fontWeight(_ weight: SwiftUICore.Font.Weight?) -> SwiftUICore.Text
}
These interface files do not provide the implementation, just this skeleton that the compiler needs to verify your code is correct. For instance, this is how the compiler
knows the expected type of the weight
parameter. Next, the linker takes the output of the compiler and creates the final binary. Just like the compiler, the linker
needs to know where to find the function you are calling. This is done with .tbd
files, which are text based stubs informing the linker of what functions are available
to be called in a framework. For our function, the relevant file is $SDKROOT/System/Library/Frameworks/SwiftUICore.framework/SwiftUICore.tbd
.
They are text files, and opening it up in a text editor reveals lines like _$s7SwiftUI4TextV10fontWeightyAcA4FontV0E0VSgF
. This is a mangled Swift symbol, and
it demangles to SwiftUI.Text.fontWeight(SwiftUI.Font.Weight?) -> SwiftUI.Text
, which is exactly the function we are looking for.
There is a third place the function needs to be found for everything to work - the exported symbols of the framework at runtime and can be viewed with nm
:
nm -gU /Library/Developer/CoreSimulator/Volumes/iOS_22C5125e/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 18.2.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/SwiftUICore.framework/SwiftUICore | xcrun swift-demangle | grep '.fontWeight('
This is so dyld knows what code at runtime to execute when your app calls a function in another framework.
To summarize, calling an external function from your code means it must be defined in three places:
- A
.swiftinterface
file for the compiler to recognize it - A
.tbd
file for the linker to recognize it - The exported symbols of the binary at runtime for dyld to recognize it
System Programming Interface
In addition to the regular access levels in Swift, library developers can define a System Programming Interface (SPI), which is a kind of API that only some clients can use.
This is defined using the experimental attribute @_spi(spiName)
. For example:
public class MyClass {
public function doSomething() { ... }
@_spi(Private)
public function doSomethingPrivate() { ... }
}
Now to call doSomethingPrivate
clients will need to use a special import syntax: @_spi(Private) import MyModule
. SPI is implemented by producing a new *.swiftinterface
file, separate from the default interface that only contains the API. Apple frameworks appear to make frequent use of this feature, because they have functions that are
available at link time but not part of the API. For an in-depth look at this attribute, check out this blog post.
How to access any SPI
Now that we’ve seen the public API of a framework is also in the .tbd
file, we can use this to view what functions are available to us even if we do not have the SPI interface.
For SwiftUI, we can run the entire text based stub through swift-demangle and notice some APIs that are not publicly documented, like this one:
'SwiftUI.ViewPreviewSource.makeView.modify : @Swift.MainActor () -> SwiftUI.View', 'SwiftUI.ViewPreviewSource.makeView.getter : @Swift.MainActor () -> SwiftUI.View',
So there is a type called ViewPreviewSource
that has a makeView
property which we can call to get a SwiftUI.View
. This is
how the #Preview
macro works internally. Since it is still accessible by the linker, we only need it to be in a swift interface file to use it. Unfortunately,
it was not included as part of the ABI, likely because it is an SPI and not in the interface files that Xcode provides. However, there is
a surprisingly easy workaround for this: We can edit the .swiftinterface
file to add the missing APIs. The addition to the file looks like this:
public struct ViewPreviewSource {
public var makeView: @_Concurrency.MainActor () -> any SwiftUI.View
}
There’s still one catch. After modifying the interface, you can build code that references the SPI, but if you distribute that code to others they will not be able to use it. I encountered this problem when developing the preview based snapshot testing package SnapshotPreviews. This Swift package needed to call the SPI to render an Xcode preview to an image, but I wanted anyone to be able to use it without making any changes to their Xcode installation. To work around this I defined a protocol that re-declared the functionality:
public protocol MakeUIViewProvider {
var makeView: @MainActor () -> UIView { get }
}
@_spi(Private)
extension ViewPreviewSource: MakeViewProvider { }
Notice the use of @_spi
again. This is so when the code is compiled that conformance is not included in the interface file. This is crucial because otherwise, users
of the framework would also need to modify their interface files for the type ViewPreviewSource
to be defined. Once this code is compiled using a version
of Xcode that has the modifications, other Swift packages can import it as a dynamic framework and then cast types to MakeUIViewProvider
. This lets the
runtime (instead of the compiler) find the function that was previously only in SPI. This technique re-exports a hidden type through a runtime protocol check.
The full code for this can be found in the SnapshotsPreviews repo. If you find any other interesting hidden APIs in Apple’s frameworks I would love to hear about them! Feel free to reach out on X or Mastodon.