
GeoDiveExa・GeoDiveMrg1・GeoDiveExa for GPS の iOS アプリ3本(.NET MAUI)を、.NET 8 から .NET 10 へ一気に移行しました。2世代ジャンプということで身構えていましたが、結果は C# ソースの修正わずか8行・ロジック変更ゼロ。とはいえ準備とハマりどころはそれなりにあったので、手順と注意点をまとめます。
なぜ .NET 9 を飛ばして .NET 10 か
当初は .NET 9 への移行で作業を始めましたが、途中で方針変更しました。理由は単純で、.NET 9 は STS(標準サポート)のため 2026年11月でサポート終了予定だからです。移行した瞬間にサポート切れのランタイムに乗ることが予想されます。
LTS である .NET 10 へ直行する場合、NuGet パッケージも最新世代がそのまま使えるという利点もあります。逆に .NET 9 に移行する場合は「net9 対応の最終バージョン」を依存関係から逆引きする必要があり、かえって手間でした。
準備: git worktree で移行用の隔離環境を作る
本線(master)の開発を止めずに移行作業をするため、git worktree で別フォルダ・別ブランチのチェックアウトを作りました。
git worktree add 1net10-code net10-code
この方式の利点:
- VS Code を2窓で並行運用できる。master のウィンドウと worktree のウィンドウを同時に開ける
- SDK もフォルダ単位で自動的に切り替わる。
global.jsonはフォルダごとに効くので、master 側は .NET 8 SDK、worktree 側は .NET 10 SDK を同居させたまま使い分けられる
{
"sdk": {
"version": "10.0.100",
"rollForward": "latestFeature"
}
}
rollForward: latestFeature を付けておくと、インストール済みの任意の 10.0.x を拾ってくれるので、SDK のパッチ更新のたびに global.json を書き換えずに済みます。
sparse-checkout 併用時の注意
worktree には git sparse-checkout(–no-cone)を併用して、移行に関係ないドキュメントやテストデータを除外しました。ここでひとつ罠があり、sparse-checkout 環境では git update-index --skip-worktree が効きません。skip-worktree ビットは sparse-checkout 機構が自前で管理しているため、手動で立てても即座にクリアされます。
Git LFS 管理のバイナリが「見かけ上 modified」になる問題には、skip-worktree ではなく git add で clean フィルタを通し直すことで対処しました(実体のハッシュがポインタと一致していれば index は変わらず、表示だけが解消されます)。
環境要件
| 項目 | バージョン |
|---|---|
| .NET SDK | 10.0.301 |
| maui ワークロード | 10.0.20 |
| Xcode | 26 系必須(net10.0-ios のビルド要件) |
# SDK インストール後
sudo dotnet workload install maui
csproj の変更
TargetFramework と DefineConstants
net8.0-ios → net10.0-ios の置換です。TargetFrameworks 本体のほか、条件式(Condition 属性)にも TFM がハードコードされているので、1ファイルあたり10箇所前後の置換になりました。手動で定義していた NET8_0 定数も NET10_0 へ。
<TargetFrameworks>net10.0-ios</TargetFrameworks>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net10.0-ios|AnyCPU'">
パッケージ更新
| パッケージ | .NET 8 時代 | .NET 10 対応版 |
|---|---|---|
| Microsoft.Maui.Controls / .Maps | 8.0.82 | 10.0.71 |
| CommunityToolkit.Maui | 8.0.0 | 14.2.0 |
| CommunityToolkit.Maui.Markup | 3.3.1 | 7.0.1 |
| System.Text.Encoding.CodePages | 8.0.0 | 削除 |
CommunityToolkit.Maui は 8 → 14 という大ジャンプですが、バージョン番号が速く進むだけで、TFM ごとの対応は NuGet の依存関係メタデータで機械的に確認できます(14.x が net10、12.x までが net9)。
System.Text.Encoding.CodePages は .NET 10 ではフレームワーク提供になったため、参照を残すと NU1510 警告(プルーニング対象)が出ます。PackageReference を削除するだけで、CodePagesEncodingProvider のコードはそのまま動きます。
コンパイルエラーは2種類だけ
3アプリ共通で出たエラーは次の2つでした。
1. CS0122: MessagingCenter がアクセス不能
MAUI 9 以降、MessagingCenter は internal 化されており、使っているとコンパイルが通りません。公式の移行先は CommunityToolkit.Mvvm の WeakReferenceMessenger です。
ただし今回は移行前に grep で送信側(Send)の有無を確認したところ、購読(Subscribe)だけ残って送信が存在しない死にコードと判明。WeakReferenceMessenger へ書き換えるのではなく、条件コンパイルで無効化するだけで済ませました。機械的に置換する前に「そもそも生きているコードか」を確認する価値はあります。
2. MCT001: UseMauiCommunityToolkit のチェーン必須
CommunityToolkit.Maui 14 には新しいアナライザが入っていて、UseMauiApp<T>() に .UseMauiCommunityToolkit() がチェーンされていないとエラー(警告ではなく)になります。
本アプリには「初期化失敗時に最小構成で立ち上げる」フォールバック経路があり、そこは意図的に Toolkit を外しています。この設計を維持するため、該当箇所だけ pragma で抑制しました。
// 最小限の設定のみ(意図的に CommunityToolkit なし)
#pragma warning disable MCT001
fallbackBuilder.UseMauiApp<App>()
.UseMauiMaps();
#pragma warning restore MCT001
C# ソースの変更はこの2種類(計8行)だけ。共有コード数万行は1行も変わっていません。条件コンパイル #if NET8_0 をソース内で使っていなかったことと、CommunityToolkit の利用が UseMauiCommunityToolkit() の1箇所だけ(Popup / Snackbar / Toast 不使用)だったことが効きました。
ビルド以外のハマりどころ
シェルスクリプトの TFM ハードコード
シミュレータ実行用スクリプトに -f net8.0-ios や成果物パス(bin/Debug/net8.0-ios/...)がハードコードされていて、ビルドは通るのにスクリプトが古い成果物を探しに行く状態になりました。csproj だけでなく ビルドスクリプト・VS Code の tasks.json も grep 対象に含めるべきです。
simctl launch の Bundle ID 不一致
移行後、xcrun simctl launch だけが失敗するアプリがありました(インストールは成功)。原因は移行とは無関係の元からの不整合で、csproj の ApplicationId と Info.plist の CFBundleIdentifier が食い違っている場合、実際のアプリには Info.plist 側が採用されるというもの。スクリプトが csproj 側の ID で launch しようとして空振りしていました。
simctl launch が FBSOpenApplicationServiceErrorDomain code=4 で落ちる場合は、ビルド成果物の Info.plist から実際の ID を確認するのが早道です:
/usr/libexec/PlistBuddy -c "Print CFBundleIdentifier" path/to/App.app/Info.plist
まとめ
| 項目 | 結果 |
|---|---|
| 対象 | .NET MAUI iOS アプリ ×3 |
| 移行幅 | .NET 8 → .NET 10(2世代) |
| C# ソース修正 | 2ファイル・8行(ロジック変更ゼロ) |
| 設定ファイル修正 | global.json、csproj ×4、スクリプト ×6、tasks.json |
| ビルド結果 | 3アプリとも 0 エラー、シミュレータ起動確認済み |
「2世代ジャンプ」という字面より、実際は TFM とパッケージバージョンの総取り替え+破壊的変更2件の対処という、見通しの立てやすい作業でした。鍵は事前調査で、(1) #if NET8_0 の使用箇所、(2) MessagingCenter など廃止 API の生死、(3) パッケージの TFM 対応表、の3点を先に grep / NuGet メタデータで潰しておくと、あとは機械的に進められます。
お願い
本記事の情報は参考目的で掲載しており、正確性・完全性を保証するものではありません。誤記・不正確な情報がございましたら、コメント欄よりご指摘いただければ、確認のうえ修正いたします。

コメントを残す