Swift Package Manager is now the default dependency manager and code-sharing tool for Apple platforms. CocoaPods is dead, Carthage is legacy, and SPM handles dependencies, local packages, and resource bundles in a single Package.swift file. For multi-platform apps (iOS + macOS + watchOS), SPM is the most practical way to share code without duplicating it across targets.
This guide covers SPM patterns for production multi-platform apps in 2026: Package.swift structure for shared code, resource bundle handling, conditional platform support, and the integration story with XcodeGen for projects that generate their Xcode project from project.yml.
Package.swift for a shared code library
The typical pattern for an app portfolio: one Swift Package called something like AppFoundation containing analytics, premium management, crash reporting, and shared UI components. Every app in the portfolio depends on this package via a local path.
The Package.swift manifest declares: package name, default localization, supported platforms (iOS, macOS, watchOS with minimum version each), exposed library products, external dependencies (RevenueCat, TelemetryDeck, Sentry), and the targets themselves with their dependencies and resources.
For a portfolio with both iOS/macOS apps and a watchOS extension, define two library products: AppFoundation (full, with RevenueCat) and AppFoundationLite (subset without RevenueCat, since RevenueCat doesn't support watchOS). Apps pick the right product based on platform.
Resources. The resources: [.process("Resources")] line tells SPM to treat the Resources folder as a resource bundle. Files inside are accessible at runtime via Bundle.module. The build system handles localisation, asset catalogs, and lproj folders automatically. Important: Bundle.module is generated by SPM and only exists inside the package - application code uses the bundle returned by Bundle.main.
The whole Package.swift typically fits in 80-120 lines for a portfolio shared library. Version control it with the rest of your code, and treat it as the source of truth for shared dependencies across all apps.
Conditional platform code and XcodeGen integration
Conditional code. Some shared modules need different behaviour per platform. For example, AppFoundation might use UIKit on iOS but AppKit on macOS. The standard pattern uses #if canImport(UIKit) for iOS-specific code and #elseif canImport(AppKit) for macOS-specific code, with a public typealias PlatformImage to abstract over UIImage vs NSImage at the API boundary.
For target-level conditional dependencies (e.g. RevenueCat only on iOS/macOS, not watchOS), use the condition: parameter on .product entries: condition: .when(platforms: [.iOS, .macOS]). The dependency is skipped for other platforms, and any code referencing it must also be conditional.
XcodeGen integration. Apps using XcodeGen to generate their Xcode project from project.yml can depend on a local Swift Package by adding it to the packages section of project.yml with the relative path, then listing it as a dependency on the relevant target.
The relative path is stable as long as your repo structure is. For the PARA workspace setup, ../../Packages/AppFoundation works across all apps in the portfolio because every app lives at the same depth.
Running xcodegen generate after modifying Package.swift dependencies regenerates the .xcodeproj with the new dependencies linked correctly. No manual Xcode wiring required. The combination of Package.swift for code sharing and XcodeGen for project regeneration is the cleanest workflow for an app portfolio that needs to share substantial code across many apps.
One gotcha: SourceKit (Xcode's autocomplete engine) caches stale diagnostics after bulk edits across multiple targets. After significant Package.swift changes, run a clean build (xcodebuild clean build) rather than incremental - SourceKit will sometimes show errors that don't actually exist after an incremental build.
Quick comparison
| Option | Best for | Cost / effort | Notes |
|---|---|---|---|
| Package.swift | Manifest | swift-tools-version 5.10 | Targets + dependencies |
| Bundle.module | Resource access | SPM-generated | Localized strings + assets |
| Conditional deps | Per-platform | condition: .when(...) | watchOS exclusions |
| XcodeGen | Package path | project.yml | Re-run xcodegen on change |
Common pitfalls and how to avoid them
Across every domain this article touches, the same shape of mistake recurs. Practitioners new to the field overweight the most visible piece of the system — the screenshot, the paywall, the exam question, the headline price — and underweight the underlying constraint that actually determines outcomes.
The five most common failure modes:
- Optimising for the demo, not the durability. A working demo in a controlled environment proves nothing about reliability under real conditions. In iOS development, an in-app purchase flow that works in the Xcode Simulator says nothing about how it behaves in App Store sandbox with network latency and Ask to Buy approvals. In an exam, a 100% score on an untimed quiz tells you nothing about whether you can do 49/50 in 45 minutes with no second guesses. Build for the hardest realistic case from the start.
- Skipping the first-principles documentation. Every system has a canonical specification. App Review Guidelines for iOS, the official EU regulations for tax deductibility, the CITB question bank for CSCS, the OMIE market rules for Spanish electricity. Reading them takes a few hours but saves weeks of wrong-direction work. Secondary sources (blogs, tutorials, this article included) are useful as orientation but never authoritative.
- Ignoring the rate limit. Every external system has rate limits — explicit (APNs silent push throttling, RevenueCat API quotas, exam retake fees) or implicit (App Review patience, customer attention spans, your own working memory). Plan around them. A workflow that requires more rate-limited operations than the system allows will fail in production, not on day one but during the first stress event.
- Underweighting localisation and regional variation. What is true for Germany is not always true for Italy. What is true for English-speaking users is not always true for Japanese ones. What is true for the UK CSCS test is not always true for the Irish equivalent. Always check the local rule before applying a general one.
- Treating the documentation as static. Apple updates App Review Guidelines. The Bundeslaender change Schonzeiten. OMIE adjusts market clearing algorithms. Set up a periodic review (quarterly is enough for most things) and re-read the canonical sources. Workflows that worked perfectly a year ago can be silently broken today.
None of these are dramatic. The dramatic mistakes (catastrophic bugs, audit findings, exam failures) are the visible tip of a longer-running iceberg of small misses. Catching the small misses is what separates routine outcomes from problematic ones.
Key takeaways
- Package.swift — Manifest. Targets + dependencies.
- Bundle.module — Resource access. Localized strings + assets.
- Conditional deps — Per-platform. watchOS exclusions.
- XcodeGen — Package path. Re-run xcodegen on change.
The pattern that runs through every section above: start with the constraint, not the wishlist. In an exam, the constraint is the question bank and the pass mark. In an electricity market, it is the auction clearing rule. In a tax workflow, it is the receipt-retention requirement. In a code architecture, it is the platform's design decision (StoreKit's transaction lifecycle, App Review's guideline, APNs's authentication model). Get the constraint right and the rest follows.
The opposite failure mode — designing for an aesthetic ideal, then trying to retro-fit the constraint — is the most common cause of wasted work in every domain covered here. A beautiful paywall that hangs in sandbox is rejected at App Review. A polished freelancer expense report that lacks receipts is disallowed by the tax office. A study plan that ignores the actual question distribution leaves the candidate stuck below the pass mark.
The practical recommendation: read the official rules of whatever system you are operating in, extract the binding constraints, and treat them as inputs to the design — not afterthoughts. Every section of this article is the application of that principle to a specific domain.
FAQ
Should I use SPM or CocoaPods in 2026?
SPM. CocoaPods is deprecated and no longer actively developed. SPM has caught up on every feature CocoaPods used to have advantages on (resource bundles, build settings, dynamic frameworks). New projects should use SPM exclusively.
Can I share UI code between iOS and macOS via SPM?
Yes, via SwiftUI. SwiftUI works on iOS, macOS, watchOS, tvOS and visionOS. UIKit code can be shared with AppKit equivalents via conditional compilation, but pure SwiftUI is the cleanest path.
How do I version-lock a Swift Package?
Use .exact, .upToNextMajor, or .upToNextMinor in the dependencies section. For local packages, version locking isn't needed since the path is fixed.
Can SPM handle binary frameworks?
Yes, via .binaryTarget with a remote URL or local path. Useful for closed-source SDKs distributed as XCFrameworks.
Further reading and references
The references below cover the official sources for the rules cited in this article. Where applicable, they include the canonical documentation, regulatory text, or vendor-provided guides. For each one, prefer the official source over secondary commentary — secondary sources go stale fast and frequently misquote the binding rule.
- Official documentation of the system in question (linked from each app or service's own help centre).
- Apple Developer Documentation for any iOS/macOS reference — the WWDC session videos and the corresponding Human Interface Guidelines pages are the authoritative source.
- For EU regulatory questions (taxation, data protection, energy market structure), consult the relevant national authority — most publish their guidance in English.
- For Spain and Italy energy market data, OMIE and GME both publish full historical price series in CSV format from their public websites — no API key required.
- For UK CSCS prep, the CITB publishes the official question bank book each year — buy a current copy if you want the authoritative source.
If you find a contradiction between this article and an official source, the official source wins. Article rules of thumb are summaries — they have edge cases, exceptions, and regional variations that the source documents specify exactly.
Need help structuring an app portfolio?
Multi-platform iOS/macOS development with shared Swift Packages. AppFoundation, RevenueCat, TelemetryDeck integrated.
Book a discovery call