Swift / iOS — SwiftPM, CocoaPods, Carthage
SwiftPM (Package.resolved)
// Package.swift
.package(url: "https://github.com/apple/swift-nio.git", exact: "2.65.0"),
// Or for a version range:
.package(url: "...", .upToNextMajor(from: "2.0.0")),
swift package update
swift package resolve # write Package.resolved without building
exact() is the strictest constraint; upToNextMajor and upToNextMinor give caret-style ranges. Package.resolved records the resolved git SHA per dep. Transitive coercion isn’t a first-class feature — declare the package directly with exact().
Gotcha: SwiftPM’s resolver is slow on large graphs; consider committing Package.resolved to avoid re-resolving in CI.
CocoaPods (Podfile.lock)
# Podfile
pod 'Alamofire', '5.9.1'
pod 'Starscream', '~> 4.0' # coerce transitive by declaring directly
pod update Alamofire
pod install --deployment # frozen install
CocoaPods’ lockfile records the resolved version per pod. Coercion follows the Bundler pattern — declare the transitive directly in the Podfile.
Gotcha: pod install vs pod update semantics — install honours the lockfile, update re-resolves.
Carthage (Cartfile.resolved)
carthage update Alamofire --use-xcframeworks
Carthage is simpler than SwiftPM / CocoaPods — fewer features, less drift. Resolution is direct; coercion is by editing the Cartfile.
Gotcha: Carthage builds frameworks from source; an upgrade can break ABI compatibility with downstream consumers if the platform version is bumped.
Developer gotchas — written for people who live in the code
- Xcode and SwiftPM CLI use different caches. Xcode caches in
~/Library/Developer/Xcode/DerivedData/; CLI uses.build/. After a manifest bump, both need clearing. Package.resolvedlives in two places. In an SwiftPM-only repo it’s at the root. In an Xcode project it’s nested at<project>.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved. Scanners that only look at the root may miss it.- CocoaPods and SwiftPM coexisting in the same project. Some libs are only available on one. Scanners may flag a dep twice (once per resolver). Reconcile in your VEX by stating which resolver’s artefact is actually linked.
- Binary frameworks (
.xcframework) are scanner-opaque. Their contents aren’t visible to source-level tooling. A CVE in a vendored xcframework requires the vendor to ship a fix; you can’t bump it locally. #if canImport(Foo)makes reachability conditional. Compile-time module availability changes which Swift files are included. A CVE in a conditional import path may not be in your shipping binary.- iOS App Store distribution strips dead code. Swift’s
-Ononedebug builds keep more code than-Orelease builds. Reachability via runtime traces against debug builds over-estimates what ships.
Reachability
swift package show-dependencies --format json | jqfor the resolved graph.- The Xcode call graph instrument shows runtime call edges.
- Runtime:
xcodebuild -enableCodeCoverage YES.