5 min read

Removing Swift Package Manager (SPM) from a Flutter iOS Project: Lessons Learned

A deep dive into migrating a Flutter iOS project from Swift Package Manager back to CocoaPods, including debugging, Podspec analysis, and dependency cleanup.

#Flutter#Dev-tools#Learning-log

Table of contents

  1. Context: What I’m Building
  2. What I Learned / Architecture Decisions
  3. Key Concepts Explained
  4. 🧩 CocoaPods vs Swift Package Manager
  5. ⚙️ What Happens Behind the Scenes
  6. Code Snippets & Scenarios
  7. 🔍 Removing SPM from Xcode
  8. 🧱 CocoaPods Podfile Fix
  9. 🧠 Debugging the Missing Frameworks
  10. What Worked, What Didn’t
  11. What I’d Do Differently
  12. Real-World Use Case
  13. References / Further Reading

Context: What I’m Building

As part of my ongoing work on a Flutter SDK that wraps a native iOS SDK (ComplyCube), I’ve had to navigate multiple dependency management systems on iOS — namely CocoaPods and Swift Package Manager (SPM). While SPM integration initially simplified setup, it later caused build inconsistencies when the plugin and host app fell out of sync.

This post documents how I reverted my Flutter iOS project from SPM back to CocoaPods, the technical challenges I faced, and what I learned along the way.


What I Learned / Architecture Decisions

The original setup worked like this:

  1. The Flutter plugin used CocoaPods to include the ComplyCube iOS SDK.
  2. The host Flutter app also relied on CocoaPods.

Later, I migrated both to use SPM, which Xcode handles natively. However, when reverting to CocoaPods:

  • The Flutter plugin successfully returned to Pods.
  • The host app, however, still attempted to fetch dependencies from SPM, leading to confusing build errors.

This highlighted an important architectural insight:

When mixing Flutter’s plugin architecture with iOS dependency managers, consistency is everything. The plugin and app must use the same dependency manager, or Xcode will attempt to resolve both.


Key Concepts Explained

🧩 CocoaPods vs Swift Package Manager

  • CocoaPods: A Ruby-based dependency manager that generates a workspace (.xcworkspace) and integrates Pods through a Podfile. It’s the default for Flutter plugins.
  • SPM: Apple’s native dependency manager integrated into Xcode. It uses Package.swift, Package.resolved, and .swiftpm metadata for automatic fetching.

The catch: SPM and CocoaPods don’t share dependency resolution metadata. If one remains in your workspace, Xcode will still attempt to fetch it.

⚙️ What Happens Behind the Scenes

When building a Flutter app for iOS:

  • Flutter runs pod install inside ios/ to configure Pods.
  • The generated Runner.xcworkspace links plugin frameworks through CocoaPods.
  • If .swiftpm or Package.resolved exists, Xcode still checks for SPM packages and may attempt to fetch or build them.

This explains why my build logs still showed lines like:

Fetching Swift packages…
Updating Package.resolved…

even though I thought I’d reverted to CocoaPods.


Code Snippets & Scenarios

🔍 Removing SPM from Xcode

To fully remove SPM dependencies:

# Step 1: Delete SPM metadata
rm -rf ios/.swiftpm
rm -f ios/Package.resolved

# Step 2: Clean Xcode build cache
rm -rf ~/Library/Developer/Xcode/DerivedData

# Step 3: Reinstall CocoaPods
cd ios
pod deintegrate
pod repo update
pod install
cd ..
flutter clean
flutter pub get

Then reopen the workspace:

open ios/Runner.xcworkspace

Finally, remove any remaining entries under Package Dependencies in Xcode’s Project Navigator.


🧱 CocoaPods Podfile Fix

When reverting, I discovered that my Podfile needed proper framework linkage to handle Swift modules. Here’s the working configuration:

platform :ios, '13.0'

use_frameworks! :linkage => :dynamic
use_modular_headers!

target 'Runner' do
  flutter_application_path = '../'
  eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)

end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
    target.build_configurations.each do |config|
      config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES'
    end
  end
end

This ensures that all Swift frameworks are linked dynamically and are exportable across binary boundaries — critical when your plugin uses multiple nested frameworks.


🧠 Debugging the Missing Frameworks

After reverting to Pods, I ran into this error:

Unable to find module dependency: 'ComplyCubeAppDesign'
import ComplyCubeAppDesign

The issue: the ComplyCubeMobileSDK.xcframework referenced four internal Swift modules that weren’t shipped inside the Pod.

This led to a deeper discovery: SPM automatically resolves transitive Swift dependencies, but CocoaPods does not — every framework must be declared explicitly.


What Worked, What Didn’t

Worked:

  • Removing .swiftpm and Package.resolved stopped SPM fetches.
  • Reintegrating CocoaPods rebuilt a clean dependency graph.
  • Using use_frameworks! and BUILD_LIBRARY_FOR_DISTRIBUTION fixed most Swift import issues.

Didn’t Work:

  • The ComplyCube Podspec only included the umbrella binary (ComplyCubeMobileSDK.xcframework), not the transitive frameworks (AppDesign, AppTheme, etc.).
  • Xcode still showed gray “Package Dependencies” because of cached workspace metadata.

What I’d Do Differently

If I were designing an SDK or plugin again:

  1. Keep dependency management consistent — use either SPM or CocoaPods across both the SDK and the app. Mixed setups are fragile.

  2. Ship all frameworks explicitly — list all .xcframeworks in the Podspec, for example:

    spec.vendored_frameworks = [
      'Frameworks/ComplyCubeMobileSDK.xcframework',
      'Frameworks/ComplyCubeAppDesign.xcframework',
      'Frameworks/ComplyCubeAppTheme.xcframework',
      'Frameworks/ComplyCubeCommon.xcframework',
      'Frameworks/ComplyCubeNetworking.xcframework'
    ]
  3. Validate Podspecs using pod lib lint before publishing.

  4. Version alignment — ensure Podspec versions match Git tags and actual binary releases.

  5. Document both paths (SPM and CocoaPods) clearly in SDK README to avoid confusion for integrators.


Real-World Use Case

In a large app ecosystem, teams often manage SDKs that need to support multiple package managers (for example, open-source SDKs distributed both via SPM and CocoaPods). This story reflects a real-world friction point:

When SDK authors rely on SPM transitivity, downstream users relying on CocoaPods will experience missing module issues unless the Podspec explicitly lists all binaries.

Understanding this distinction helps teams avoid “phantom” Swift import errors and provides smoother CI/CD pipelines.


References / Further Reading


Summary: Migrating between CocoaPods and SPM in Flutter projects is not just a tooling exercise — it’s a dependency architecture problem. This experience taught me how dependency managers interact with Flutter’s plugin model, why transitive modules must be explicitly defined for CocoaPods, and how to maintain a clean build system across iOS and Flutter.

This debugging journey didn’t just fix a build — it deepened my understanding of how Flutter, Xcode, and Swift frameworks interoperate under the hood.

Share

More to explore

Keep exploring

Previous

German eID (Personalausweis) on Android with OIDC: A Practical Learning Journey

Next

Solving Redirects and Token Flows in Android SDKs: My Learning Journey