.NET MAUI(iOS)で、特定ページだけ「スワイプで戻る(戻るジェスチャー)」を無効化したい…そんなときは NavigationPage と Shell で対処方法が異なります。本記事ではクラッシュ原因から、ページ単位の制御、iOS側カスタム実装まで、実務で使える手順とコードをまとめます。
.NET MAUI(iOS)で「戻るスワイプ」を止めたいときに最初に知っておくこと
.NET MAUI の iOS で起きる「画面左端からスワイプすると前のページに戻ってしまう」挙動は、MAUI 独自のジェスチャーというより、iOS の UINavigationController が持つ “インタラクティブな戻る” の仕組みによるものです。つまり、止めるべき対象は UINavigationController の InteractivePopGestureRecognizer(さらに iOS 26 以降は InteractiveContentPopGestureRecognizer)になります。
ただし、MAUI のナビゲーション方式が NavigationPage なのか Shell なのかで、取得できる ViewController 構造が変わり、同じコードがそのまま動きません。特に Shell では「キャストしたら落ちる」ケースが多いので、最初に方式を切り分けるのが近道です。
| ナビゲーション方式 | 戻るスワイプの止め方 | 「特定ページだけ」止めやすさ | よくある落とし穴 |
|---|---|---|---|
| NavigationPage(Push/Pop) | 表示中ページのタイミングで UINavigationController.InteractivePopGestureRecognizer を無効化 | 比較的やりやすい(OnAppearing/Handler でページ単位に切替) | ViewController の取り方が雑だと null / 別VCで効かない |
| Shell(GoToAsync) | iOS 側で ShellRenderer / ShellSectionRenderer を拡張して無効化 | 工夫が必要(表示中ページを見て Enabled を切替) | Platform.GetCurrentUIViewController() が UINavigationController ではなくキャストでクラッシュ |
なお、Microsoft のドキュメントでも NavigationPage と Shell は別物として整理されており、Shell アプリで NavigationPage を併用しようとすると例外になる旨が明記されています。アプリ全体が Shell なら Shell 側の対策に寄せるのが基本です。
なぜ「戻るスワイプ」を無効化したいのか(要件を言語化すると実装がブレない)
戻るスワイプを止めたい場面は、単なる好みではなく UX とデータ整合性の要件であることが多いです。たとえば次のようなケースです。
- ウィザード形式(入力→確認→決済)の途中で、戻られると状態が壊れる/二重送信のリスクがある
- サインアップや本人確認など、戻ると不正な遷移になりやすいフロー
- 編集画面で未保存の変更があり、戻る前に確認ダイアログを必ず出したい
- 「戻る」はアプリ独自の条件でのみ許可し、プログラム制御に統一したい
ここでポイントは、ユーザーの戻る操作そのものを全面禁止するのではなく、必要なページで「戻る」をアプリのロジックに沿って制御することです。戻るスワイプだけを止めても、ナビバーの戻るボタンが残っていれば同じ問題が起きるため、後半で「戻る操作をプログラム制御に寄せる」追加対策も紹介します。
NavigationPage の場合:ページ表示中だけ戻るスワイプを止める
NavigationPage で Push している構成なら、iOS ネイティブ側で UINavigationController を掴み、InteractivePopGestureRecognizer.Enabled を切り替えるのが一番シンプルです。
最小実装(OnAppearing で無効化)
「このページでは戻るスワイプを止めたい」という場合、ページ側で OnAppearing を使う方法が分かりやすいです。ポイントは “UINavigationController への固定キャストを避ける” ことです。Platform.GetCurrentUIViewController() は状況によって別型を返すことがあるため、まずは NavigationController プロパティ経由で辿る方が安全です。
using Microsoft.Maui.ApplicationModel;
#if IOS
using UIKit;
#endif
public partial class NoSwipeBackPage : ContentPage
{
protected override void OnAppearing()
{
base.OnAppearing();
#if IOS
var nav = GetNavigationController();
if (nav != null)
{
// iOS標準の「戻るスワイプ(画面端)」を無効化
nav.InteractivePopGestureRecognizer.Enabled = false;
// iOS 26 以降の「コンテンツ戻るスワイプ(画面中から)」にも注意(後述)
DisableInteractiveContentPopIfAvailable(nav);
}
#endif
}
protected override void OnDisappearing()
{
base.OnDisappearing();
#if IOS
// 次のページでは元に戻したい場合は再度有効化する
var nav = GetNavigationController();
if (nav != null)
{
nav.InteractivePopGestureRecognizer.Enabled = true;
EnableInteractiveContentPopIfAvailable(nav);
}
#endif
}
#if IOS
static UINavigationController? GetNavigationController()
{
var vc = Platform.GetCurrentUIViewController();
if (vc is UINavigationController nav)
return nav;
return vc?.NavigationController;
}
static void DisableInteractiveContentPopIfAvailable(UINavigationController nav)
{
// コンパイル時に API が無い環境でも落ちないようにリフレクションで安全にアクセスする
var prop = nav.GetType().GetProperty("InteractiveContentPopGestureRecognizer");
if (prop?.GetValue(nav) is UIGestureRecognizer gr)
gr.Enabled = false;
}
static void EnableInteractiveContentPopIfAvailable(UINavigationController nav)
{
var prop = nav.GetType().GetProperty("InteractiveContentPopGestureRecognizer");
if (prop?.GetValue(nav) is UIGestureRecognizer gr)
gr.Enabled = true;
}
#endif
}
この実装は「特定の1ページだけ無効化」に向いています。一方で、ページごとに同じコードを書くのが面倒なら、次の “共通化” を検討します。
共通化のコツ:ベースページ化 or Handler でページ単位に注入
実務では、戻るスワイプを止めたい画面は少数(決済、同意、本人確認など)であることが多いです。次のどちらかに寄せると保守が楽になります。
- ベースページ化:
NoSwipeBackPageを作り、対象ページはそれを継承する - Handler で注入:ページの Handler 生成タイミングで iOS ネイティブ側を調整する(UIコードを iOS プロジェクト側に寄せられる)
Handler 注入は「各ページに OnAppearing を書きたくない」場合に便利です。MAUI は Handler を通じてネイティブ View を触れるため、Mapper.PrependToMapping でページ生成時に処理を差し込めます。
// MauiProgram.cs など(iOS 条件コンパイル推奨)
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
public static class SwipeBackConfig
{
public static void Configure()
{
#if IOS
PageHandler.Mapper.PrependToMapping("DisableSwipeBack", (handler, view) =>
{
// 対象ページだけ無効化したい
if (view is NoSwipeBackPage)
{
var navigationController = handler?.PlatformView?.GetNavigationController();
if (navigationController != null)
{
navigationController.InteractivePopGestureRecognizer.Enabled = false;
}
}
});
#endif
}
}
この方式のメリットは、iOS 固有コードをページ側に置かず、プラットフォーム設定としてまとめられる点です。反面、MAUI の内部構造やライフサイクルの影響を受けやすいので、トラブル時は「どのタイミングで nav が取れているか」をデバッガで確認してください。
Shell の場合:同じやり方が通用しない理由(キャストで落ちる)
Shell ナビゲーション(Shell.Current.GoToAsync)では、Platform.GetCurrentUIViewController() が UINavigationController ではないケースがあります。たとえば ShellFlyoutRenderer のような型になり得るため、NavigationPage と同じ感覚で (UINavigationController)Platform.GetCurrentUIViewController() と書くと Specified cast is not valid でクラッシュします。
つまり Shell では「ページの OnAppearing で nav を掴む」よりも、Shell の iOS Renderer 側で “Shell が使っている NavigationController” を確実に掴む方針が安定します。次のセクションで、その定番パターンを紹介します。
Shell で戻るスワイプを無効化する:Custom ShellRenderer を使う
Shell では iOS 側の ShellRenderer を拡張し、CreateShellSectionRenderer をオーバーライドして独自の ShellSectionRenderer を返すのが分かりやすい実装です。ShellSectionRenderer は iOS のナビゲーションスタックと近い位置にあるため、ここで InteractivePopGestureRecognizer.Enabled を制御できます。
| 配置場所(例) | ファイル | 役割 |
|---|---|---|
Platforms/iOS/Handlers | CustomShellRenderer.cs | ShellRenderer を拡張し、独自の SectionRenderer を作る |
Platforms/iOS/Handlers | CustomShellSectionRenderer.cs | iOS の戻るスワイプを無効化(必要ならページ単位で切替) |
| ルートプロジェクト | MauiProgram.cs | iOS だけハンドラー登録を行う |
手順1:iOS 用 CustomShellRenderer を作成
#if IOS
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Handlers.Compatibility;
using Microsoft.Maui.Controls.Platform.Compatibility;
namespace MyApp.Platforms.iOS.Handlers;
// ShellRenderer を拡張して、独自の ShellSectionRenderer を差し替える
public class CustomShellRenderer : ShellRenderer
{
protected override IShellSectionRenderer CreateShellSectionRenderer(ShellSection shellSection)
=> new CustomShellSectionRenderer(this);
}
#endif
手順2:ShellSectionRenderer 側でスワイプバックを無効化
#if IOS
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Platform.Compatibility;
namespace MyApp.Platforms.iOS.Handlers;
public class CustomShellSectionRenderer : ShellSectionRenderer
{
public CustomShellSectionRenderer(IShellContext context) : base(context) { }
public override void ViewDidLoad()
{
base.ViewDidLoad();
// Shell 全体で戻るスワイプを無効化(まずはこれが最小)
InteractivePopGestureRecognizer.Enabled = false;
// iOS 26 以降の追加ジェスチャーにも備える(後述)
DisableInteractiveContentPopIfAvailable();
}
void DisableInteractiveContentPopIfAvailable()
{
// ShellSectionRenderer 自体が UINavigationController なので “自分” から取れる場合が多い
var prop = GetType().GetProperty("InteractiveContentPopGestureRecognizer");
if (prop?.GetValue(this) is UIKit.UIGestureRecognizer gr)
gr.Enabled = false;
}
}
#endif
手順3:MauiProgram.cs で登録(iOS だけ)
MAUI では “従来のカスタムレンダラー登録” ではなく、ConfigureMauiHandlers で登録するのが基本です。登録対象は Shell でも良いですが、実務では自作の AppShell 型に限定する方が意図が明確です。
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureMauiHandlers(handlers =>
{
#if IOS
// Shell 全体ではなく AppShell にだけ適用する例
handlers.AddHandler(typeof(AppShell), typeof(MyApp.Platforms.iOS.Handlers.CustomShellRenderer));
#endif
});
return builder.Build();
}
}
ここまでで「Shell アプリで戻るスワイプが効かなくなる」状態になります。次は “特定ページだけ無効化” の実装に進みます。
Shell で「特定ページだけ」戻るスワイプを無効化する
Shell では ShellSectionRenderer がページ遷移を管理しているため、表示中ページが変わったタイミングで Enabled を切り替えるのが自然です。たとえば、ページ側に “戻るジェスチャーを許可するか” を持たせ、iOS 側がそれを参照します。
方法:添付プロパティ(Attached Property)でページにフラグを持たせる
以下は「ページごとに戻るスワイプを許可/禁止したい」場合に扱いやすい設計です。既定は true(許可)にしておき、止めたいページだけ false を指定します。
// 例:CustomController/BackGesture.cs(共有プロジェクト側に置いてOK)
namespace MyApp.CustomController;
public static class BackGesture
{
public static readonly BindableProperty IsEnabledProperty =
BindableProperty.CreateAttached(
"IsEnabled",
typeof(bool),
typeof(BackGesture),
true);
public static bool GetIsEnabled(BindableObject obj) => (bool)obj.GetValue(IsEnabledProperty);
public static void SetIsEnabled(BindableObject obj, bool value) => obj.SetValue(IsEnabledProperty, value);
}
そして、CustomShellSectionRenderer で表示中ページの変更を検知し、添付プロパティ値に合わせてスワイプバックを切り替えます。
#if IOS
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Platform.Compatibility;
using MyApp.CustomController;
namespace MyApp.Platforms.iOS.Handlers;
public class CustomShellSectionRenderer : ShellSectionRenderer
{
Page? _displayedPage;
public CustomShellSectionRenderer(IShellContext context) : base(context) { }
protected override void OnDisplayedPageChanged(Page page)
{
base.OnDisplayedPageChanged(page);
// 同じページなら何もしない
if (_displayedPage == page) return;
_displayedPage = page;
UpdateBackGesture();
}
void UpdateBackGesture()
{
var enabled = _displayedPage != null
? BackGesture.GetIsEnabled(_displayedPage)
: true;
InteractivePopGestureRecognizer.Enabled = enabled;
UpdateInteractiveContentPopIfAvailable(enabled);
}
void UpdateInteractiveContentPopIfAvailable(bool enabled)
{
var prop = GetType().GetProperty("InteractiveContentPopGestureRecognizer");
if (prop?.GetValue(this) is UIKit.UIGestureRecognizer gr)
gr.Enabled = enabled;
}
}
#endif
ページ側の指定例(XAML / C#)
<ContentPage
x:Class="MyApp.Pages.PaymentPage"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:cc="clr-namespace:MyApp.CustomController">
<!-- 決済画面では戻るスワイプを禁止 -->
<cc:BackGesture.IsEnabled>false</cc:BackGesture.IsEnabled>
<!-- ページ内容 -->
// C# から指定する場合
using MyApp.CustomController;
public partial class PaymentPage : ContentPage
{
public PaymentPage()
{
InitializeComponent();
BackGesture.SetIsEnabled(this, false);
}
}
この形にしておくと、「このページは無効」「このページは有効」という設計が XAML で読み取れ、後から見直したときに迷いにくいのがメリットです。
「戻る操作はプログラム制御だけにしたい」場合の追加対策
戻るスワイプを止めても、ナビバーの戻るボタンが残っていると “戻れてしまう” ため、要件によっては追加の対策が必要です。
| 目的 | NavigationPage | Shell |
|---|---|---|
| 戻るボタンを非表示にする | NavigationPage.SetHasBackButton(page, false) | Shell.SetBackButtonBehavior(page, new BackButtonBehavior { IsVisible = false }) |
| 戻る処理を自前コマンドに統一 | 画面内に「戻る/キャンセル」ボタンを置き、必要条件を満たしたら PopAsync() | BackButtonBehavior.Command を設定し、条件に応じて GoToAsync("..") |
とくに入力途中の編集画面では、「戻る前に確認ダイアログを出す」「未保存なら保存を促す」といった制御が必要になりがちです。戻る操作を “アプリのイベント” として扱うなら、UI で必ず代替導線(戻る/閉じる)を用意するのが UX 的にも安全です。
よくあるつまずきと解決策(ShellRenderer が見つからない等)
| 症状 | 原因の例 | 解決のヒント |
|---|---|---|
Specified cast is not valid で落ちる | Shell なのに UINavigationController へキャストしている | Shell は ShellRenderer/ShellSectionRenderer 側で制御する |
CustomShellRenderer could not be found | 名前空間が違う/iOS 以外でもコンパイルされている | 完全修飾名を使う、#if IOS で囲む、Platforms/iOS 配下に置く |
| 無効化したはずなのに、別ページでも戻れなくなる | 有効化に戻す処理が無い/グローバルに無効化している | OnDisappearing で復帰、または「表示中ページ」で Enabled を切替 |
| iOS 26 端末で戻るスワイプが残る | InteractiveContentPopGestureRecognizer 側が有効のまま | iOS 26 以降は両方の gesture を無効化する |
iOS 26 以降の注意:InteractiveContentPopGestureRecognizer も無効化対象
最近の iOS では、従来の “画面端からの戻るスワイプ” に加えて、コンテンツ領域でも戻れるタイプのジェスチャーが追加されたという報告があります。Apple のドキュメントにも interactiveContentPopGestureRecognizer が記載されており、iOS 26 以降ではこちらも無効化しないと期待通りにならないケースが出てきます。
ただし、古い SDK でビルドしているとプロパティが存在しない可能性があるため、本記事では “リフレクションで安全にアクセスする” 例を載せました。iOS 26 SDK(net-ios 26.0 系)に合わせているプロジェクトなら、型安全にプロパティへアクセスしても構いません。
まとめ(実務で迷ったらここだけ見ればOK)
- NavigationPage:表示中ページで
UINavigationController.InteractivePopGestureRecognizer.Enabledを切替。必要なら離脱時に元へ戻す。 - Shell:
Platform.GetCurrentUIViewController()のキャストではなく、ShellRenderer/ShellSectionRendererを拡張して制御する。 - 特定ページだけ:Shell なら
OnDisplayedPageChangedで「表示中ページのフラグ」を見て Enabled を切替、NavigationPage ならベースページ化や Handler 注入が現実的。 - プログラム制御に統一:戻るスワイプ停止に加えて、戻るボタンの表示/挙動も合わせて設計する。

コメント