Skip to content

[tool][iOS] Slow Apple Watch companion detection: scans every SwiftPM scheme #186004

@lukemmtt

Description

@lukemmtt

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High-priority issues at the top of the work listc: performanceRelates to speed or footprint issues (see "perf:" labels)platform-iosiOS applications specificallyplatform-macosBuilding on or for macOS specificallyteam-iosOwned by iOS platform teamtoolAffects the "flutter" command-line tool. See also t: labels.triaged-iosTriaged by iOS platform team

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions