Modularization with Swift Package Manager
A module is a compiler-enforced dependency boundary. It can protect core behavior from UI and storage frameworks, but every module adds manifests, public API decisions, build graph edges, and coordination cost.
Start with an inward graph
FieldNotesSwiftUI ----+ |FieldNotesUIKit ------+--> FieldNotesApplication --> FieldNotesDomain |SwiftDataAdapter -----+The domain target imports no Apple UI, database, or network framework. The application target owns use cases and ports. App targets compose selected adapters.
// Package.swift excerpt.target(name: "FieldNotesDomain"),.target( name: "FieldNotesApplication", dependencies: ["FieldNotesDomain"]),.target( name: "FieldNotesPersistence", dependencies: ["FieldNotesApplication", "FieldNotesDomain"])Extract along stable seams
Useful signals include reuse by two app targets, independent ownership, slow incremental builds, a dangerous dependency that should not spread, or a core that needs command-line tests. A folder can express organization before a package is justified.
Design the public surface
Moving code into a package forces access-control choices. Export use cases and domain values that clients need, not internal helpers or database shapes. Avoid a catch-all shared module that becomes a new global namespace.
Do not start with dozens of feature and utility packages. Measure build time and coordination, then split the dependency graph where a concrete outcome improves.
Series navigation
- Previous: Part 66: Repositories, gateways, clients, and ports and adapters
- Next: Part 68: Data architecture, source of truth, caching, offline sync, and conflict
- Series index: Zero to iOS Hero
References
- Swift Package Manager documents package products, targets, dependencies, and builds.
- Access control defines Swift module visibility.