.NET MAUI の TabbedPage は「各タブのタイトル表示」がネイティブ(Android の TabLayout / BottomNavigationView、iOS の UITabBar)に委ねられるため、FontFamily や FontSize をまとめて揃えたいときに一筋縄ではいきません。本記事では、Android の回避策、iOS で詰まりやすい理由と現実的な落としどころ(代替案)を、実装例つきで整理します。
なぜ TabbedPage の「タブタイトル」は揃えにくいのか
TabbedPage 配下の各 ContentPage に付けた Title は、プラットフォームごとに「タブのラベル」としてネイティブ部品へ渡されます。ここで重要なのは、MAUI が “全プラットフォーム共通でフォントを指定する API” を用意していない点です。そのため、見た目(フォントファミリー・サイズ)を統一したい場合は、基本的にプラットフォーム別のネイティブ API を叩く方向になります。
| 揃えたい文字 | 表示場所 | 代表的な実現方法 | 向き/不向き |
|---|---|---|---|
| タブ(TabBar)の文字 | 画面上部/下部のタブ | Android/iOS でネイティブ側をカスタム(Handler/Compatibility Renderer) | 厳密に揃えたい人向け。保守コスト高め。 |
| ページタイトルの文字 | ナビゲーションバー(上部) | NavigationPage.TitleView / Shell.TitleView で Label を配置 | 「見た目の統一」を低コストで達成しやすい(ただしタブ文字は変わらない)。 |
まず整理:TabbedPage のタブは「上部タブ」と「下部タブ」で中身が違う
Android では TabbedPage のタブ表示が、構成や設定によって以下のように変わります。
- 上部タブ:主に
TabLayout(Material) - 下部タブ:主に
BottomNavigationView(Material)
同じ「タブタイトル」でも、触るべきネイティブ部品が違うため、実装を流用するとハマりやすいです(選択状態で文字サイズが変わる・ラベルが2つ存在する等)。
Android:回避策で「タブタイトルのフォント」を上書きする
Android は比較的やりやすく、Handler/Mapper、もしくは Compatibility Renderer を使った回避策がよく採られます。代表例として「TabLayout 内の TextView を拾って Typeface を差し替える」方法が紹介されています。
Android 実装例(Handler 寄り):TabLayout の TextView に Typeface を適用
ポイントは次の3つです。
- フォントを Android リソースとして読める場所に置く(例:
Platforms/Android/Resources/raw) - タブの TextView を列挙して
Typefaceを設定する - 内部構造依存(リフレクション等)になりがちなので、MAUI 更新で壊れる可能性を織り込む
参考実装イメージ(概念を掴む用):
using Android.Widget;
using Google.Android.Material.Tabs;
using Microsoft.Maui.Handlers;
using System.Reflection;
namespace YourApp.Platforms.Android;
public class MyTabbedViewHandler : TabbedViewHandler
{
public override void SetVirtualView(IView view)
{
base.SetVirtualView(view);
SetTabFont(view);
}
private bool SetTabFont(IView view)
{
try
{
// TabbedPage内部のTabLayoutへ辿る(内部実装依存)
FieldInfo managerField = typeof(TabbedPage)
.GetField("_tabbedPageManager", BindingFlags.NonPublic | BindingFlags.Instance);
object manager = managerField?.GetValue(view);
FieldInfo tabLayoutField = manager?.GetType()
.GetField("_tabLayout", BindingFlags.NonPublic | BindingFlags.Instance);
TabLayout tabLayout = tabLayoutField?.GetValue(manager) as TabLayout;
if (tabLayout == null) return false;
for (int i = 0; i < tabLayout.TabCount; i++)
{
var tab = tabLayout.GetTabAt(i);
var tabView = tab?.View;
for (int j = 0; j < (tabView?.ChildCount ?? 0); j++)
{
var child = tabView.GetChildAt(j);
if (child is TextView tv)
{
tv.Typeface = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity
.Resources.GetFont(Resource.Raw.yourfontname);
tv.TextSize = 16; // 必要なら
}
}
}
return true;
}
catch
{
return false;
}
}
}
この系統の方法は「目的は達成しやすい」一方で、内部フィールド名やビュー階層が変わると壊れます。運用するなら、MAUI のバージョンアップ時に UI 自動テスト/実機確認をセットにしておくのがおすすめです。
Android の実務ポイント:選択状態で“別ラベル”が存在することがある
BottomNavigationView は、選択状態(Selected)と非選択状態(Normal)でラベルが2種類(小/大)持たれているケースがあります。片方だけ変えると「選択した瞬間にフォントが戻る」ような見え方になりがちです。実装では両方に同じフォント/サイズを入れるのがコツです。
| 症状 | 原因の典型 | 対策 |
|---|---|---|
| 選択すると文字だけサイズ/太さが変わる | 小ラベル/大ラベルが別 TextView | 小・大の両方に同じ指定を当てる |
| 初回だけ当たらず、画面回転や再表示で当たる | レイアウト完成前に触っている | OnGlobalLayout など「描画後」に適用する |
| タブ追加・差し替えで崩れる | タブ再生成時に既定に戻る | タブ構成が変わるタイミングで再適用する |
iOS:Handler/Mapper だけでやろうとして詰まりやすい(が、やり方を変えれば可能例もある)
iOS 側は「Handler で UITabBarController(または TabBar)を素直に取れない/想定通りに繋がらない」ことで詰まりやすく、実装方法によってはクラッシュや期待通りに反映されないことがあります。質問や検証の流れでも、MAUI は TabbedPage の見た目を共通 API で変えられず、ネイティブ API を呼ぶしかないという整理になっています。
一方で、Compatibility Renderer(TabbedRenderer)側で TabBar に触り、UITabBarAppearance の TitleTextAttributes を設定することで、フォント/サイズを変える実装例も共有されています。ここでは「Handler で頑張る」のではなく、TabbedRenderer を使って TabBar を直接いじるのが現実的です。
iOS 実装例(Compatibility Renderer):UITabBarAppearance でタイトル文字を統一
ポイントは次のとおりです。
TabbedRendererのTabBarを使うUITabBarAppearanceを作り、Normal/Selected の両方にフォントを入れる- iOS 15+ は
ScrollEdgeAppearanceも設定する - 表示後やページ切替後に再適用する(戻る現象の予防)
using Microsoft.Maui.Controls.Compatibility.Platform.iOS;
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Platform;
using UIKit;
namespace YourApp.Platforms.iOS;
public class CustomTabbedPageRenderer : TabbedRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
if (e.NewElement is TabbedPage) ApplyCustomTabBarStyle();
}
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
ApplyCustomTabBarStyle();
if (Element is TabbedPage tabbedPage)
{
tabbedPage.CurrentPageChanged -= OnCurrentPageChanged;
tabbedPage.CurrentPageChanged += OnCurrentPageChanged;
}
}
void OnCurrentPageChanged(object sender, EventArgs e) => ApplyCustomTabBarStyle();
void ApplyCustomTabBarStyle()
{
if (TabBar == null || Element is not TabbedPage tabbedPage) return;
var bg = tabbedPage.BarBackgroundColor?.ToPlatform() ?? UIColor.White;
var appearance = new UITabBarAppearance();
appearance.ConfigureWithOpaqueBackground();
appearance.BackgroundColor = bg;
appearance.StackedLayoutAppearance.Normal.TitleTextAttributes =
new UIStringAttributes { Font = UIFont.FromName("YourFont-Regular", 12) };
appearance.StackedLayoutAppearance.Selected.TitleTextAttributes =
new UIStringAttributes { Font = UIFont.FromName("YourFont-Bold", 14) };
TabBar.StandardAppearance = appearance;
if (UIDevice.CurrentDevice.CheckSystemVersion(15, 0))
TabBar.ScrollEdgeAppearance = appearance;
TabBar.BarTintColor = bg;
TabBar.BackgroundColor = bg;
}
}
登録は MauiProgram.cs で Compatibility Renderer として追加する形になります(例:handlers.AddCompatibilityRenderer(typeof(TabbedPage), typeof(CustomTabbedPageRenderer));)。
iOS のハマりどころ:FontFamily 名ではなく「PostScript 名」が必要なことがある
UIFont.FromName() は、MAUI の FontFamily で登録したエイリアスと一致しない場合があります。iOS はフォントの内部名(PostScript 名)を要求することがあるため、
- Info.plist へフォントを含めたうえで
- 実機で
UIFont.FamilyNames/UIFont.FontNamesForFamilyNameをログ出しして - 本当に使える名前を確認する
という手順が、地味ですが効きます。
「このやり方だと iOS は無理」に見える理由(そして現実的な整理)
iOS で詰まるパターンは大きく分けて2つです。
- TabbedPage を ViewHandler で自前生成しようとして、既存の MAUI 生成物(TabBar)と噛み合わず、期待したコントローラが取れない
- Renderer を Handler と同列に混ぜて登録し、
HandlerNotFound系の例外で落ちる(登録対象・基底クラスがズレている)
結論としては、iOS は「Handler/Mapper だけで綺麗に」よりも、「Compatibility Renderer で TabBar に直接触る」方が通りやすい、というのが現場での落としどころになりがちです。
補足:ナビゲーションバーのタイトルなら TitleView で統一できる(タブ文字とは別物)
「下部タブ(TabBar)の文字」ではなく、画面上部のナビゲーションバーに出ているタイトル文字を統一したいだけなら、プラットフォーム別の TabBar 改造に踏み込まなくても済みます。
NavigationPage.TitleView(または Shell.TitleView)でカスタム Label を置く
TitleView に Label を置けば、FontFamily/FontSize を XAML で素直に統一できます。Shell を使っている構成なら Shell.TitleView を使うのが定番です。
<Shell.TitleView>
<Label
Text="Settings"
FontFamily="YourFont"
FontSize="20"
FontAttributes="Bold"
VerticalTextAlignment="Center" />
</Shell.TitleView>
この方法のメリットは、
- クロスプラットフォームでほぼ同じ見た目を作れる
- タブの内部構造に依存しない(壊れにくい)
という点です。ただし、これはタブ(TabBar)のラベル自体は変わりません。「タブの文字まで同フォントにしたい」場合は、結局プラットフォーム別対応に戻る、という整理になります。
どの方針を選ぶべき?実務目線のおすすめ
| 要件 | おすすめ方針 | 理由 |
|---|---|---|
| とにかく全OSで同じフォント・同じサイズにしたい(タブ文字も) | Android/iOS で TabBar をネイティブカスタム | TabbedPage は共通APIがないため。 |
| 「上部タイトル」が揃えばOK(タブは多少妥協) | TitleView(NavigationPage/Shell) | 実装コストが低く、保守しやすい。 |
| デザインを強く作り込みたい(アニメーション/バッジ/長文折返しなど) | TabbedPage を捨ててカスタムTab(外部UI含む) | ネイティブ部品依存の限界を越えやすい。 |
よくある不具合と対処(“適用されたり戻ったり” 問題)
TabbedPage のタブは、ページ追加や CurrentPage 変更、レイアウト再計算のタイミングで再生成/再適用が走り、こちらが設定したフォントが戻ることがあります。実際に「適用したが戻る」系の報告もあり、対策としては適用タイミングの見直しが重要です。
- 描画後に当てる:Android は OnGlobalLayout 等、iOS は ViewDidAppear 後に再適用
- ページ切替のたびに当てる:CurrentPageChanged を購読して再適用
- “選択/非選択” の両方に当てる:片側だけだと状態変化で戻る
- 例外を握りつぶさずログを出す:ビュー階層が想定と違うときに原因が追える
まとめ:TabbedPage のタブタイトル統一は「割り切り」と「再適用」が鍵
- TabbedPage のタブタイトル(子 ContentPage の Title)はネイティブ描画で、共通APIだけではフォント統一できない
- Android は TabLayout/BottomNavigationView を直接触る回避策が現実的
- iOS は Handler で詰まりやすいが、Compatibility Renderer(TabbedRenderer)で UITabBarAppearance を設定する方法が通りやすい
- 「上部タイトルだけでOK」なら TitleView で統一するのが低コスト

コメント