Hey there
I'd like to present a pitch for how the Swift language could be evolved to add a new access control keyword designed for use in Swift Packages.
I look forward to hearing your feedback!
Introduction
Swift currently has a handful of access control modifiers, namely: open public internal private fileprivate
. These keywords change the visibility of parts of our code to change what other parts of our codebase have access to said code.
Swift Packages define targets which are groups of code. Targets can depend on other targets, and multiple targets can be combined to create a product. A product can be a dependency of another application or package.
I should state that I am massively oversimplifying for the sake of brevity here...
This pitch looks at access control keywords in the context of target to target communication.
Motivation
Package: "A"
Product: "A"
Target: "A"
Target: "B"
Dependency: Target "A"
Imagining the Swift package above, we have two targets one of which depends on the other. As a developer this allows me to separate concerns and test parts of my codebase in smaller chunks.
If I add a function (for example) to target A, and I want target B to be able to have visibility of it then I (currently) have no option but to use public
or open
. The other keywords would prevent package B from being able to see the function.
Now imagine that we have a separate application which depends on our package. This application now has access to all the code that was made public in target A - even though this function was only designed for use in target B. This is the issue this pitch aims to solve.
I believe it should be possible to write code which is public only within the package it is within.
Proposed Solution
My resolution to this problem follows safely on from the known and predictable access control methodology which Swift has had for a long time. We would add a new keyword: package
.
Exact keyword name can obviously be changed...
The keyword would only be available for code which is within a Swift package. In essence, projects where #if SWIFT_PACKAGE
is true. If you use it in a normal Xcode Project then you would get a warning.
Code marked with the package
access control keyword would be the equivalent of open
but only within the scope of the Swift package it sits within. It would be visible to all other targets which depend on said code.
If an application (not within the package) calls the function marked with the package
keyword, an error would be shown explaining that they need to change the visibility. This is the same as if you try to access an internal
function (etc) now.
Examples
// Target One
package func someFunction() -> String {
return "Hey folks!"
}
public func someFunctionTwo() -> String {
return "Hey folks!"
}
// Target Two
import TargetOne
someFunction() // = "Hey folks!"
someFunctionTwo() // = "Hey folks!"
// Application (Not in Package)
import TargetTwo
someFunction() // Error: Function is not available due to access control
someFunctionTwo() // = "Hey folks!"
Impact on Existing Code
This change should be entirely additive and as such would have no impact on existing code.
Users of a dependency using this feature would need to use the latest Xcode/Swift version in order for their code to compile.
Alternatives Considered
Private Target
It may be possible to add an enum
to the PackageDescription listing for a target. This would allow us to define a target as "internal" and as such all code within the target (even stuff marked as "public") would only be available to other targets within the same package.
This vaguely builds on top of the pitch raised by Ankit here.
Other Notes
It may be worth noting the @_spi
API which was introduced in Xcode 12 (Swift 5.3) as completed in this pull request. This feature is still experimental and while it puts a barrier up to importing the "hidden" package code, it's still possible and does not completely prevent users from calling those functions.
This attribute works by providing a label to definitions such as @_spi(Testing) class MyPackage {
and then can be imported by using the same label: @_spi(Testing) import PackageName
. This is similar (but more flexible) than the @testable
flag which has long been part of Swift giving a test target access to internal code.