
Swift Package Manager for video apps is the module system to build on in 2026, whether you ship video chat, live streaming, recording, or broadcasting. SPM comes with Xcode, it tracks Swift 6.2 concurrency boundaries per module, and it is the only dependency manager Apple recommends for new iOS code. The timing matters: the CocoaPods trunk registry goes read-only on December 2, 2026, so a video app still leaning on Pods is now on a clock. Done right, SPM makes a video app cheaper to maintain and faster to build. Done wrong, it becomes nested-package hell with 15-minute resolves and flaky CI.
This is the playbook we use at Fora Soft to structure Swift Package Manager around video apps that actually ship: module boundaries, dependency pinning, Sendable-safe interfaces under Swift 6.2, the correct WebRTC binary target, package traits for optional features, CI pipelines, and a production-grade Package.swift. If you are building a video conferencing app, replacing a streaming stack, or shipping custom video and audio processing, this is the module architecture to copy.
Key takeaways
• SPM is the default, and CocoaPods is on a deadline. The Pods trunk registry turns read-only on December 2, 2026. For any new or actively maintained iOS video app, SPM is the manager to standardize on now.
• Split modules along concurrency domains, not features. One module per actor domain (Core, Signalling, MediaEngine, CaptureSession, MediaFX, CallFlow, UI) keeps Swift 6.2 Sendable boundaries enforceable at compile time.
• Ship WebRTC as a binary target. Use the maintained stasel/WebRTC xcframework, not the unmaintained google/GoogleWebRTC pod. Linking a prebuilt binary cuts clean-build time from tens of minutes to seconds.
• Pin exactly; refresh on a schedule. Commit Package.resolved, pin every external dependency to an exact version or checksum, and run a quarterly dependency review instead of drive-by upgrades.
• Do not over-package. Six to ten modules is the sweet spot for a video app. Package traits (Swift 6.1+) gate optional features like screen share or AI filters without spawning a dozen extra packages.
Why Fora Soft wrote this playbook
Fora Soft has shipped video and audio apps since 2005, across 250+ projects and 20 years of iOS releases. That is long enough to remember CocoaPods and Carthage, and every iteration of SPM before it was good. We run Swift Package Manager in production on BrainCert’s learning platform, VALT enterprise video review (770+ US organizations, 50,000+ users, HIPAA scope), SuperPower FX real-time mobile video effects, and the bellicon Smart TV companion apps.
The architecture below is what we use daily, refined across CocoaPods-to-SPM migrations, the Swift 5 to Swift 6 jump, and the split from single-module monoliths into properly segmented packages. Our Agent Engineering model (senior engineers supervising coding agents) keeps this mechanical refactor work inside budget, because module migration is exactly the kind of repetitive, well-specified change an AI pair does fast under review.
Stuck on a tangled Swift Package Manager setup?
Slow builds, dependency hell, Sendable leaks across modules? Send us your Package.swift and a rough module list. We’ll return a clean layout plan and a tight estimate.
SPM vs CocoaPods vs Carthage — what to pick in 2026
For a greenfield video app, SPM is the only answer worth considering. For a legacy app, plan the CocoaPods migration deliberately, but do not treat it as optional anymore. CocoaPods is in maintenance mode, and its trunk registry becomes read-only on December 2, 2026: existing builds keep resolving, but no new pod versions publish after that date. The wider ecosystem has already turned. Flutter 3.44 made SwiftPM its default iOS dependency manager, and 61% of the top-100 iOS plugins had migrated by early 2026.
| Dimension | SPM | CocoaPods | Carthage |
|---|---|---|---|
| Xcode integration | Native since Xcode 11 | Workspace plugin | Manual |
| Swift 6.2 concurrency | Per-module swiftSettings |
Workarounds only | Workarounds only |
| Binary targets | XCFramework, signed checksum | Vendored binary | Prebuilt binary |
| Conditional deps (traits) | Package traits (6.1+) | Subspecs | None |
| Registry status (2026) | Active, Apple-supported | Read-only from Dec 2, 2026 | Community, low activity |
| New iOS video app default | Yes | Legacy only | Rare |
Reach for SPM when: you are starting a new iOS project, migrating a legacy app to Swift 6.2, or consolidating dependency management across teams. With the Pods registry going read-only, there is no credible reason left to pick CocoaPods for greenfield work.
Module layout for a production video app
The structure below is what we ship for a Swift 6.2 video chat or streaming app. Each module maps to a concurrency domain, keeps its public surface small, and makes Sendable boundaries enforceable at compile time. Read the graph bottom-up: Core has zero dependencies, and the app target sees only Features.

Figure 1. Dependency graph for a nine-module video app. Arrows point from a module to what it depends on; the WebRTC binary sits outside the source tree.
MyVideoApp/
Package.swift
Sources/
Core/ // Sendable types, errors, logging (zero deps)
Signalling/ // WebSocket + protocol (depends: Core)
Networking/ // REST / auth (depends: Core)
MediaEngine/ // WebRTC wrapper actor (depends: Core, WebRTC)
CaptureSession/ // AVCaptureSession actor (depends: Core)
MediaFX/ // Core ML, Vision filters (depends: Core)
CallFlow/ // orchestration actor (depends: all above)
UIKitVideoView/ // UIViewRepresentable + Metal (depends: Core)
Features/ // SwiftUI feature slices (depends: CallFlow, UIKitVideoView)
BinaryFrameworks/
WebRTC.xcframework
Tests/
...
App/ // the Xcode project (depends on Features)
Nothing in the UI or glue code has to know about WebRTC or Core ML. The binary target lives outside Sources/ and is referenced from the manifest. When two engineers work on MediaEngine and Features in the same sprint, the module boundary keeps them from stepping on each other.
Package.swift essentials for Swift 6.2
Here is the manifest shape we start from. Tools version 6.2 turns on approachable concurrency, so each target opts into a default isolation instead of scattering @MainActor annotations by hand. WebRTC comes in as a remote binary from the maintained stasel/WebRTC distribution, pinned to an exact milestone.
// swift-tools-version: 6.2
import PackageDescription
let concurrency: [SwiftSetting] = [
.defaultIsolation(MainActor.self), // SE-0466: Main-actor by default
.enableUpcomingFeature("NonisolatedNonsendingByDefault"), // SE-0461
.enableUpcomingFeature("InferIsolatedConformances"), // SE-0470
]
let package = Package(
name: "MyVideoApp",
platforms: [.iOS(.v17), .tvOS(.v17)],
products: [
.library(name: "Features", targets: ["Features"]),
],
dependencies: [
// maintained WebRTC xcframework, pinned to an exact milestone
.package(url: "https://github.com/stasel/WebRTC", exact: "150.0.0"),
],
targets: [
.target(name: "Core", swiftSettings: concurrency),
.target(
name: "MediaEngine",
dependencies: ["Core", .product(name: "WebRTC", package: "WebRTC")],
swiftSettings: concurrency
),
// ...repeat per module
.testTarget(
name: "MediaEngineTests",
dependencies: ["MediaEngine"],
swiftSettings: concurrency
),
]
)
If you self-host the binary, swap the remote package for a local .binaryTarget with a url and checksum. The checksum is not optional in production: it is what stops a silent binary swap from reaching your users.
Reach for binary targets when: a dependency is slow to build from source (WebRTC, FFmpeg, TensorFlow Lite) or ships as a closed binary. Compile everything else from source so stack traces and debugging stay clean.
Swift 6.2 concurrency and Sendable-safe module interfaces
Swift 6.2 changed the default posture. With .defaultIsolation(MainActor.self), a target’s code runs on the main actor unless you say otherwise, which removes a wall of false-positive data-race warnings in UI-adjacent modules. A video pipeline still needs real off-main work (capture, encode, network), so the discipline is about where you deliberately step off the main actor. Three rules keep cross-module interfaces clean.
1. Public Sendable types live in Core only. The Core module owns every value type shared across modules: IDs, errors, stats, state snapshots. No other module exports Sendable types, so there is one place to reason about thread safety.
2. Actor APIs return Sendable values, never live objects. When MediaEngine hands state to the UI, it returns a Core.CallState snapshot, never the internal RTCPeerConnection. The peer connection stays behind the actor.
3. Transfer buffers with sending. Video frames, audio payloads, and other single-use objects cross module boundaries as sending parameters (SE-0430). The caller gives up ownership, the compiler enforces it, and you avoid a needless copy of a 1080p frame.
Package traits for optional video features
Package traits (SE-0450, shipped in Swift 6.1) are the feature that most SPM guides still miss, and they fit video apps well. A trait lets one package expose optional features and optional dependencies that a consumer turns on by name, with compile-time gating through #if. Instead of a separate package for every add-on, you keep one module and switch capabilities per build.
Concretely: a ScreenShare trait that pulls in a ReplayKit helper only when enabled, an AIFilters trait that links a heavier Core ML model pack, or a TVOS trait that trims capture code the TV build never runs. Consumers pass --traits AIFilters, or set defaults in the manifest, and CI can matrix over trait combinations so every variant stays green.
// in the video package manifest
traits: [
"ScreenShare",
.trait(name: "AIFilters"),
.default(enabledTraits: ["ScreenShare"]),
],
// in code
#if AIFilters
import CoreML
public func attachBackgroundBlur() { /* ... */ }
#endif
Reach for traits when: a feature is heavy, optional, or platform-specific (screen share, on-device AI, tvOS). Keep it a plain module when every build ships it anyway. Traits earn their keep by shrinking builds that do not need the feature.
Need help pruning your Package.resolved?
We routinely cut SPM dependency trees on production video apps without losing functionality. Send us your manifest; we’ll come back with a prune list and estimated build-time savings.
The SPM dependency catalogue for video apps
After two decades of video work, the dependency set we trust in 2026 is short. Fewer, well-chosen packages beat a long Package.resolved.
1. WebRTC. Use stasel/WebRTC, a community-maintained binary xcframework whose releases track Chromium milestones (recent tags run M137 to M150, latest 150.0.0). The old google/GoogleWebRTC was a CocoaPods pod and is no longer maintained. Pin to an exact milestone and update quarterly.
2. LiveKit Swift SDK. If you run on LiveKit Cloud or a self-hosted LiveKit SFU, its Swift SDK is a first-class SPM target, and LiveKit also publishes its own webrtc-xcframework if you want their build.
3. Starscream or URLSessionWebSocketTask. WebSocket client for signalling. Starscream is battle-tested; URLSessionWebSocketTask is zero-dependency and fine when you do not need custom framing.
4. swift-collections and swift-algorithms. Apple’s own standard-adjacent packages: OrderedSet and Deque for jitter buffers and participant lists, lazy algorithms for stats. Zero controversy.
5. swift-log. Structured logging with pluggable backends. One line swaps console logs in development for OSLog or a remote sink in production.
6. Kingfisher or SDWebImage. Only if you need heavy image caching (avatars at scale). For simple cases, AsyncImage plus a small cache is enough.
7. Core ML and Vision (no dependency). On-device video effects like background blur and segmentation come from Apple’s own frameworks. They build on the same fundamentals we cover in our video encoding primer, and no third-party wrapper buys enough to justify the supply-chain risk.
WebRTC: binary target vs building from source
This is the single biggest build-time decision in a WebRTC app. Compiling WebRTC from source is a large native build measured in tens of minutes on a clean CI runner. Linking a prebuilt .xcframework is a link step measured in seconds. For day-to-day CI, the binary wins by a wide margin.

Figure 2. Clean-build time on a typical CI runner: compiling WebRTC from source versus linking a prebuilt binary target. Directional, from our own pipelines; your numbers move with runner class and cache state.
Build WebRTC from source only when you genuinely need it: a custom patch, a non-standard branch, or a field-of-view change that upstream will not take. In every other case, the binary is the right default, and pinning it to an exact milestone keeps builds reproducible.
CI and build-time caching — what actually speeds builds up
Three levers dominate CI time for an SPM-based video app, in order of impact.
1. Binary WebRTC. Covered above, and it is the biggest single-day win. Moving WebRTC from source to a binary target is usually worth more than every other CI tweak combined.
2. Cache the SPM artefacts. Cache the .build directory and ~/Library/Caches/org.swift.swiftpm in your runner. GitHub Actions, Bitrise, and Xcode Cloud all support this in under 15 lines of config, so a PR does not re-resolve and rebuild every dependency from scratch.
3. Explicitly built modules, measured not assumed. Xcode 16 turns on explicit modules by default for C and Objective-C; for Swift you opt in. Apple pitches better parallelism and faster debugging, but early benchmarks were mixed, and the payoff depends on your module graph. Enable it per target, measure clean and incremental build time before and after, and keep it only where the numbers move.
Combined, a well-tuned pipeline for a mid-size video app lands test plus archive in single-digit minutes: fast enough to support the multiple-deploys-per-day cadence product teams want.
Versioning, pinning, and quarterly dependency refresh
For production video apps we pin every external dependency to an exact version, commit Package.resolved, and review updates on a quarterly schedule rather than as drive-by PRs. Security patches jump the queue; everything else waits for the review.
For binaries (WebRTC, FFmpeg, Core ML model packs), pin to the exact checksum of the .xcframework. Mismatched binaries cause the worst class of production bug, because they can change on a new CI runner without a single test noticing.
Reach for exact-version pinning when: the dependency is on a shipping production path. Relax to .upToNextMinor only for pure-Swift internal utilities where breaking changes are rare and low-impact.
Testing a Swift Package Manager video app
SPM’s native testTarget pairs cleanly with Swift Testing: one test target per module, @Suite per major flow, and parameterized tests for matrix coverage across codecs or network conditions.
Keep integration and UI tests at the Xcode project level with XCUITest, not inside SPM. Package tests run without a device by default and are not the place for UIKit or SwiftUI interaction coverage. Our broader approach is in the Fora Soft QA process writeup.
Migrating from CocoaPods to SPM without breaking the build
With the Pods registry going read-only in December 2026, the safe migration path is worth starting early. It runs in three phases, and a live CocoaPods fallback stays in place until the last one lands.

Figure 3. The dual-resolve migration path. CocoaPods and SPM coexist through phases 1 and 2; the Podfile is deleted only after a full release ships clean on SPM alone.
Phase 1 — Dual-resolve. Keep the Podfile. Add Package.swift with the first module, usually Core. Confirm Xcode and CI build both resolvers cleanly before touching anything else.
Phase 2 — Move one dependency at a time. Migrate each pod to SPM one release cycle at a time, pure-Swift dependencies first. Objective-C dependencies (WebRTC, AVFoundation helpers) usually need XCFramework packaging, so schedule them last.
Phase 3 — Delete the Podfile. Only when the last pod is gone and a full release has shipped clean under SPM. Do not rush this: keeping a working Pods fallback during migration is cheap insurance.
Security, supply chain, and the packages you actually audit
Every SPM dependency is code you ship. For a video app handling end-to-end encryption, user media, or HIPAA-regulated content, treat the dependency tree as a security asset, not a convenience.
1. Review every dependency before adding it. License, maintainer, last commit, open advisories. If any flag fails, find an alternative or vendor the slice you need.
2. Verify binary checksums. SPM records a checksum for every binary target; keep it pinned. A silent swap on your binary host is a supply-chain attack, and the checksum is the tripwire.
3. Monitor advisories. Automate with Dependabot or Renovate against your Package.swift so a new CVE opens a PR instead of waiting for someone to notice.
Reach for a vendored internal dependency when: the upstream is abandoned, a critical CVE is unpatched, or your compliance scope (HIPAA, SOC 2) requires an auditable fork you own.
Cost math — SPM refactor budgets
Rough effort for production video apps, scaled by codebase size. Your numbers move with dependency count and Objective-C bridging, so treat these as planning anchors, not quotes.

Figure 4. Refactor effort in engineer-weeks by app size, split into CocoaPods-to-SPM migration and the module reorganization. Agent Engineering typically trims each bar.
1. Small app (<50k lines, 5–8 deps). Migration: 1–2 engineer-weeks. Module split: another 1–2.
2. Mid-size app (50k–200k lines, 10–20 deps). Migration: 3–5 weeks. Full split along concurrency domains: another 4–6.
3. Large app (200k+ lines, 25+ deps, heavy ObjC). Migration: 8–12 weeks across two releases. Module work is a larger sub-project (12–20 weeks) that usually rides along with a Swift 6 migration.
Worked example. Take the mid-size migration at 4 engineer-weeks. At 40 hours a week that is 160 hours. At an illustrative mid-market blended rate of $50 to $85 per hour, the arithmetic lands at roughly $8,000 to $13,600 for the migration, with the module split a similar block on top. Agent Engineering usually keeps us at the low end of that range or below, because the mechanical parts move fast under supervision. For a real number against your repo, book a 30-minute call.
Mini case — SPM refactor on an enterprise video app
Situation. An enterprise video app with 20+ CocoaPods, a single-module monolith, 20-minute clean CI builds, and mounting Swift 6 migration pain. The goal: SPM-native, modules split along concurrency domains, sub-10-minute clean CI, and no disruption to release cadence.
12-week plan. Weeks 1–3: Package.swift scaffolding, Core extracted, dual-resolve with the existing Podfile. Weeks 4–7: per-release migration of pure-Swift pods (7 moved). Weeks 8–9: WebRTC repackaged as a binary target; MediaEngine, CaptureSession, and Signalling split out. Weeks 10–11: Podfile deleted, CI cache tuned, Swift 6 concurrency enabled per module. Week 12: rollout and retro.
Outcome. Clean CI dropped from about 20 minutes to under 8. The new boundaries surfaced three latent concurrency bugs during the Swift 6 flip that had shipped undetected for months. Feature cycle time improved because parallel work on MediaEngine and Features stopped colliding. Want a similar assessment? Grab a slot.
A decision framework — plan your SPM setup in five questions
Five questions decide most of the setup. Answer them before you write a line of the manifest.

Figure 5. A five-question path from repo state to an SPM plan: greenfield or legacy, concurrency mode, WebRTC packaging, private dependencies, and upgrade ownership.
1. Greenfield or legacy? Greenfield: SPM from day one. Legacy: plan the migration across two or three releases, not one big-bang sprint.
2. Swift 6.2 or still Swift 5? Modules per concurrency domain pay off most with Swift 6.2 default isolation. On Swift 5.10 with complete checking you already get much of the benefit.
3. WebRTC binary or source? Binary for CI speed, source only when you need a custom patch or a non-standard branch.
4. Private or open dependencies? Open dependencies go in the main manifest. Private ones belong in a separate repo with SSH access configured in CI.
5. Who owns upgrades? Assign one engineer per quarter as the dependency owner: review outstanding updates, read the changelogs, land the upgrade PR. Rotate the role so no one hoards the context.
Pitfalls to avoid
1. One module per feature. Feature modules sound tidy and usually produce circular dependency graphs. Split along concurrency or layer boundaries; features are directories inside those modules.
2. Compiling WebRTC from source in CI. Unless you truly need a custom branch, use the binary xcframework. The day-to-day CI saving is large and compounding.
3. .upToNextMajor on everything. Open version ranges in production guarantee a surprise mid-release. Pin exactly, upgrade on purpose.
4. Living on dual-resolve forever. CocoaPods plus SPM is fine during migration. If both are still running a year later, the Podfile has quietly become a dependency graveyard, and now it also has a 2026 deadline.
5. No package cache in CI. Without it, every PR re-resolves and rebuilds every dependency. Fifteen lines of config is the cheapest performance win in the stack.
KPIs — what to measure after an SPM refactor
Quality KPIs. Count of @unchecked Sendable escapes (lower is better), cross-module Swift 6 warnings in CI (target zero), and dependency count on the critical path.
Business KPIs. Feature cycle time before and after the split, CI green-to-deploy latency, and onboarding time for new engineers, which should fall as the module graph becomes legible.
Reliability KPIs. Clean CI build time, incremental build time, and how often the team asks “why did my build fail” in chat. All three should drop measurably after a proper SPM setup.
When not to refactor to SPM this quarter
SPM is the right destination for most iOS video apps, but not always right now. Hold off if your team is mid-migration to Swift 6 or SwiftUI (do one migration at a time), if you are inside a freeze window for compliance or App Store review, or if your release cadence is already under pressure and CI is barely green.
In those cases, plan the SPM refactor as the first project of next quarter, well before the December 2026 Pods deadline. It pays off fastest from a stable base, not in the middle of another migration.
Ready to refactor your video app around Swift Package Manager?
We have moved production iOS video apps from CocoaPods monoliths to Swift 6.2 and SPM module structures. Send us the repo and your target release; we’ll come back with a staged plan.
FAQ
Is Swift Package Manager production-ready for video apps?
Yes. SPM has been Xcode-native since Xcode 11 and is Apple’s recommended manager for new Swift code. Under Swift 6.2 it is the only manager that tracks concurrency isolation per module. We run it in production across multiple shipped video apps.
Which WebRTC package should you use with SPM?
Use the community-maintained stasel/WebRTC binary xcframework, pinned to an exact Chromium milestone (recent tags run M137 to M150, latest 150.0.0). The old google/GoogleWebRTC pod is no longer maintained. If you run LiveKit, its Swift SDK and webrtc-xcframework are good options too.
When does CocoaPods stop working?
CocoaPods is in maintenance mode, and its trunk registry becomes read-only on December 2, 2026. Existing builds keep resolving after that, but no new pod versions publish. For any actively maintained video app, plan the SPM migration before then.
How many modules is too many?
For a typical video app, six to ten modules is the sweet spot. Beyond fifteen you usually see diminishing returns and higher CI overhead. Split along concurrency or architectural layers, and use package traits for optional features instead of spawning more packages.
What are package traits, and do I need them?
Traits (Swift 6.1+) let one package expose optional features and dependencies that consumers enable by name, gated with #if. For video apps they are handy for optional screen share, on-device AI filters, or a tvOS build. You do not need them for a small app, but they keep a growing one from fragmenting into many packages.
How do you manage private SPM packages?
Host them in a private Git repo, use SSH or an HTTPS token, and configure CI credentials. No central registry is required. For larger orgs, a separate shared-packages repo keeps internal SDKs discoverable without polluting every app’s manifest.
How does SPM affect build time for large video apps?
With binary WebRTC, an SPM artefact cache in CI, and explicit modules where they help, a mid-size video app typically lands clean builds inside 10 minutes and incremental PR builds under 3. A CocoaPods monolith often exceeds those by two to three times.
Is SPM a good fit for tvOS or Smart TV video apps?
Yes. SPM targets support tvOS, and the same module layout carries from iOS to tvOS with minor changes to input and focus handling. A package trait can trim capture code the TV build never runs. We use it in production on Smart TV apps like bellicon Smart TV.
What to read next
Language
Swift 6 Explained
Concurrency, Sendable, sending, typed throws — the language layer under this playbook.
Video
Swift 6 iOS Development for Video Chat
The applied version of SPM plus Swift 6 for a video chat iOS app.
UI
SwiftUI Video Conferencing vs UIKit
Where SwiftUI wins and where UIKit still earns its keep in a video app.
Interop
SIP Integration for Video Conferencing
When PSTN and legacy SIP endpoints meet your modern SPM-structured SFU.
Services
Dedicated Development Team
Staff a team that has shipped this SPM structure in production video apps.
Ready to make Swift Package Manager work for your video app?
SPM is the default module system for modern iOS video: Xcode-native, Swift 6.2 aware, CI-friendly, and the one manager where concurrency isolation travels cleanly across modules. Split along concurrency domains, pin dependencies exactly, ship WebRTC as a binary target, gate optional features with traits, and cache CI. You end up with a project that compiles fast, reviews cleanly, and scales across teams, well before the December 2026 CocoaPods deadline.
If you are scoping a greenfield video app or migrating a CocoaPods monolith to SPM and Swift 6.2, the playbook above is the one we use every day. When you want engineers who have already shipped this structure in production, we are a 30-minute call away.
Want the SPM playbook applied to your repo?
Tell us your dependency count, CI time, and Swift 6 status. We’ll return a staged refactor plan and a defendable estimate.

