🛎️ Controlling Access Within a Swift Package

TL;DR: Swift 5.9 introduces the package access modifier, allowing APIs to be shared across Targets within the same package without exposing them publicly. This supports safer and more modular Swift code by enabling internal sharing while preserving external encapsulation—ideal for designs like multi-Target packages that require internal-only protocols or methods.

Background

With the rise of modular programming, developers are increasingly using Swift Package Manager (SPM) to separate functionalities into multiple packages. Within a single package, it’s common to further divide code into multiple Targets. A recurring challenge in this structure is limiting API access to within the package (including between Targets) without making them public, which would expose them outside the package.

Introducing the package Access Modifier

As of Swift 5.9, the language introduces the package access control level. This modifier allows APIs to be shared across different Targets within the same package, without exposing them externally.

Swift
public struct MyStruct {
    public init() { ... }
    public var name: String { ... }
    package func action() { ... } // Accessible only within the same package
}

Real-World Use Case: Persistent Package Design

Consider a project that includes a Persistent package for all data persistence logic, split into multiple Targets:

Bash
Project
├── Domain
   └── Package.swift
└── Persistent
    ├── Package.swift
    └── Sources
        ├── Models       (Target)
        ├── CURD         (Target)
        └── Stack        (Target)

In the Domain package, a protocol defines a thread-safe method to convert a model to a ViewModel:

Swift
public protocol ViewModelConvertible {
    associatedtype Value: ViewModelProtocol
    @MainActor
    func convertToViewModel() -> Value
}

Since NSManagedObject is not thread-safe, some conversions need to happen in the thread of the NSManagedObjectContext, making @MainActor unsuitable. In such cases, a separate internal protocol can be defined using the package modifier—in any Target within the Persistent package.

Swift
package protocol ViewModelConvertibleUnsafe {
    associatedtype Value: ViewModelProtocol
    func convertToViewModelUnsafe() -> Value
}

This keeps the unsafe conversion method available across Targets inside the Persistent package, while hiding it from external packages. Consumers interact only with the safe, @MainActor-scoped API.

Further Reading

Weekly Swift & SwiftUI highlights!