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.pbxproj sets INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.example.App.
- The watch target's
Info.plist does not contain a literal WKCompanionAppBundleIdentifier key (it's only in build settings).
Run:
flutter build ios --debug --simulator --no-pub -v
IosProject.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
Environment
Flutter 3.41.6
Xcode 26.4.1 (Build version 17E202)
macOS 26.4.1
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=watchOSbefore finding the real watch scheme. In my project this added 161.5 seconds (60 scheme probes, ~2.7s each) beforeRunning Xcode build....A literal
WKCompanionAppBundleIdentifierin the watch target'sInfo.plistis an effective workaround, but the underlying fallback scan is still over-broad.Steps to reproduce
Project shape:
Runner.xcodeproj/project.pbxprojsetsINFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.example.App.Info.plistdoes not contain a literalWKCompanionAppBundleIdentifierkey (it's only in build settings).Run:
IosProject.containsWatchCompanion()falls through the cheap Info.plist check into the build-settings fallback, which iteratesprojectInfo.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=watchOSfor each SwiftPM package scheme before reaching the real watch scheme. Examples from-voutput:The build appears stalled (nothing else prints) until the scan finishes.
Workaround (user side)
containsWatchCompanion()first does a cheap check for a literalWKCompanionAppBundleIdentifierkey in the watch target'sInfo.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:This is effective but it's a workaround, not a fix. Projects that rely on
INFOPLIST_KEY_WKCompanionAppBundleIdentifierinproject.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.xcschemefile in the host Xcode project, so SwiftPM package schemes are skipped before anyxcodebuild -showBuildSettingscall:I ran a prototype of this filter against the same project and measured it (table below).
Measurements
All runs on Flutter 3.41.6.
WatchAppflutter_toolspatch implementing the filter below (no plist key)WatchAppdirectlyWatch companion app found.printedRelated
Environment