Browse by section

Web Design & Dev 日本語

Flutter Widget Extension Build Errors: 3 Fixes for Xcode

Adding an iOS home screen widget to a Flutter app can break the Xcode build even when you have not touched a single line of Dart. When I added widgets to my own apps, I ran into three different errors, one after another.

The short answer: all three errors come from Xcode target settings, not from Flutter. “Cycle inside Runner” is a build phase order problem. “Invalid placeholder attributes” at install time means the extension lost its version keys. The containerBackground error is a deployment target mismatch.

I reproduced all three in a freshly created Flutter project and verified each fix. For the widget implementation itself (App Group and MethodChannel), see Building an iOS Home Screen Widget for a Flutter App.

Sponsored

Which environment did I test with?

I used Flutter 3.41.9 and Xcode 26.4.1, and added a Widget Extension to a project straight out of flutter create.

Item Version / details
Flutter 3.41.9 (stable), Dart 3.11.5
Xcode 26.4.1 (17E202)
Test device iOS Simulator (iPhone 16 Pro Max)
How the extension was added A Ruby script using xcodeproj 1.27.0 that adds the Widget Extension target and its embed phase

I added the extension with a script instead of the Xcode UI so that every step was repeatable. When xcodeproj creates a new build phase, it appends it to the end of the phase list. I did not check the phase order you get when adding the target from the Xcode UI.

Why does “Cycle inside Runner” happen?

It happens when the “Embed Foundation Extensions” phase that copies the widget into the app sits after Flutter’s “Thin Binary” phase.

After adding the extension, the build stopped with this error (paths shortened):

Error (Xcode): Cycle inside Runner; building could produce unreliable results.
○ Target 'Runner' has process command with output '.../Runner.app/Info.plist'
○ Target 'Runner' has copy command from '.../TestWidgetExtension.appex' to '.../Runner.app/PlugIns/TestWidgetExtension.appex'

At that point, the build phases of the Runner target were in this order:

Run Script → Sources → Frameworks → Resources → Embed Frameworks → Thin Binary → Embed Foundation Extensions

Thin Binary is a shell script phase that flutter create adds to every iOS project. In the project file, it takes the app’s Info.plist as input and is set to run on every build.

alwaysOutOfDate = 1;
inputPaths = (
	"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";

The two “○” lines in the error show a loop in the build graph. One step handles Runner.app/Info.plist, and another copies the .appex into Runner.app. Changing the contents of Runner.app again after Thin Binary is what closes that loop.

How do you fix the phase order?

Move “Embed Foundation Extensions” above “Thin Binary”, and the build succeeds.

In Xcode, open the Runner target, go to Build Phases, and drag “Embed Foundation Extensions” above “Thin Binary”. With a script, it looks like this:

require 'xcodeproj'
project = Xcodeproj::Project.open('Runner.xcodeproj')
runner = project.targets.find { |t| t.name == 'Runner' }
embed = runner.build_phases.find { |ph| ph.display_name == 'Embed Foundation Extensions' }
thin  = runner.build_phases.find { |ph| ph.display_name == 'Thin Binary' }
runner.build_phases.delete(embed)
runner.build_phases.insert(runner.build_phases.index(thin), embed)
project.save

Once the order became ... → Embed Frameworks → Embed Foundation Extensions → Thin Binary, the build succeeded and the .appex appeared in Runner.app/PlugIns/.

Sponsored

Why does installation fail with “Invalid placeholder attributes”?

The extension’s Info.plist is missing its version keys, CFBundleShortVersionString and CFBundleVersion. The build still succeeds, so you only notice when you install the app.

With the phase order fixed, installing to the simulator failed:

An error was encountered processing the command (domain=IXErrorDomain, code=2):
Simulator device failed to install the application.
Invalid placeholder attributes.
Underlying error (domain=IXErrorDomain, code=2):
	Failed to create app extension placeholder for .../Runner.app/PlugIns/TestWidgetExtension.appex
	Failed to create promise.

Reading the built extension’s Info.plist showed that the version keys did not exist at all:

% /usr/libexec/PlistBuddy -c 'Print CFBundleVersion' Runner.app/PlugIns/TestWidgetExtension.appex/Info.plist
Print: Entry, "CFBundleVersion", Does Not Exist

The extension’s Info.plist refers to $(MARKETING_VERSION) and $(CURRENT_PROJECT_VERSION). If the extension target does not define those two settings, the keys disappear from the built Info.plist. The main app’s Info.plist still had its version, which is why looking only at the app hides the cause.

Why doesn’t $(FLUTTER_BUILD_NAME) fix it on its own?

Setting MARKETING_VERSION = $(FLUTTER_BUILD_NAME) on the extension still left the version keys empty in my tests.

$(FLUTTER_BUILD_NAME) and $(FLUTTER_BUILD_NUMBER) are written to ios/Flutter/Generated.xcconfig. Only the Runner target’s configuration files, such as Debug.xcconfig, include that file. The extension target does not. The keys were missing whether I built with flutter build ios or with xcodebuild directly.

How do you give the extension the same version as the app?

Create an xcconfig for the extension that includes Generated.xcconfig, and set it as the extension target’s base configuration. The extension then gets exactly the app’s version.

// ios/TestWidget/Widget.xcconfig
// For the widget extension: import only Flutter's version variables
#include "../Flutter/Generated.xcconfig"

Keep MARKETING_VERSION = $(FLUTTER_BUILD_NAME) and CURRENT_PROJECT_VERSION = $(FLUTTER_BUILD_NUMBER) in the extension’s build settings. Then assign Widget.xcconfig to the extension’s Debug, Release and Profile configurations.

ext = project.targets.find { |t| t.name == 'TestWidgetExtension' }
group = project.main_group.find_subpath('TestWidget', false)
ref = group.new_file('Widget.xcconfig')
ext.build_configurations.each { |c| c.base_configuration_reference = ref }
project.save

With version: 2.3.4+56 in pubspec.yaml, both the app and the extension came out as 2.3.4 (56), and the simulator install succeeded.

Why not hard-code the extension version?

App Store Connect expects an app extension to have the same version numbers as its containing app.

A fixed value such as MARKETING_VERSION = 1.0 does let the install succeed. But every time you bump the app version, you also have to bump the extension, or the upload gets flagged for a version mismatch (ITMS-90473). There is a thread on the Apple Developer Forums stating that an app extension’s CFBundleShortVersionString must match its containing parent app. Including Generated.xcconfig keeps both in sync whenever you change version in pubspec.yaml.

Why is containerBackground “only available in iOS 17.0 or newer”?

The extension’s deployment target is below iOS 17, but the code calls containerBackground, an API added in iOS 17, without an availability check.

With the extension’s deployment target set to iOS 16.1, the build failed with:

Swift Compiler Error (Xcode): 'containerBackground(for:alignment:content:)' is only available in iOS 17.0 or newer
Swift Compiler Error (Xcode): 'widget' is only available in iOS 17.0 or newer

containerBackground(for:alignment:content:) was added in iOS 17 for widget backgrounds. When the extension’s deployment target was iOS 17 or later, the same code compiled without any check.

How do you support iOS 16 and iOS 17 at the same time?

Wrap the call in #available(iOSApplicationExtension 17.0, *) inside a small View extension.

extension View {
  /// Use containerBackground on iOS 17+, and a normal background on iOS 16 and earlier
  @ViewBuilder
  func widgetBackground(_ color: Color) -> some View {
    if #available(iOSApplicationExtension 17.0, *) {
      containerBackground(for: .widget) { color }
    } else {
      background(color)
    }
  }
}

// Usage
Text(entry.date, style: .time)
  .widgetBackground(Color.blue)

With the deployment target left at iOS 16.1, this version built successfully. The built extension’s MinimumOSVersion was 16.1.

▼How to choose the extension’s deployment target

▶ The app itself requires iOS 17 or later → set the extension to iOS 17 too, and call containerBackground directly

▶ The app also supports iOS 16 or earlier → match the extension’s deployment target to the app and use #available

Sponsored

What should you check after adding a widget extension?

A successful build is not enough. Check the phase order, the extension’s version, the deployment target and an actual install.

The version problem above only showed up at install time, even though the build succeeded. If you treat “it builds” as proof that it works, this kind of bug slips through.

▼Checklist after adding an extension

① In the Runner target, Embed Foundation Extensions comes before Thin Binary

② The extension’s Info.plist has version keys that match the app

③ Any API newer than the extension’s deployment target is wrapped in #available

④ You install to a simulator or device, not just build

You can check item 2 from the command line:

APP=build/ios/iphonesimulator/Runner.app
for p in "$APP/Info.plist" "$APP"/PlugIns/*.appex/Info.plist; do
  echo "$p: $(/usr/libexec/PlistBuddy -c 'Print CFBundleShortVersionString' "$p") ($(/usr/libexec/PlistBuddy -c 'Print CFBundleVersion' "$p"))"
done

Related articles

▶ Building an iOS Home Screen Widget for a Flutter App…passing data to the widget and animating it

▶ Pixel Islet (ピクレット) support page…an app that ships a home screen widget built this way

Summary

▼Key points

① Cycle inside Runner is fixed by moving Embed Foundation Extensions above Thin Binary

② Invalid placeholder attributes means the extension’s Info.plist has no version keys. The build still succeeds

③ $(FLUTTER_BUILD_NAME) alone is not enough. Include Generated.xcconfig to match the app’s version

④ The containerBackground error appears when the extension targets iOS 16 or earlier. Use #available

The hardest one to spot was the second error. The build succeeded, and the app’s own Info.plist had a version. I only found the cause after opening the Info.plist inside the extension.

Tested with Flutter 3.41.9 and Xcode 26.4.1 on September 12, 2026.