ジャンルから探す

Web Design & Dev English

FlutterにWidget Extensionを足すと出るビルドエラー3つと直し方

FlutterアプリにiOSのホーム画面ウィジェットを足すと、Dartのコードは何も変えていないのに、Xcodeのビルドやインストールが通らなくなることがあります。自作のアプリにウィジェットを入れたとき、3種類のエラーに順番に当たりました。

結論から書くと、3つともFlutterではなく、Xcodeのターゲット設定の問題です。①「Cycle inside Runner」は同梱フェーズの順番、②インストール時の「Invalid placeholder attributes」は拡張の版数の抜け、③「containerBackground」のエラーは最低iOSバージョンの差が原因でした。

この記事では、新しく作ったFlutterプロジェクトで3つを再現し、直し方をそれぞれ確かめた結果をまとめます。ウィジェットの実装そのもの(App GroupとMethodChannelでのデータの受け渡し)は、FlutterアプリのiOSウィジェットをSwiftで作る|App GroupとMethodChannelの実装に書いています。

スポンサーリンク

検証した環境

結論:Flutter 3.41.9とXcode 26.4.1で、flutter createした直後のプロジェクトにWidget Extensionを足して確かめました。

項目 バージョン・内容
Flutter 3.41.9(stable)/Dart 3.11.5
Xcode 26.4.1(17E202)
確認に使った端末 iOSシミュレータ(iPhone 16 Pro Max)
拡張の足し方 Rubyのxcodeproj 1.27.0で、Widget Extensionのターゲットと同梱フェーズを追加

拡張の追加はXcodeの画面ではなく、再現できるようにスクリプトで行いました。xcodeprojで新しいビルドフェーズを作ると、フェーズの並びの末尾に追加されます。Xcodeの画面から追加したときの並び順は、今回は確かめていません。

エラー1:Cycle inside Runner はなぜ出るのか

結論:拡張を同梱する「Embed Foundation Extensions」フェーズが、Flutterの「Thin Binary」フェーズより後ろにあるのが原因です。

拡張を足してビルドすると、次のエラーで止まりました(パスは短くしています)。

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'

このときのRunnerターゲットのビルドフェーズは、次の並びでした。

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

Thin Binaryは、flutter createが作るシェルスクリプトのフェーズです。プロジェクトファイルを見ると、入力にRunner.appのInfo.plistを持ち、毎回実行される設定になっています。

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

エラーの「○」の2行は、Runner.appのInfo.plistを扱う処理と、appexをRunner.appの中へコピーする処理が、ビルドの依存関係の中で輪になっていることを示しています。Thin Binaryの後ろで、もう一度Runner.appの中身を書き換える並びが、この輪を作っていました。

Embed Foundation Extensionsを前に移す

結論:「Embed Foundation Extensions」を「Thin Binary」より前に移すと、ビルドが通ります。

Xcodeなら、Runnerターゲットの「Build Phases」で「Embed Foundation Extensions」をドラッグし、「Thin Binary」の上に置きます。スクリプトで直すなら次のとおりです。

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

並びが... → Embed Frameworks → Embed Foundation Extensions → Thin Binaryになると、ビルドは成功し、Runner.app/PlugIns/に拡張のappexが入りました。

スポンサーリンク

エラー2:インストールで Invalid placeholder attributes になるのはなぜか

結論:拡張のInfo.plistから、版数(CFBundleShortVersionStringとCFBundleVersion)のキーが消えているのが原因です。ビルドは成功するので、インストールするまで気づけません。

エラー1を直してビルドが通ったあと、シミュレータへのインストールで止まりました。

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.

できあがった拡張のInfo.plistを読むと、版数のキーそのものがありませんでした。

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

拡張のInfo.plistは、版数を$(MARKETING_VERSION)と$(CURRENT_PROJECT_VERSION)で参照しています。拡張ターゲットにこの2つの値が無いと、ビルド後のInfo.plistからキーごと消えます。アプリ本体のRunner.appには版数が入っていたので、本体だけ見ていると原因に気づけません。

$(FLUTTER_BUILD_NAME)を書くだけでは直らない

結論:拡張の設定にMARKETING_VERSION = $(FLUTTER_BUILD_NAME)と書いても、版数は空のままでした。

$(FLUTTER_BUILD_NAME)と$(FLUTTER_BUILD_NUMBER)は、ios/Flutter/Generated.xcconfigに書き出される値です。このGenerated.xcconfigを読み込んでいるのはRunnerターゲットの設定ファイル(Debug.xcconfigなど)だけで、拡張ターゲットは読み込んでいません。flutter build iosでビルドしても、xcodebuildで直接ビルドしても、拡張の版数のキーは消えたままでした。

拡張にもGenerated.xcconfigを読み込ませる

結論:拡張用のxcconfigを作ってGenerated.xcconfigを読み込み、拡張ターゲットの設定ファイルに指定すると、拡張の版数がアプリと同じになります。

// ios/TestWidget/Widget.xcconfig
// ウィジェット拡張用。Flutter のバージョン変数だけを取り込む
#include "../Flutter/Generated.xcconfig"

拡張ターゲットのビルド設定は、MARKETING_VERSION = $(FLUTTER_BUILD_NAME)とCURRENT_PROJECT_VERSION = $(FLUTTER_BUILD_NUMBER)のままにしておきます。そのうえで、拡張ターゲットのDebug・Release・Profileの設定ファイルにWidget.xcconfigを指定します。

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

pubspec.yamlのversionを2.3.4+56にしてビルドすると、アプリも拡張も2.3.4(56)になり、シミュレータへのインストールも通りました。

版数を固定値で書かないほうがいい理由

結論:App Store Connectに提出するときは、拡張とアプリの版数が一致している必要があるからです。

拡張にMARKETING_VERSION = 1.0のような固定値を書いても、インストールは通ります。ただしアプリの版数を上げるたびに拡張もそろえないと、提出時に版数の不一致(ITMS-90473)で指摘されます。Appleの開発者フォーラムにも、拡張の版数は親アプリと一致させる必要があるというやり取りがあります。Generated.xcconfigを読み込ませておけば、pubspec.yamlのversionを上げるだけで両方がそろいます。

エラー3:containerBackground が iOS 17.0 以上と言われるのはなぜか

結論:拡張の最低iOSバージョンが17未満なのに、iOS 17で追加されたcontainerBackgroundを分岐なしで呼んでいるのが原因です。

拡張の最低iOSを16.1にしてビルドすると、次のエラーになりました。

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:)は、iOS 17でウィジェットの背景を指定するために追加されたAPIです。拡張の最低iOSが17以上なら、分岐なしで書いてもビルドは通りました。

#availableで分岐する

結論:iOS 16以前も対象にするなら、#available(iOSApplicationExtension 17.0, *)で分岐したViewの拡張を用意します。

extension View {
  /// iOS 17 以降は containerBackground、16 以前は通常の background にする
  @ViewBuilder
  func widgetBackground(_ color: Color) -> some View {
    if #available(iOSApplicationExtension 17.0, *) {
      containerBackground(for: .widget) { color }
    } else {
      background(color)
    }
  }
}

// 使う側
Text(entry.date, style: .time)
  .widgetBackground(Color.blue)

拡張の最低iOSを16.1のままにして、このコードでビルドが通ることを確かめました。できあがった拡張のMinimumOSVersionは16.1でした。

▼最低iOSバージョンの決め方

▶ アプリ本体もiOS 17以上 → 拡張も17以上にして、containerBackgroundを分岐なしで書く

▶ アプリ本体がiOS 16以前にも対応 → 拡張の最低iOSを本体と合わせ、#availableで分岐する

スポンサーリンク

ウィジェットを足したあとに確かめること

結論:ビルドの成功だけでは足りません。フェーズの順番・拡張の版数・最低iOS・インストールの4つを確かめます。

エラー2は、ビルドが成功するのにインストールで初めて失敗しました。ビルドが通ったことを「動く」の根拠にすると、この種類の不具合を見逃します。

▼拡張を足したあとのチェックリスト

① Runnerのビルドフェーズで、Embed Foundation ExtensionsがThin Binaryより前にある

② 拡張のInfo.plistに版数があり、アプリと一致している

③ 拡張の最低iOSより古いOSを対象にするAPIは、#availableで分岐している

④ ビルドだけでなく、シミュレータか実機へのインストールまで行う

②はコマンドで確かめられます。

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

関連する記事

▶ FlutterアプリのiOSウィジェットをSwiftで作る|App GroupとMethodChannelの実装…ウィジェットにデータを渡して、動かすところまでの実装

▶ Rin Blog Apps…公開しているアプリの一覧

まとめ

▼要点

① Cycle inside Runnerは、Embed Foundation ExtensionsをThin Binaryより前に移すと直る

② Invalid placeholder attributesは、拡張のInfo.plistから版数が消えているのが原因。ビルドは通るのでインストールまで気づけない

③ 拡張に$(FLUTTER_BUILD_NAME)を書くだけでは足りない。Generated.xcconfigを読み込ませるとアプリと版数がそろう

④ containerBackgroundのエラーは、拡張の最低iOSが17未満のとき。#availableで分岐する

3つのうち、いちばん気づきにくかったのは②でした。ビルドは成功し、アプリ本体のInfo.plistにも版数は入っています。拡張の中のInfo.plistまで開いて、ようやく原因が見えました。

※Flutter 3.41.9・Xcode 26.4.1で、2026年9月12日に確認した内容です。