Overview
A Swift module is the unit of compilation and access control. Everything in a module shares internal visibility by default; only explicitly public declarations are visible outside. This page covers Swift Package Manager (SPM) target layout, import semantics, access level rules, @_implementationOnly, and resource bundles. For the top-level Swift rules, see swift.
Structure packages by feature, not by layer
Organize an SPM package so each target owns a vertical slice of functionality, not a horizontal layer (models, services, views). A feature target can be compiled, tested, and replaced independently.
MyApp/
├── Package.swift
├── Sources/
│ ├── AppCore/ # domain types shared across features
│ ├── ProfileFeature/ # profile UI, view model, domain logic
│ ├── FeedFeature/ # feed UI, view model, domain logic
│ └── NetworkClient/ # HTTP layer; depends on AppCore
└── Tests/
├── ProfileFeatureTests/
└── FeedFeatureTests/
Each target declares its dependencies in Package.swift. A feature that does not depend on another feature’s target cannot accidentally import its internals. This is the module boundary you want to enforce.
Use private as the default access level
Start every declaration at private. Open up to fileprivate, internal, or public only when a caller actually requires the symbol. The compiler catches access violations; be explicit rather than relying on convention.
// private: only this type can call it
private func validateCredentials(_ creds: Credentials) -> Bool { ... }
// fileprivate: a helper type in the same file needs it
fileprivate func sharedFormatting(date: Date) -> String { ... }
// internal (the default): other files in this module
func buildRequest(for endpoint: Endpoint) -> URLRequest { ... }
// public: exported to other modules
public func configure(with settings: AppSettings) { ... }Spell out internal on declarations you review in diffs; silent defaults disappear in code review.
Import only what you need; prefer granular imports
Swift resolves import Module by importing all public symbols. For large modules, a more specific import reduces build system dependencies and makes the dependency graph legible.
// Importing an entire framework when only one type is needed
import Foundation // pulls in hundreds of types
// Preferred when you only need one type (Swift 5.9+)
import struct Foundation.Date
import class Foundation.URLSessionGranular imports are available in Swift 5.9 and later. Use them in library targets to make the dependency surface explicit. In application targets, the ergonomic cost may outweigh the benefit; use your judgment.
Use @_implementationOnly to hide internal dependencies
When a target uses another module internally but does not want to expose that module on its public API surface, @_implementationOnly import prevents re-exporting.
// In a library target: CryptoKit is an implementation detail, not API
@_implementationOnly import CryptoKit
public func hash(_ data: Data) -> String {
let digest = SHA256.hash(data: data)
return digest.map { String(format: "%02x", $0) }.joined()
}Callers of the library do not need to import CryptoKit. The @_implementationOnly attribute is technically unofficial but is widely used and stable in practice. Swift 5.9 introduced internal import as the official replacement; prefer internal import on new code targeting Swift 5.9 or later.
Handle module resources through the bundle accessor
SPM generates a Bundle.module accessor for targets that declare resources in Package.swift. Always use it instead of Bundle.main or a hardcoded bundle identifier.
// Package.swift resource declaration
.target(
name: "ProfileFeature",
resources: [.process("Resources")]
)
// Runtime access
let image = UIImage(named: "avatar-placeholder", in: .module, compatibleWith: nil)
let url = Bundle.module.url(forResource: "sample-data", withExtension: "json")Bundle.main fails when the target runs in a Swift Playgrounds app, a test host, or a Swift macro plugin. Bundle.module always resolves to the correct bundle regardless of the hosting context.
Limit open to types designed for subclassing
open allows subclassing and override from outside the module. It is rarely the right choice: it commits the module to a stable superclass contract and enables callers to replace any behavior.
- Use
openonly on a type explicitly designed as an extension point for other modules. - Use
publicfor types that are visible externally but should not be subclassed outside the module. - Mark non-final
publicclasses with a comment explaining why subclassing is intentionally allowed within the module.
Virtually all swiftui usage and most networking code never needs open. Prefer protocol conformances and view modifiers over subclassing entirely.
Pin minimum versions and test on the supported matrix
A Package.swift manifest that omits a swiftLanguageVersions setting inherits the toolchain default. Declare the versions you support explicitly to prevent silent behavior changes during upgrades.
// Package.swift
let package = Package(
name: "ProfileFeature",
platforms: [.iOS(.v17), .macOS(.v14)],
...
)Test targets inherit the same access control rules as library targets. Internal types are accessible to test targets declared in the same package via @testable import, which grants internal access to the test bundle.