Swift Package Manager dependency management for iOS video app development

Swift Package Manager (SPM) is the right module system for a modern video app — video chat, streaming, recording, or live broadcasting. It ships with Xcode, it integrates natively with Swift 6 concurrency boundaries, and it is the only package manager Apple recommends for new iOS code in 2026. Done right, SPM makes a video app cheaper to maintain, faster to build, and easier to reason about across teams. Done wrong, it turns into nested package hell with 15-minute resolve times and brittle CI.

This guide 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 package interfaces, WebRTC and streaming dependency choices, CI pipelines, and the concrete layout of a production-grade Package.swift. If you are building a video conferencing app, replacing a streaming stack, or shipping custom video/audio processing, this is the module architecture to copy.

Key takeaways

SPM is the default for new video iOS apps. Xcode-native, Swift 6 aware, CLI-friendly, and the only manager where Sendable boundaries translate cleanly across modules. CocoaPods stays relevant only for legacy integration.

Split along concurrency boundaries, not feature boundaries. One SPM module per actor domain (Signalling, MediaEngine, CaptureSession, MediaFX, UI) avoids Sendable leaks between modules.

Pin everything; upgrade deliberately. Use Package.resolved in version control, pin to exact commits for ObjC-heavy binaries (WebRTC), and schedule quarterly dependency refreshes rather than drive-by updates.

Binary targets cut build time 10× for WebRTC. Distribute the Google WebRTC iOS framework as an .xcframework binary target, not as source. CI builds drop from tens of minutes to under 60 seconds for clean builds.

Do not over-package. Sub-packages add resolver cost and CI complexity. A 6–10 module structure is typically the sweet spot for a video app; anything beyond 15 modules usually signals accidental fragmentation.

Why Fora Soft wrote this playbook

Fora Soft has been shipping video and audio iOS apps since 2005 — long enough to remember CocoaPods, Carthage, and all three iterations of SPM before it was good. We run SPM in production on BrainCert’s learning platform, VALT enterprise video review, SuperPower FX video effects, and the Smart TV companion apps for bellicon.

This playbook is the module architecture we use daily, refined across migrations from CocoaPods to SPM, from Swift 5 to Swift 6, and from single-module monoliths to properly segmented packages. Agent Engineering (senior engineers supervising Cursor/Claude/Copilot) keeps SPM refactor work inside budget, typically delivering 20–40% below standard agency rates because the AI pair excels at mechanical module migrations.

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.

Book a 30-min call → WhatsApp → Email us →

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, the migration from CocoaPods is worth planning but not rushing; CocoaPods still works and is still maintained at press time.

Dimension SPM CocoaPods Carthage
Xcode integration Native Plugin-based Manual
Swift 6 concurrency Per-module swiftSettings Workarounds only Workarounds only
Binary targets XCFramework Vendored binary Yes
CI performance Good (with cache) Mixed Fast for prebuilt
Private repos SSH / HTTPS native Private specs repo Git URL
New iOS app default Yes Legacy only Rare

Reach for SPM when: you are starting a new iOS project, migrating a legacy app to Swift 6, or consolidating dependency management across several teams. 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 video chat or streaming app. Each module maps to a concurrency domain, minimises public surface area, and makes Sendable boundaries enforceable at compile time.

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, WebRTCBinary)
    CaptureSession/    // AVCaptureSession actor (depends: Core)
    MediaFX/           // Core ML, 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)

The app-level Xcode project depends only on Features; nothing in the UI or glue code has to know about WebRTC or Core ML. Binary targets (WebRTC) live outside Sources/ and are referenced from the package manifest.

Package.swift essentials

Below is the manifest shape we start from. Swift tools version 6.0, per-module swiftSettings, explicit binary target, and Sendable-complete checking everywhere.

// swift-tools-version: 6.0
import PackageDescription

let swift6Settings: [SwiftSetting] = [
  .enableExperimentalFeature("StrictConcurrency"),
  .enableUpcomingFeature("InferSendableFromCaptures"),
]

let package = Package(
  name: "MyVideoApp",
  platforms: [.iOS(.v17)],
  products: [
    .library(name: "Features", targets: ["Features"]),
  ],
  dependencies: [
    .package(url: "https://github.com/google/GoogleWebRTC", exact: "125.6422.06"),
    // pin every external dep to `exact` in production
  ],
  targets: [
    .binaryTarget(
      name: "WebRTC",
      path: "BinaryFrameworks/WebRTC.xcframework"
    ),
    .target(name: "Core", swiftSettings: swift6Settings),
    .target(
      name: "MediaEngine",
      dependencies: ["Core", "WebRTC"],
      swiftSettings: swift6Settings
    ),
    // ...repeat per module
    .testTarget(
      name: "MediaEngineTests",
      dependencies: ["MediaEngine"],
      swiftSettings: swift6Settings
    ),
  ]
)

Reach for binary targets when: a dependency is slow to build from source (WebRTC, FFmpeg, TensorFlow Lite), is distributed as a closed binary, or changes rarely. Compile everything else from source so stacktraces and debugging stay clean.

Sendable-safe module interfaces

Cross-module Sendable hygiene is what separates a fast Swift 6 build from a tangled one. Three rules keep interfaces clean:

1. Public types in Core only. The Core module owns every Sendable value type shared across modules — IDs, errors, stats, state snapshots. No other module exports Sendable types.

2. Actor APIs return Sendable values. When MediaEngine hands state to the UI layer, it returns Core.CallState snapshots, never the internal RTCPeerConnection.

3. sending parameters across module boundaries. Buffers, payloads, and single-use objects pass as sending. The caller documents the ownership transfer; the compiler enforces it.

The Swift Package Manager dependency catalogue for video apps

After ten years of shipping video apps, the dependency set we trust in 2026 is short. Fewer, well-chosen deps beat a long Package.resolved.

1. Google WebRTC. Binary XCFramework. Pin to exact upstream tag. Update quarterly.

2. Apple WebRTC alternatives. LiveKit Swift SDK when you use LiveKit Cloud or a self-hosted LiveKit SFU; the team maintains a first-class SPM target.

3. Starscream or URLSessionWebSocketTask. WebSocket client for signalling. Starscream is battle-tested; URLSession is zero-dependency and works fine if you do not need extensions.

4. Swift Collections and Swift Algorithms. Standard-adjacent collections (OrderedSet, Deque) and lazy algorithms. Zero controversy.

5. swift-log. Structured logging with pluggable backends. One line to swap between console logs in development and an OSLog/remote backend 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 + Vision (no dependency). Everything on-device-AI comes from Apple’s own frameworks in 2026 — no third-party wrapper buys you enough to justify the risk.

Need help pruning your Package.resolved?

We routinely cut SPM dependency trees by 30–60% on production video apps without losing functionality. Send us your manifest; we will come back with a prune list and estimated build-time savings.

Book a 30-min call → WhatsApp → Email us →

CI and build-time caching — what actually speeds builds up

Three levers dominate CI time for an SPM-based video app:

1. Binary WebRTC. Distributing WebRTC as an XCFramework shrinks the clean-build time from 15–25 minutes (source) to under a minute (binary). This single change is the biggest single-day CI win.

2. SPM cache in CI. Cache the .build directory and ~/Library/Caches/org.swift.swiftpm in your CI runner. GitHub Actions, Bitrise, and Xcode Cloud all support this with <15 lines of config.

3. Incremental builds and Explicit Module Builds. Xcode 15+ ships explicit module builds. They speed up incremental builds dramatically on large codebases; enable per-target.

In combination, a well-tuned CI pipeline for a mid-size video app lands test + archive under 10 minutes end-to-end — fast enough to support the multiple-deploys-per-day cadence modern 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 dependency updates on a quarterly schedule rather than as drive-by PRs. The exception is security patches, which jump the queue.

For binaries (WebRTC, FFmpeg, Core ML models), we pin to the exact SHA of the .xcframework zip. Mismatched binaries cause the worst class of production bugs because they rebuild silently on a new CI runner without tests catching the change.

Reach for exact-version pinning when: the dep is part of a shipping production pipeline. 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 with Swift Testing cleanly. Per module test target, @Suite per major flow, parameterized tests for matrix coverage.

For integration and UI tests, keep them at the Xcode project level (XCUITest) rather than inside SPM — SPM tests are still device-less by default and not suitable for UIKit/SwiftUI interaction coverage. Our broader QA approach is documented in the Fora Soft QA process article.

Migrating from CocoaPods to SPM without breaking the build

The safe migration path runs in three phases:

Phase 1 — Dual-resolve. Keep the Podfile. Add Package.swift with the first module (usually Core). Confirm Xcode and CI build both resolvers cleanly.

Phase 2 — Move one dep at a time. Move each CocoaPod to SPM one release cycle at a time, prioritising pure-Swift deps first. Objective-C deps (WebRTC, AVFoundation helpers) usually require XCFramework packaging; schedule them last.

Phase 3 — Delete Podfile. Only when the last pod is gone and a full release has shipped clean under SPM only. Do not rush this step; a live CocoaPods fallback is cheap insurance during migration.

Security, supply chain, and the packages you actually audit

Every SPM dependency is code you ship. For a video app handling E2EE, user media, or HIPAA-regulated content, treat the dep tree like a security asset.

1. Review every dep before adding. License, maintainer, last commit, open security advisories. If any flag fails, find an alternative or vendor the code you need.

2. Sign and verify binary targets. SPM supports signed XCFramework checksums; use them for every binary dep. A silent swap on your binary hosting is a supply-chain attack.

3. Monitor CVEs. Subscribe to the CVE feed for every dep, or automate with a Dependabot / Renovate config that opens PRs against your Package.swift.

Reach for a vendored internal dep 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 numbers for production video apps, scaled by codebase size. Your mileage varies with dependency count and Objective-C bridging.

1. Small iOS video app (<50k lines, 5–8 deps). CocoaPods → SPM migration: 1–2 engineer-weeks. Splitting into modules: another 1–2.

2. Mid-size app (50k–200k lines, 10–20 deps). Migration: 3–5 weeks. Full module split along concurrency domains: another 4–6 weeks.

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 coincides with a Swift 6 migration.

4. Binary target packaging for an in-house SDK. 1–2 weeks per SDK for the initial XCFramework and release pipeline. Quarterly refreshes after that are a day of work.

Agent Engineering shortens each of these by 20–40% on well-scoped migrations. For an honest estimate 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, single-module monolith, 20-minute clean CI builds, and rising Swift 6 migration pain. Goal: SPM-native, module split along concurrency domains, sub-10-minute clean CI, no release-cadence disruption.

12-week plan. Weeks 1–3: Package.swift scaffolding, Core module extracted, dual-resolve with existing Podfile. Weeks 4–7: per-release migration of pure-Swift pods (7 deps moved). Weeks 8–9: WebRTC repackaged as XCFramework; MediaEngine, CaptureSession, 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 build dropped from ~20 minutes to under 8. The new module boundaries caught three latent concurrency issues during the Swift 6 flip that had previously shipped undetected. Feature cycle time improved because parallel work on MediaEngine and Features no longer stepped on each other.

A decision framework — plan your SPM setup in five questions

1. Greenfield or legacy? Greenfield: SPM from day one. Legacy: plan the migration in two or three releases rather than one big-bang sprint.

2. Swift 6 or still Swift 5? SPM modules per concurrency domain pay off most with Swift 6. If you are on Swift 5.10 with complete checking, you already get most of the benefit.

3. WebRTC binary or source? Binary for CI speed; source only when you need a custom patch or are on a non-LTS WebRTC branch.

4. Private or open dep? Open deps go into the main Package.swift. Private deps belong in a separate private repo with SSH access configured in CI.

5. Who owns dependency upgrades? Assign one engineer per quarter to the “dep janitor” role: review outstanding updates, run the changelog, land the upgrade PR. Rotates every quarter.

Pitfalls to avoid

1. One module per feature. Feature-based modules sound tidy but 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 absolutely need a custom branch, use the binary XCFramework. The day-to-day CI win is enormous.

3. .upToNextMajor on everything. Open-range versions in production guarantee a surprise mid-release. Pin exactly; upgrade deliberately.

4. Mixing SPM with CocoaPods permanently. Dual-resolve is fine during migration. If you are still running both a year later, the Podfile has quietly become a dependency graveyard.

5. Ignoring package cache in CI. Without a cache, every PR rebuilds every dep from scratch. Fifteen lines of CI config is the cheapest performance win in the stack.

KPIs — what to measure after an SPM refactor

Quality KPIs. Number of @unchecked Sendable occurrences (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 module split, CI green-to-deploy latency, and onboarding time for new engineers (should fall as the module graph becomes legible).

Reliability KPIs. Clean CI build time, incremental CI build time, and “why did my build fail” frequency 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 this quarter. Hold off if your team is mid-migration to Swift 6 or SwiftUI (do one migration at a time), if your product is inside a freeze window for compliance or App Store review, or if your release cadence is already under pressure and your CI is barely green.

In those cases, plan SPM refactor as the first project of the next quarter. It pays off fastest when you start it from a stable base, not in the middle of another migration.

Ready to refactor your video app around Swift Package Manager?

We have migrated production iOS video apps from CocoaPods monoliths to Swift 6 + SPM module structures. Send us the repo and your target release. We’ll come back with a staged plan.

Book a 30-min call → WhatsApp → Email us →

FAQ

Is Swift Package Manager production-ready for video apps?

Yes. SPM has been Xcode-native since Xcode 11, stable for Swift 5+, and in Swift 6 it is the only manager that tracks concurrency boundaries per module. We use it in production across multiple shipped video apps.

How many modules is too many?

For a typical video app, 6–10 modules is the sweet spot. Beyond 15 modules you usually see diminishing returns and higher CI overhead. Split along concurrency or architectural layers, not feature boundaries.

Can SPM replace CocoaPods for legacy Objective-C code?

Yes, but the work is heavier. Objective-C deps need XCFramework packaging or careful module-map work. For a big ObjC surface, plan the migration across two or three releases rather than a single sprint.

How do you manage private SPM packages?

Host in a private Git repo, use SSH or HTTPS with a 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 Package.swift.

Do I need to ship the Google WebRTC binary myself?

Google publishes an official XCFramework. Mirror it to your own storage (S3 or GCS) and reference that URL from your Package.swift so your build survives upstream changes. Always pin to exact SHA.

How does SPM affect build time for large video apps?

With binary WebRTC + CI cache + explicit module builds, a mid-size video app typically lands clean builds inside 10 minutes and incremental PR builds under 3. A monolithic Xcode project with CocoaPods often exceeds those targets by 2–3×.

What does a Fora Soft SPM engagement look like?

Typically a 2–3 week discovery to audit the repo, propose the module layout, and pick the migration order. Then a staffed sprint team ships the refactor across two or three of your normal release cycles. We often pair with clients as a dedicated development team.

Is SPM a good fit for Smart TV or tvOS video apps?

Yes. SPM targets support tvOS as a platform, and the same module layout translates from iOS to tvOS with minor adjustments (input handling, focus engine). We use it in production on Smart TV companion apps like the bellicon Smart TV product.

Language

Swift 6 Explained

Language-level pairing — concurrency, Sendable, sending, typed throws.

Video

Swift 6 iOS Development for Video Chat

The applied version of SPM + Swift 6 for a video chat iOS app.

UI

SwiftUI Video Conferencing vs UIKit

Where SwiftUI wins, 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.

Context

Summer 2025 Tech Digest

iOS 26, Swift on Android, GPT-5 — the release context around Swift 6 and SPM.

Ready to make Swift Package Manager work for your video app?

SPM is the default module system for modern iOS video development: Xcode-native, Swift 6 aware, CI-friendly, and the only manager where concurrency boundaries translate cleanly across modules. Split along concurrency domains, pin dependencies exactly, distribute WebRTC as a binary target, cache CI, and you end up with a project that compiles fast, reviews cleanly, and scales across teams.

If you are scoping a greenfield video app or migrating a CocoaPods monolith to SPM + Swift 6, 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 dep count, CI time, and Swift 6 status. We’ll return a staged refactor plan and a defendable estimate.

Book a 30-min call → WhatsApp → Email us →

  • Technologies