Repository navigation
Description
Summary
When an iOS app has a watchOS companion target and uses Swift Package Manager, Flutter's watch-companion detection probes every SwiftPM package scheme with xcodebuild -showBuildSettings -destination generic/platform=watchOS before finding the real watch scheme. In my project this added 161.5 seconds (60 scheme probes, ~2.7s each) before Running Xcode build....
A literal WKCompanionAppBundleIdentifier in the watch target's Info.plist is an effective workaround, but the underlying fallback scan is still over-broad.
Steps to reproduce
Project shape:
- iOS app target + watchOS companion target.
- SwiftPM enabled for iOS dependencies.
Runner.xcodeproj/project.pbxprojsetsINFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.example.App.- The watch target's
Info.plistdoes not contain a literalWKCompanionAppBundleIdentifierkey (it's only in build settings).
Run:
flutter build ios --debug --simulator --no-pub -vIosProject.containsWatchCompanion() falls through the cheap Info.plist check into the build-settings fallback, which iterates projectInfo.schemes. With SwiftPM, that list contains every package/plugin scheme.
Expected
Flutter detects the watch companion without probing unrelated SwiftPM package schemes. The fallback should only consider schemes that can plausibly be host/watch app schemes.
Actual
Flutter runs xcodebuild -showBuildSettings ... -destination generic/platform=watchOS for each SwiftPM package scheme before reaching the real watch scheme. Examples from -v output:
xcodebuild -project Runner.xcodeproj -scheme advertising-id -destination generic/platform=watchOS -showBuildSettings ...
xcodebuild -project Runner.xcodeproj -scheme amplitude-flutter -destination generic/platform=watchOS -showBuildSettings ...
xcodebuild -project Runner.xcodeproj -scheme Amplitude-Swift -destination generic/platform=watchOS -showBuildSettings ...
xcodebuild -project Runner.xcodeproj -scheme app-links -destination generic/platform=watchOS -showBuildSettings ...
xcodebuild -project Runner.xcodeproj -scheme cloud-firestore -destination generic/platform=watchOS -showBuildSettings ...
xcodebuild -project Runner.xcodeproj -scheme device-calendar -destination generic/platform=watchOS -showBuildSettings ...
The build appears stalled (nothing else prints) until the scan finishes.
Workaround (user side)
containsWatchCompanion() first does a cheap check for a literal WKCompanionAppBundleIdentifier key in the watch target's Info.plist. If that key is present, the expensive fallback never runs.
Adding the key explicitly to the watch target's Info.plist (e.g. ios/<WatchTarget>/Info.plist) short-circuits detection:
<key>WKCompanionAppBundleIdentifier</key>
<string>com.example.App</string>This is effective but it's a workaround, not a fix. Projects that rely on INFOPLIST_KEY_WKCompanionAppBundleIdentifier in project.pbxproj (the modern Xcode-generated form) still hit the slow path until they manually duplicate the value into the plist. Worth documenting either way.
Suggested fix direction (Flutter side)
The fallback loop lives in packages/flutter_tools/lib/src/xcode_project.dart. Restrict it to schemes that actually have an .xcscheme file in the host Xcode project, so SwiftPM package schemes are skipped before any xcodebuild -showBuildSettings call:
final Directory hostSchemesDir = xcodeProject
.childDirectory('xcshareddata')
.childDirectory('xcschemes');
for (final String scheme in projectInfo.schemes) {
// Flutter assumes single build target per scheme, so skip default scheme.
if (scheme == defaultScheme) {
continue;
}
// SwiftPM package schemes can't be a watch companion. Only consider
// schemes declared in the host .xcodeproj.
if (!hostSchemesDir.childFile('$scheme.xcscheme').existsSync()) {
continue;
}
final Map<String, String>? allBuildSettings = await buildSettingsForBuildInfo(
buildInfo,
scheme: scheme,
isWatch: true,
);
// ...unchanged...
}I ran a prototype of this filter against the same project and measured it (table below).
Measurements
All runs on Flutter 3.41.6.
| Variant | Outcome | Time to detection |
|---|---|---|
| Baseline (no plist key, unpatched Flutter) | 60 watchOS scheme probes; fallback scan completes; finds WatchApp |
161.5s (avg 2.69s, median 2.66s, slowest 3.88s per probe) |
Local flutter_tools patch implementing the filter below (no plist key) |
No package-scheme probes; fallback reaches WatchApp directly |
22.0s |
| Workaround (plist key added, unpatched Flutter) | Cheap path hits; Watch companion app found. printed |
11.3s |
Related
- Unable To Run Flutter App With Apple Watch Extension After Upgrading 3.27.0 #160622 / Fix #160622: change containsWatchConpanion function to detect companion watch apps defined by only the project info file. #176832: fixed correctness of watch build-settings queries (iOS device id), but the fallback still scans every scheme.
- Flutter: Cleaning Xcode workspace takes 2-3 minutes #173940: adjacent. SwiftPM-created schemes also make other serial Xcode scheme loops expensive.
Environment
Flutter 3.41.6
Xcode 26.4.1 (Build version 17E202)
macOS 26.4.1
Activity
[-]iOS watch companion detection scans SwiftPM package schemes and can stall before build[/-][+]iOS watch companion detection stalls during build while scanning SPM package schemes[/+][-]iOS watch companion detection stalls during build while scanning SPM package schemes[/-][+][iOS, Apple Watch] Watch companion detection stalls during build due to scanning of SPM package schemes[/+][-][iOS, Apple Watch] Watch companion detection stalls during build due to scanning of SPM package schemes[/-][+][tool][iOS] Slow Apple Watch companion detection: scans every SwiftPM scheme[/+]mbcorona commentedon May 5, 2026
Hi @lukemmtt, thanks for the analysis, the performance breakdown, and for already opening a PR with the fix.
Routed to team-ios for review.
Skip non-host schemes in watch companion fallback to avoid SwiftPM scan
Filter out SwiftPM schemes when fetching schemes (flutter#186006)
Filter out SwiftPM schemes when fetching schemes (flutter#186006)
github-actions commentedon Jul 14, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.