.NET MAUI を iOS 実機にデプロイした途端、「Failed to marshal the Objective‑C object … VerticalCell」で落ちる――そんな“再現するのに時間がかかるのに、直す糸口が薄い”系の不具合を、一気に片付けるための実践ガイドです。原因の見分け方から、最短の応急処置、恒久対策、検証の型までを具体的な手順とコードで整理します。
エラーの正体を短く把握する
iOS ランタイムがネイティブ側で生成した VerticalCell(UITableViewCell 相当)のオブジェクトに対し、対応する .NET 側のマネージドインスタンスを作れずに例外を投げています。代表的なスタックでは、Failed to marshal the Objective-C object … (type: Microsoft_Maui_Controls_Handlers_Items_VerticalCell) と出力され、アプリは起動直後またはリスト画面の描画時にクラッシュします。
Failed to marshal the Objective-C object 0x... (type: Microsoft_Maui_Controls_Handlers_Items_VerticalCell).
Could not create an native instance of the type 'Microsoft.Maui.Controls.Handlers.Items.VerticalCell':
the native class hasn't been loaded.
この“マーシャリング(橋渡し)エラー”が起きる典型パターンは次の3つです。
- VerticalCell にハンドル受け取り用コンストラクタ(
(IntPtr handle))が無い/到達不能 - 古い FFImageLoading が内部でセルをフックして壊す(依存関係の不一致/リフレクションの失敗)
- MAUI・Xcode・.NET SDK のバージョン不整合で、既知バグやレジストラ差異に踏み込む
原因マップ(見分け方と初動)
| 主な要因 | 詳細説明 | 見分け方・兆候 | 初動・推奨対応 |
|---|---|---|---|
VerticalCell に IntPtr コンストラクタが無い | iOS ランタイムは NSObject 派生型に protected TypeName(IntPtr handle) を要求。カスタムセル/カスタムハンドラで未実装・トリミングで消滅すると橋渡し不能。 | 実機のみで再現、Release/AOT で顕著。トリミング有効時に増える。 | コンストラクタを実装し トリマーから保護。Preserve 属性または linker.xml を併用。 |
| FFImageLoading の残存 | 2023年以降メンテ停止。MAUI 最新と併用すると内部セルのラップや Renderer 差し替えで失敗。VerticalCell に波及。 | 画像を多用する画面でのみ発火/パッケージ削除で改善する報告。 | FFImageLoading をアンインストールし、標準 Image+UriImageSource のキャッシュへ移行。 |
| MAUI/SDK/Xcode の不整合 | ワークロード更新漏れ、古い Xcode のまま、ターゲット iOS の差異などで既知問題に該当。 | 別機種・別OSバージョンで症状が変わる。新規プロジェクトでは再現しない。 | 全レイヤを最新安定版へ整合、クリーンビルド、実機再ペアリング。 |
最短で止血するチェックリスト(5〜10分)
- プロジェクトから FFImageLoading 系パッケージを全削除(
FFImageLoading/FFImageLoading.Forms/FFImageLoading.Maui)。 bin/とobj/を削除して 完全クリーン。dotnet workload restore→dotnet build -t:Run -f net8.0-iosで実機再デプロイ。- まだ落ちる場合は MAUI / .NET / Xcode を更新(後述のバージョン整合手順)。
- 自作のセルやハンドラがあるなら
(IntPtr handle)コンストラクタを実装+トリミング保護。
FFImageLoading をやめる:安全な置換パターン
アンインストールと初期化コードの撤去
# パッケージの削除(存在するものだけでOK)
dotnet remove package FFImageLoading
dotnet remove package FFImageLoading.Forms
dotnet remove package FFImageLoading.Maui
初期化コードも削除します(例)。
// 例: AppDelegate / MauiProgram などに残っていれば削除
// FFImageLoading.Forms.Platform.CachedImageRenderer.Init();
// ImageService.Instance.Initialize();
標準 <Image> + UriImageSource でキャッシュ
MAUI 標準の UriImageSource はキャッシュ機能を備えています。ほとんどのユースケースは外部ライブラリ不要です。
<Image Aspect="AspectFill" WidthRequest="160" HeightRequest="90">
<Image.Source>
<UriImageSource Uri="{Binding ThumbnailUrl}"
CachingEnabled="True"
CacheValidity="14.00:00:00" />
</Image.Source>
</Image>
C# から設定する場合:
var src = new UriImageSource {
Uri = new Uri(viewModel.ThumbnailUrl),
CachingEnabled = true,
CacheValidity = TimeSpan.FromDays(14)
};
myImage.Source = src;
プレースホルダー/エラーフォールバックの簡易実装
FFImageLoading の「Placeholder」の代替は、XAML の トリガーや 重ね合わせで実現できます。
<Grid WidthRequest="160" HeightRequest="90">
<Image x:Name="MainImage"> ... </Image>
<ActivityIndicator IsRunning="True" IsVisible="{Binding Source={x:Reference MainImage}, Path=IsLoading}" />
<Image Source="placeholder.png"
IsVisible="{Binding Source={x:Reference MainImage}, Path=IsLoading}" />
</Grid>
VerticalCell/カスタムハンドラーを使う場合の恒久対策
自作の UITableViewCell/UICollectionViewCell 相当や、iOS ハンドラーを拡張している場合は、必ず次のパターンを満たしてください。
(IntPtr handle) コンストラクタの実装
using ObjCRuntime;
using UIKit;
[Register(nameof(CustomVerticalCell))]
public class CustomVerticalCell : UITableViewCell
{
// ネイティブ→マネージド生成のために必須
protected CustomVerticalCell(IntPtr handle) : base(handle) { }
// マネージド→ネイティブ生成で使う通常コンストラクタ(必要に応じて)
public CustomVerticalCell() : base(UITableViewCellStyle.Default, nameof(CustomVerticalCell))
{
// 初期化処理
}
}
トリミング(AOT)で消されないように保護
Release/AOT では未参照メンバーが トリマーにより除去されることがあります。到達解析で見えないコンストラクタは消されやすいので、Preserve 属性または linker 設定で守ります。
using Microsoft.Maui.Controls.Internals;
[Preserve(AllMembers = true)]
[Register(nameof(CustomVerticalCell))]
public class CustomVerticalCell : UITableViewCell
{
protected CustomVerticalCell(IntPtr handle) : base(handle) { }
public CustomVerticalCell() : base(UITableViewCellStyle.Default, nameof(CustomVerticalCell)) { }
}
linker.xml を併用する例:
<linker>
<assembly fullname="MyApp">
<type fullname="MyApp.iOS.CustomVerticalCell" preserve="all" />
</assembly>
</linker>
MAUI 単一プロジェクトの .csproj に登録:
<ItemGroup>
<TrimmerRootDescriptor Include="linker.xml" />
</ItemGroup>
カスタムハンドラーから CollectionView 標準へ寄せる
将来互換性とメンテ性の観点から、iOS ネイティブのセルを直接触るのではなく、MAUI 標準の CollectionView を使い、必要に応じて DataTemplate と Template Selector で表現するのが安全です。
<CollectionView ItemsSource="{Binding Items}" ItemsLayout="VerticalList">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid Padding="12" ColumnDefinitions="Auto,*">
<Image WidthRequest="56" HeightRequest="56" Margin="0,0,12,0">
<Image.Source>
<UriImageSource Uri="{Binding ImageUrl}" CachingEnabled="True" CacheValidity="30.00:00:00" />
</Image.Source>
</Image>
<Label Grid.Column="1" Text="{Binding Title}" FontAttributes="Bold" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
バージョン整合:環境側の“地ならし”
ツールチェーンの不整合は、ランタイムのレジストラ差異や既知不具合に直結します。次を順に確認・更新してください。
- .NET SDK/MAUI Workload:
dotnet --list-sdksで SDK を確認 → 最新安定へ。dotnet workload updateとdotnet workload restoreを実行。 - Xcode:対象 iOS と一致する最新版へ更新し、選択を固定。
sudo xcode-select -s /Applications/Xcode.app、xcodebuild -versionを確認。 - iOS Target:
SdkVersionsとTargetFramework(例:net8.0-ios)の整合をとる。 - 証明書/プロビジョニング:実機だけ落ちる場合は署名の差異がトリガーになることがあるため、いったん再生成。
ログの取り方:原因切り分けの精度を上げる
クラッシュ直前のログを押さえると「FFImageLoading 由来」か「カスタムセル由来」かが高精度に判断できます。
- Xcode > Devices and Simulators から対象デバイスを選択し、Open Console でライブログを確認。
- 「marshal」「VerticalCell」「Objective‑C object」などでフィルタ。
- Managed 側例外が一緒に出る場合は、堆積したレンダラー初期化コードの見直しを優先。
実例に学ぶ“直った構成”の作り方
現場での再現しがちな構成から、安全側へ寄せるためのリファクタ例をまとめます。
Before:FFImageLoading 併用+カスタムセル
- 画像読み込みは
CachedImage(FFImageLoading)。 - iOS で
UITableViewCellを直接拡張、IntPtrコンストラクタ無し。 - Release/AOT のみクラッシュ。
After:標準 Image+CollectionView+コンストラクタ保護
- 画像は
Image+UriImageSource(キャッシュ有効)。 - リストは
CollectionViewに寄せる。やむを得ずカスタムセルを使う箇所は(IntPtr handle)を実装し、[Preserve]orlinker.xmlで保護。 - MAUI/Xcode/SDK を最新安定へ。ワークロード再インストール。
よくある質問(FAQ)
Q. シミュレーターでは落ちないのに実機だけ落ちます。
A. 実機 Release(AOT/トリミング)のみで顕在化するケースが多いです。トリミングで (IntPtr) コンストラクタが削除される、古いライブラリのネイティブ取り回しが AOT 下で壊れる、などが理由です。実機 Release を基準に検証しましょう。
Q. (IntPtr handle) を足すだけで必ず直りますか?
A. カスタム型が原因なら有効ですが、FFImageLoading やバージョン不整合が根因なら効果は限定的です。まず FFImageLoading を外し、環境整合を済ませた上で、なお再現する箇所に対してコンストラクタ実装+保護を行うのが再現性の高い順序です。
Q. 画像のキャッシュは本当に標準機能だけで足りますか?
A. 典型的なリスト・詳細画面での画像表示であれば、UriImageSource の CachingEnabled + CacheValidity で十分です。きめ細かなキャッシュ戦略や低メモリ端末での手動解放が必要な場合は、IImageSourceService の注入や HttpClient の再利用など、アプリ側の設計でカバーできます。
Q. MAUI のバージョンはいくつにすれば良い?
A. 原則として「最新の安定版」に合わせてください。旧版の既知不具合や、Xcode の API 変更に追随していないことが直接原因になります。ワークロード更新(dotnet workload update)を忘れないことが重要です。
検証の型(再現→修正→再確認)
- FFImageLoading を削除してビルド・デプロイ(Debug/Release 両方)。
クラッシュしなければ根因はライブラリ依存です。 - まだ落ちるなら MAUI / SDK / Xcode を最新安定に更新。
クリーンビルド、実機再起動、デバイスの再ペアリングも実施。 - 自作の iOS クラスを洗い出し、
(IntPtr handle)を追加。
Release/AOT で トリミング保護([Preserve]またはlinker.xml)。 - それでも再現する箇所は、標準の CollectionView へ置換して VerticalCell に依存しない設計へ寄せる。
設計の踏み抜きを避けるためのベストプラクティス
- 「標準で書けるものは標準で」:画像は
Image、リストはCollectionView。ネイティブセル直タッチは最後の手段。 - 「ランタイムの橋渡しを壊さない」:
(IntPtr)は iOS の世界との必須インターフェース。忘れない・消さない。 - 「Release 実機で必ず検証」:Debug シミュレーターのみ合格は信用しない。
- 「トリマーと仲良く」:未参照に見えるメンバーは消える。
[Preserve]とlinker.xmlをチーム規約に。 - 「外部ライブラリは“使わない勇気”」:メンテ停止ライブラリは早めに撤去。置換は段階的に。
“一枚で分かる”対応早見表
| 症状 | 高確度の原因 | やること(優先順) |
|---|---|---|
| 起動直後に VerticalCell で落ちる | FFImageLoading の介在 | FFImageLoading 削除 → クリーン → 再デプロイ |
| Release だけ落ちる/実機だけ落ちる | トリミングで (IntPtr) 消失 | (IntPtr handle) 追加 → [Preserve] or linker.xml |
| 端末や iOS バージョンで再現性がまちまち | MAUI・Xcode の不整合 | ワークロード更新/Xcode 更新/ターゲット揃え |
| 新規プロジェクトでは再現しない | 過去のレンダラー初期化コードが残存 | 初期化コードを全撤去/標準 Handler に統一 |
CI/CD とチーム開発での落とし穴
- ビルドエージェントの Xcode 固定:毎回
xcode-selectで明示指定。異なる Xcode が紛れ込むと症状がぶり返します。 - ワークロードのバージョンピン:
global.jsonで SDK を固定し、dotnet workload restoreを CI の最初に実行。 - リンカ記述子の共有:
linker.xmlはプロジェクトにコミットし、レビューの対象に。 - リグレッション検出:実機 Release の UI テストを最小でも1本通す。セル描画直後の画面を狙うのが効果的。
トラブル時の“深掘り”ポイント
ここまでで解消しない場合は、次を掘ってみてください。
- Managed <> Native の所有権:どちらが init を握るかで必要なコンストラクタが変わります。ネイティブから上がってくる型には
(IntPtr)が必須。 - 登録名(
[Register])の一致:ネイティブ側クラス名と[Register]名の不一致は致命傷です。nameof(Type)を使うとズレにくい。 - 拡張点の選び方:
CollectionViewの ItemTemplate で表現できることを、レンダラー/ハンドラーで無理に書かない。 - 例外の初出位置:最初の「marshal エラー」の直前ログに、実は
TypeLoadExceptionやMissingMethodExceptionが出ていることも。そこが真犯人です。
まとめ(結論だけ先に欲しい人へ)
- 第一手は FFImageLoading の撤去。標準
Image+UriImageSourceで十分。 - カスタムセルは
(IntPtr handle)実装+トリミング保護。ここが欠けると実機 Release で必ず揺れます。 - ツールチェーンの整合(MAUI/.NET/Xcode)を常に最新安定へ。ワークロード更新を習慣化。
- 根治策は「標準の CollectionView に寄せる」。VerticalCell の詳細はなるべく意識しない設計へ。
付記:MAUI の画像キャッシュ事情
2024 年後半以降の MAUI では、標準 Image と UriImageSource の組み合わせで自動キャッシュが強化されています。サーバ側のキャッシュ・ヘッダー設計(ETag/Cache-Control)と併せ、アプリ側の CacheValidity をバランスさせると、ユーザー体感速度とデータ使用量の両方を最適化できます。無闇に“永遠キャッシュ”にせず、サムネイルとフルサイズで有効期限を分けるのが実戦的です。
実装スニペット集(コピペで使える)
画像キャッシュの期限をバインドで制御
<Image>
<Image.Source>
<UriImageSource Uri="{Binding ImageUrl}"
CachingEnabled="True"
CacheValidity="{Binding CacheDays, Converter={StaticResource DaysToTimeSpanConverter}}" />
</Image.Source>
</Image>
型をトリマーから守る(属性版)
using System.Diagnostics.CodeAnalysis;
using Microsoft.Maui.Controls.Internals;
[Preserve(AllMembers = true)]
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicConstructors, typeof(CustomVerticalCell))]
public static class TrimmingRoots { }
型をトリマーから守る(linker.xml 版)
<linker>
<assembly fullname="MyApp">
<type fullname="MyApp.iOS.CustomVerticalCell" preserve="all" />
<type fullname="MyApp.iOS.CustomCollectionViewCell" preserve="all" />
</assembly>
</linker>
ワークロード更新とクリーン
dotnet workload update
dotnet clean
rd /s /q bin obj # Windows
rm -rf bin obj # macOS/Linux
最後に:再発させない運用
- 依存ライブラリの棚卸し:半年に一度は「メンテ中か/置換できるか」をレビュー。
- テンプレートの標準化:新しい画面は
CollectionView+標準Imageを初期値に。 - リリース前チェック:実機 Release で「大量セル描画→画像大量読み込み→スクロール」を踏む自動テストを1本入れる。
一行まとめ
FFImageLoading を抜き、標準 Image/CollectionView に寄せ、必要なら (IntPtr) を実装してトリマーから守る——これで「Failed to marshal … VerticalCell」は止まります。

コメント