Skip to content

Modules, packages, access control, interoperability, and API design

A Swift module is a compilation and import boundary. A Swift package describes products, targets, dependencies, platform floors, and language modes that Swift Package Manager can build together. Good package design begins with the behavior consumers need, not with making every declaration public.

Field Notes uses a FieldNotesCore library so SwiftUI, UIKit, a command-line checkpoint, and tests can share the same note rules without importing one another.

Package, product, target, and module

The words describe different layers:

TermRole
packagemanifest and source tree managed by SwiftPM
productlibrary or executable another package can consume
targetunit of sources, settings, and dependencies
modulenamespace produced by compiling a target

The package manifest publishes FieldNotesCore as a library product and builds it from a target with the same name:

let package = Package(
name: "FieldNotesCore",
platforms: [
.iOS(.v17),
.macOS(.v14),
],
products: [
.library(
name: "FieldNotesCore",
targets: ["FieldNotesCore"]
),
],
targets: [
.target(name: "FieldNotesCore"),
.testTarget(
name: "FieldNotesCoreTests",
dependencies: ["FieldNotesCore"]
),
],
swiftLanguageModes: [.v6]
)

The manifest’s platform floor describes where the product can be used. It does not prove an iOS app build. SwiftPM tests on this machine prove the package’s host-compatible core only.

Public API is a promise

Declarations default to internal, visible inside their module. Raise access only for a consumer need.

public protocol NoteRepository: Sendable {
func load() async throws -> [FieldNote]
func save(_ note: FieldNote) async throws
func delete(id: NoteID) async throws
}
public struct NoteLibrary: Sendable {
private let repository: any NoteRepository
}

The app can supply a repository, but it cannot reach through NoteLibrary and replace its dependency. The module exposes the use-case boundary and hides storage.

Swift access levels move from private and fileprivate through internal, package, and public to open. open matters for subclassing and overriding outside a module. Most domain APIs do not need it.

package supports implementation sharing among targets in one package without publishing that declaration to clients. It can be a better seam than making test helpers public.

Design the call site first

A useful API makes ownership, mutation, failure, and concurrency visible:

let library = NoteLibrary(repository: repository)
let notes = try await library.notes(matching: "coast")

The initializer requires the dependency. The method name states the query. async throws preserves suspension and failure. The result stays in domain types.

Avoid public setters that let consumers create invalid state. Avoid framework types in the core unless the framework is part of the intended contract. A public API gets harder to change as more modules depend on it.

Tests consume the boundary

The package has a dedicated Swift Testing target. Tests import FieldNotesCore and a deterministic support module, then verify ordering, search, normalization, failure, and mutation behavior.

@Test("Blank titles are rejected", arguments: ["", " ", "\n\t"])
func blankTitlesAreRejected(title: String) {
#expect(throws: FieldNoteValidationError.blankTitle) {
try FieldNote(
id: NoteID(rawValue: "note-invalid"),
title: title,
createdAt: FieldNotesFixtures.firstDate,
updatedAt: FieldNotesFixtures.firstDate
)
}
}

Testing only through public API exposes missing contracts. @testable import can reach internal declarations, but it can also couple tests to implementation details. Prefer public behavior unless an internal algorithm needs focused proof.

Interoperability belongs at an edge

Swift can import C and Objective-C APIs through Clang modules and generated interfaces. Objective-C compatible Swift APIs use a smaller type system. Features such as associated types, many enums with payloads, and Swift-only concurrency contracts do not cross directly.

Keep imported pointers, status codes, nullability quirks, completion handlers, and Objective-C object graphs in an adapter:

C or Objective-C API
|
v
interop adapter
|
v
FieldNotesCore values and errors

The adapter translates provider data once. The domain does not spread UnsafePointer, NSError, or framework-specific objects through every use case.

Use @objc and NSObject only where an Objective-C runtime contract needs them. Do not annotate the whole domain for hypothetical compatibility.

Library evolution is a separate promise

A source package can rebuild clients and dependencies together. Binary frameworks introduce module stability, library evolution, ABI, resource, signing, and distribution concerns. Do not claim binary compatibility merely because a public Swift package builds.

Start with the source boundary. Add binary distribution constraints only when a real consumer requires them.

Validation evidence

The companion package uses Swift tools version 6.0 and Swift 6 language mode. Its library, test-support, and command-line products build with warnings treated as errors under the available Apple Swift 6.3.2 Command Line Tools.

The repository’s package-test wrapper supplies the Command Line Tools framework, macro-plugin, and linker paths that raw swift test does not discover. All seven Swift Testing checks pass through that wrapper. The package evidence proves the manifest, module imports, public API, in-memory actor repository, CLI composition, and tested domain behavior. It does not prove Xcode 26.6, an iOS SDK build, Objective-C header generation, a mixed-language target, binary library evolution, Simulator, signing, or device behavior.

Check your understanding

You should now be able to explain:

  • How a package, product, target, and module differ.
  • Why internal is the right default.
  • What consumer need makes a declaration public.
  • Why tests through public behavior resist refactors better.
  • Where C and Objective-C translation belongs.

The language foundation is now complete. The next post begins with the person using Field Notes, the situation they are in, and the smallest outcome worth shipping.

Series navigation

References