Overview
A module is the unit of compilation and access control: declarations are internal to it unless marked otherwise. Structure packages so module boundaries enforce your architecture. Top-level rules are in swift. Swift 6.4 makes Swift Build the default build system in Swift Package Manager.
Split targets by feature, not by layer
A vertical slice (UI, view model, and domain logic for one feature) can be built, tested, and replaced on its own. A target that does not list another feature in its dependencies cannot import that feature’s internals.
Sources/
├── AppCore/ # domain types shared across features
├── ProfileFeature/
├── FeedFeature/
└── NetworkClient/ # depends on AppCore
Start at the narrowest access level
Begin with private and widen only when a caller needs the symbol. The levels, narrowest first:
privateandfileprivatefor implementation details.internal(default) for the module.package(Swift 5.9) for other modules in the same package, so a multi-target package shares internals without making them public API.publicfor the consumed surface, andopenonly for classes designed for subclassing outside the module.
Avoid open: it commits you to a stable superclass contract and lets callers replace any behavior. Prefer protocol conformance (swift-protocols).
Hide dependencies with import access levels
internal import CryptoKit (SE-0409, Swift 6.0) keeps a dependency out of your public interface, so clients do not need to import it. It replaces the unofficial @_implementationOnly import and gives stricter type checking. Use package import to share a dependency inside a package. The InternalImportsByDefault upcoming feature flips the default from public to internal when you want that enforced everywhere.
Load resources through Bundle.module
SwiftPM generates Bundle.module for targets that declare resources. Bundle.main is the app bundle and misses package resources in tests, previews, and other hosts.
.target(name: "ProfileFeature", resources: [.process("Resources")])
let url = Bundle.module.url(forResource: "sample-data", withExtension: "json")Set platforms, language mode, and isolation in Package.swift
State the platforms you support, and choose the language mode explicitly. Under tools-version 6.0, swiftLanguageVersions is deprecated and renamed swiftLanguageModes, and a mode can be set per target for gradual migration to Swift 6. UI targets can also take a default isolation.
// swift-tools-version: 6.2
let package = Package(
name: "ProfileFeature",
platforms: [.iOS(.v17), .macOS(.v14)],
targets: [
.target(name: "ProfileFeature", swiftSettings: [.defaultIsolation(MainActor.self)]),
.target(name: "LegacyBridge", swiftSettings: [.swiftLanguageMode(.v5)]),
],
swiftLanguageModes: [.v6]
).defaultIsolation(MainActor.self) is the SwiftPM form of -default-isolation MainActor and needs tools-version 6.2 (swift-concurrency). Test targets reach internal declarations with @testable import.