iOS 15.8.4 の実機で .NET MAUI/Xamarin‑iOS アプリが NSInvalidArgumentException を吐いて起動直後に落ちる——その正体は、UIBarButtonItem の新しい初期化子(iOS 16 以降)を iOS 15 で呼んでしまっていることにあります。本稿では原因の分解から安全な代替実装、ビルド設定、実行時の防御策までを一気通貫で解説し、コピペできるサンプルコードとチェックリストで“今すぐ直せる”ことに徹します。
症状の概要(クラッシュログ)
iOS 15.8.4 を搭載した端末(iPhone / iPad)でアプリを起動すると、起動直後またはナビゲーションバー生成時にクラッシュし、以下のような例外が出力されます。
NSInvalidArgumentException
-[UIBarButtonItem initWithBarButtonSystemItem:primaryAction:menu:]: unrecognized selector sent to instance
つまり UIBarButtonItem に対して initWithBarButtonSystemItem:primaryAction:menu: というセレクタ(メソッド)が存在しないため、実行時に「そんなメソッドは知らない」と落ちている状態です。
原因の本質:API 可用性(Availability)の越境
- 該当の初期化子は iOS 16.0 以降で追加された API です。
- iOS 15 系にはこのセレクタが存在しないため、実行時に解決できず例外が投げられます。
- .NET MAUI/Xamarin‑iOS では、新しい iOS SDK でビルドすると最新 API を(コンパイル時には)参照できますが、古い OS で実行する際は開発者が自分で守る必要があります(バージョン分岐やセレクタ存在確認)。
なぜ「いま」表面化したのか
iOS 15 自体は以前から存在していましたが、次のようなトリガーが加わると発火します。
- プロジェクトが新しい Xcode/iOS SDK で再ビルドされ、UIBarButtonItem の新コンストラクタ(またはそれを内部で使うラッパー)を間接的に呼ぶようになった。
- 共通ライブラリやコンポーネントの更新で、16 以降 API を前提にした実装が混入した。
- MAUI/Xamarin 側で
ToolbarItem→ iOS ネイティブ変換の経路に、primaryAction / menu を伴う生成が紛れ込んだ。
最短の解決策:サポート OS を iOS 16.0 以上へ
もっとも手間が少なく、安全性・将来性も高い対処は、アプリの最小サポート OS(Deployment Target)を iOS 16.0 以上に引き上げることです。ユーザーにも iOS 16 以降へのアップデートを促すだけで、当該クラッシュは消えます。
| 項目 | 利点 | 懸念点(要確認) |
|---|---|---|
| 開発コスト | 分岐・代替コードが不要。テスト対象も減る。 | なし |
| UX / 機能 | 最新 API を安心して活用できる。 | なし |
| ユーザー影響 | クラッシュ撲滅、サポート問合せ削減。 | iOS 15 端末の切り捨て(残存比率の測定要)。 |
iOS 15 を同時サポートする場合の安全な代替実装
ユーザー母数やビジネス要件的に iOS 15 を捨てられないなら、次のいずれか(併用可)で解決します。推奨は 「古い初期化子+バージョン分岐で足りない機能だけを上乗せ」する方式です。
代替 A:従来の target/action 初期化子で生成し、必要ならメニューを後付け
古典的な target/action を使います。UIMenu が必要な場合は、OS の可用性を確認してからプロパティで付与します(UIBarButtonItem.menu は iOS 14+)。
Objective‑C
UIBarButtonItem *item = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
target:self
action:@selector(onAddTapped:)];
if (@available(iOS 14.0, *)) {
UIAction *act = [UIAction actionWithTitle:@"追加"
image:nil
identifier:nil
handler:^(__kindof UIAction *action) {
[self onAddTapped:nil];
}];
UIMenu *menu = [UIMenu menuWithTitle:@"" children:@[act]];
item.menu = menu; // iOS 14+ で有効
}
self.navigationItem.rightBarButtonItem = item;
Swift
let item = UIBarButtonItem(barButtonSystemItem: .add,
target: self,
action: #selector(onAddTapped))
if #available(iOS 14, *) {
let action = UIAction(title: "追加") { [weak self] _ in self?.onAddTapped() }
let menu = UIMenu(title: "", children: [action])
item.menu = menu // iOS 14+ で有効
}
navigationItem.rightBarButtonItem = item
.NET(MAUI/Xamarin‑iOS / C#)
using UIKit;
using ObjCRuntime;
var item = new UIBarButtonItem(UIBarButtonSystemItem.Add, (sender, e) => OnAddTapped());
if (OperatingSystem.IsIOSVersionAtLeast(14, 0)) {
var action = UIAction.Create("追加", null, _ => OnAddTapped());
var menu = UIMenu.Create(children: new[] { action });
// プロパティ Menu は iOS 14+ で有効
item.Menu = menu;
}
// item を NavigationItem へ追加
NavigationItem.RightBarButtonItem = item;
代替 B:iOS 16 以降のみ新初期化子を使い、それ以前はフォールバック
新しい primaryAction:menu: 付き初期化子は iOS 16 以降でのみ使います。
Objective‑C
UIBarButtonItem *item = nil;
if (@available(iOS 16.0, *)) {
UIAction *primary = [UIAction actionWithTitle:@"追加" image:nil identifier:nil handler:^(__kindof UIAction *a) {
[self onAddTapped:nil];
}];
UIMenu *menu = [UIMenu menuWithTitle:@"" children:@[primary]];
item = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
primaryAction:primary
menu:menu];
} else {
item = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
target:self
action:@selector(onAddTapped:)];
}
self.navigationItem.rightBarButtonItem = item;
C#(Xamarin‑iOS)
UIBarButtonItem item;
if (OperatingSystem.IsIOSVersionAtLeast(16, 0)) {
var primary = UIAction.Create("追加", null, _ => OnAddTapped());
var menu = UIMenu.Create(children: new[] { primary });
// 新コンストラクタは iOS 16+ のみ
item = new UIBarButtonItem(UIBarButtonSystemItem.Add, primary, menu);
} else {
item = new UIBarButtonItem(UIBarButtonSystemItem.Add, (s, e) => OnAddTapped());
}
NavigationItem.RightBarButtonItem = item;
代替 C:セレクタ存在確認(respondsToSelector / instancesRespondToSelector)
上級向けですが、実行時にセレクタの存在を確かめてから呼ぶ方法もあります。可用性分岐と併用するとより堅牢です。
Objective‑C
SEL sel = @selector(initWithBarButtonSystemItem:primaryAction:menu:);
if ([UIBarButtonItem instancesRespondToSelector:sel]) {
// 安全に新 API を使用
} else {
// 旧 API へフォールバック
}
C#(Xamarin‑iOS、概念例)
var sel = new Selector("initWithBarButtonSystemItem:primaryAction:menu:");
bool supported = OperatingSystem.IsIOSVersionAtLeast(16, 0); // 実戦では OS 分岐を推奨
// supported が true のときのみ新 API を呼ぶ
注意: C# で厳密に instancesRespondToSelector: を叩くには ObjCRuntime.Messaging を用いる必要があり、保守コストが上がります。現場では OS バージョン分岐が最も簡潔で安全です。
MAUI での具体的実装例(ToolbarItem → iOS ネイティブ)
MAUI の ToolbarItem は iOS で UIBarButtonItem に変換されます。iOS 16+ ならメニューを付与し、15 では従来動作に落とす Mapper 拡張の例を示します。
using Microsoft.Maui.Handlers;
using UIKit;
// App 起動時(MauiProgram など)で一度だけ登録
ToolbarItemHandler.Mapper.AppendToMapping("ConditionalMenu", (handler, view) =>
{
#if IOS
// handler.PlatformView は iOS で UIBarButtonItem
if (OperatingSystem.IsIOSVersionAtLeast(14, 0))
{
var act = UIAction.Create(view.Text ?? "アクション", null, _ =>
{
if (view.Command?.CanExecute(view.CommandParameter) == true)
view.Command.Execute(view.CommandParameter);
});
var menu = UIMenu.Create(children: new[] { act });
// iOS 14+ のみ Menu が有効。15 でも OK。
handler.PlatformView.Menu = menu;
}
// iOS 16+ で primaryAction ベースにしたい場合は、ここで PlatformView を作り直す戦略も可
#endif
});
この実装は iOS 15 でもクラッシュしません。Menu の利用を OS 14+ に限定し、新しい初期化子を直接呼ばないことがポイントです。
ビルド時のガード(弱リンク回避と設定)
.NET(MAUI/Xamarin‑iOS)側
- 最小 OS を明示:.NET 7/8 の iOS では
SupportedOSPlatformVersionまたはMinimumOSVersionを設定します。
<PropertyGroup>
<TargetFrameworks>net8.0-ios</TargetFrameworks>
<SupportedOSPlatformVersion>15.0</SupportedOSPlatformVersion> <!-- 実要件に合わせて -->
</PropertyGroup>
アプリとして iOS 16 以上に引き上げるなら、ここを 16.0 に。Xcode の「iOS Deployment Target」とも整合させます。
可用性アナライザを黙らせつつ安全に呼ぶ
OperatingSystem.IsIOSVersionAtLeast で分岐すると、C# 側の可用性アナライザ(CA1416 相当)の警告も抑えられます。
if (OperatingSystem.IsIOSVersionAtLeast(16, 0)) {
// iOS 16+ 限定呼び出し(アナライザも OK)
}
実行時の防御(クラッシュを未然に防ぐ)
例外ハンドリングとフォールバック UI
未知のセレクタ例外は基本的に 致命的 ですが、アプリ全体のハンドラを用意して「次回からは古い UI に戻す」などのフォールバックを仕込むことは有用です。
// 例:起動時に登録
AppDomain.CurrentDomain.UnhandledException += (s, e) =>
{
// ここでログを取り、次回起動時に安全な経路へ誘導するフラグを保存する等
};
// タスク例外
TaskScheduler.UnobservedTaskException += (s, e) =>
{
e.SetObserved();
// ログ送信やユーザー通知
};
ただし NSInvalidArgumentException 発生箇所でプロセスが即死することもあるため、本質的対策は「呼ばない」ことです。ハンドラは二次防御として考えましょう。
「セレクタがあるか」を動的に確認する
どうしても実行時に判断したい場合は、次のようなチェックを併用します。
SEL s = @selector(setMenu:);
if ([item respondsToSelector:s]) {
// 安全にメニューを設定
}
よくある落とし穴とベストプラクティス
- 「ビルドできる=古い OS でも動く」ではない: 新 SDK でビルドしても、実行は別物。常に OS 可用性を意識。
- メニューだけなら iOS 14+ で十分:
UIBarButtonItem.menuをプロパティで後付けすれば、iOS 15 でもリッチな体験が可能。新初期化子は「あるなら使う」程度で OK。 - テストデバイス/シミュレータの OS を網羅: iOS 15 実機での動作確認を CI/CD で自動化(スモークテスト)すると再発が防げます。
- 間接呼び出しに注意: 直に
initWithBarButtonSystemItem:primaryAction:menu:を書いていなくても、ラッパーや UI フレームワーク更新で呼ばれている場合があります。スタックトレースで経路を特定。
API 可用性の早見表
| 対象 | 導入 OS | 推奨の使い方 | iOS 15 での扱い |
|---|---|---|---|
UIBarButtonItem.menu(プロパティ) | iOS 14+ | 可用性分岐(iOS 14+)で安全に設定 | 利用可能(クラッシュしない) |
initWithBarButtonSystemItem:primaryAction:menu: | iOS 16+ | iOS 16+ のみで使用。旧 OS では target/action | 未実装。呼ぶと例外 |
UIAction を primaryAction として利用 | iOS 13+(UIAction 自体) | iOS 16+ では初期化子に渡す/それ以前はメニュー経由で | メニュー経由は可、初期化子への直渡しは不可 |
プロジェクト設定の整合(Xcode/.NET)
- Deployment Target と SDK の整合: Xcode の iOS Deployment Target と .NET プロジェクト側(
SupportedOSPlatformVersion)を一致させ、意図せぬ弱リンクを避けます。 - 条件コンパイルの明示: ネイティブコード(Objective‑C/Swift)では
@availableを、C# ではOperatingSystem.IsIOSVersionAtLeast/UIDevice.CurrentDevice.CheckSystemVersionを統一運用。 - CI での二重テスト: 「最小サポート OS」と「最新 OS」の少なくとも 2 系列で UI スモークテストを実施。
デバッグ手順(再現と切り分け)
- iOS 15.8.4 の実機を用意(または 15 系の端末)。
- 起動直後にクラッシュする場合、ナビゲーションバーやツールバーを生成するコードをコメントアウトして再ビルド。
- クラッシュが消えるなら、
UIBarButtonItem周りを重点的に確認。primaryAction / menu を伴う初期化が無いか探す。 - 第三者ライブラリを使っている場合はバージョンを固定して二分探索。該当変更が入ったコミットを同定。
- 修正後は OS 14 / 15 / 16 の少なくとも 3 段で回帰テスト。
移行戦略の比較
| 戦略 | 工数 | 互換性 | 推奨度 | コメント |
|---|---|---|---|---|
| 最小 OS を 16 へ引き上げ | 低 | 最新のみ | 高 | 最短で安全。旧端末が無視できる割合ならこれ一択。 |
target/action + menu 後付け | 中 | 14+ で広く動作 | 高 | UI 体験を維持しつつ 15 も救済。 |
| OS 分岐で新初期化子/旧初期化子を使い分け | 中 | 16+/15 の両対応 | 中 | コードがやや複雑。テスト網羅が必須。 |
| セレクタ存在確認で動的呼び分け | 中〜高 | 状況依存 | 中 | 柔軟だが保守難度が上がる。基本は OS 分岐を優先。 |
最終チェックリスト(そのまま運用に)
- UIBarButtonItem の初期化子に
primaryAction:menu:を使っていないか?(検索) - 使っている場合は iOS 16+ ガード(
@available/IsIOSVersionAtLeast)を設置。 - iOS 15 では target/action 初期化子にフォールバックし、必要なら
.menuを後付け(iOS 14+)。 - プロジェクトの 最小 OS と Xcode の Deployment Target を一致させたか。
- CI で iOS 15 実機またはデバイスファームを使ったスモークテストを追加。
- 例外ハンドラで ログ収集と次回起動時のフォールバックを準備。
まとめ
本件クラッシュは、iOS 16 以降の UIBarButtonItem 初期化子を iOS 15 で呼んだことが原因です。対処はシンプルで、(1)最小 OS を 16 へ引き上げる、もしくは(2)iOS 15 では target/action で生成し、必要に応じて .menu を後付けする、のどちらか。可用性分岐とテストを徹底すれば、同種の事故はほぼ防げます。MAUI でも Xamarin‑iOS でも、OS ガード → 旧 API でのフォールバック → (可能なら)プロパティでの後付けという基本設計を守るだけで十分に堅牢です。今日からコードベースを点検し、再発を断ちましょう。
付録:最小限の修正パッチ例(差分イメージ)
- var item = new UIBarButtonItem(UIBarButtonSystemItem.Add, primary, menu); // iOS 15 で落ちる
+ UIBarButtonItem item;
+ if (OperatingSystem.IsIOSVersionAtLeast(16, 0)) {
+ var primary = UIAction.Create("追加", null, _ => OnAddTapped());
+ var menu = UIMenu.Create(children: new[] { primary });
+ item = new UIBarButtonItem(UIBarButtonSystemItem.Add, primary, menu);
+ } else {
+ item = new UIBarButtonItem(UIBarButtonSystemItem.Add, (s, e) => OnAddTapped());
+ if (OperatingSystem.IsIOSVersionAtLeast(14, 0)) {
+ var a = UIAction.Create("追加", null, _ => OnAddTapped());
+ item.Menu = UIMenu.Create(children: new[] { a });
+ }
+ }
NavigationItem.RightBarButtonItem = item;

コメント