Skip to content

[Net11] [iOS/MacCatalyst] Migrate TabbedPage to handler architecture#36507

Draft
Tamilarasan-Paranthaman wants to merge 5 commits into
net11.0from
Net11.0-iOS-TabbedPage-Handler
Draft

[Net11] [iOS/MacCatalyst] Migrate TabbedPage to handler architecture#36507
Tamilarasan-Paranthaman wants to merge 5 commits into
net11.0from
Net11.0-iOS-TabbedPage-Handler

Conversation

@Tamilarasan-Paranthaman

Copy link
Copy Markdown
Member

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Description of Change

Replaces the monolithic TabbedRenderer (~600 lines, single class that IS a UITabBarController) with a layered TabbedViewHandler architecture for iOS and MacCatalyst. The handler is registered as the default for now; a feature flag opt-in can be introduced if needed.

Issues Fixed

Fixes #33082

Note: The shared Core infrastructure is designed for potential reuse by a future Shell handler, but Shell integration is not part of this PR)

Motivation

The TabbedRenderer is a single class that IS a UITabBarController. It owns all tab management, appearance, lifecycle, reordering, and page lifecycle in one file. This makes it:

  • Hard to maintain — changes to tab bar appearance risk breaking tab selection logic
  • Not reusable — other components that may need UITabBarController management can't reuse anything from the renderer
  • Inconsistent with handler architecture — every other control has moved to handlers; TabbedPage was still using the renderer on iOS
  • Prone to retain cycles — the renderer both IS the UIKit VC and holds event subscriptions, requiring careful manual cleanup

What Changed

New Files (Core layer — src/Core/)

File Purpose
Handlers/TabbedView/TabbedViewHandler.cs Cross-platform handler base: ViewHandler<ITabbedView, PlatformView>, Mapper + CommandMapper
Handlers/TabbedView/TabbedViewHandler.iOS.cs iOS handler: ITabBarManagerDelegate implementation, NativeSelectionInProgress directional sync flag, ConnectHandler/DisconnectHandler lifecycle
Handlers/TabbedView/ITabbedViewHandler.cs Handler interface contract extending IViewHandler
Platform/iOS/TabBarControllerManager/TabBarControllerManager.cs Shared UITabBarController manager: nested MauiTabBarController subclass, WeakReference delegate pattern, tab reordering disabled, iOS 18 DisableiOS18ToolbarTabs() fix, UpdateTabBarVisibility() for MacCatalyst
Platform/iOS/TabBarControllerManager/ITabBarManagerDelegate.cs 7-method interface bridging Core ↔ handler: OnTabSelected, OnViewDidAppear/Disappear, OnViewDidLayoutSubviews, OnTraitCollectionDidChange, OnTabsReordered, GetCurrentPageViewController
Core/ITabbedView.cs Marker interface for tabbed view contract (extends IView)

New Files (Controls layer — src/Controls/)

File Purpose
TabbedPage/TabbedPage.Mapper.cs RemapForControls() — registers 12 mapper methods + PlatformViewFactory for iOS/MacCatalyst
TabbedPage/TabbedPage.iOS.cs All iOS mapper implementations: MapItemsSource (incremental Add/Remove + full rebuild), MapCurrentPage (bidirectional sync), MapBarBackgroundColor/BarTextColor/SelectedTabColor/UnselectedTabColor, MapTranslucencyMode, status bar/home indicator forwarding, platform view factory, cleanup

Modified Files

Area File Change
Handler Registration AppHostBuilderExtensions.cs AddHandler<TabbedPage, TabbedViewHandler>() for iOS/MacCatalyst (unconditional)
Virtual View TabbedPage.cs Implements ITabbedView, adds _pendingPagesChangedArgs for incremental collection sync, OnHandlerChangingCore wires PagesChanged + page PropertyChanged events, PageTabAdapter inner class
Compatibility TabbedRenderer.cs Unchanged — remains in the Compatibility layer for manual fallback
Build targets Microsoft.Maui.Controls.targets Removed RuntimeHostConfigurationOption for UseiOSTabbedViewHandler (no longer needed)
Runtime RuntimeFeature.cs Removed UseiOSTabbedViewHandler feature switch (handler is now unconditional default)

Architecture Overview

Handler Hierarchy

┌──────────────────────────────────────────────────────────┐
│                    CONTROLS LAYER                        │
│  TabbedPage.Mapper.cs           → mappers + factory      │
│  TabbedPage.iOS.cs              → mapper implementations │
│  TabbedPage.cs                  → shared wiring          │
├──────────────────────────────────────────────────────────┤
│                     CORE LAYER                           │
│  TabbedViewHandler.iOS.cs       → lifecycle + delegate   │
│  TabBarControllerManager.cs     → shared UITabBarCtrl    │
│  ITabBarManagerDelegate.cs      → 7-method interface     │
└──────────────────────────────────────────────────────────┘

Key Design Decisions

Decision Rationale
Two-layer split (Core/Controls) Core owns UITabBarController management with no knowledge of Page, BarTextColor, or XAML. Controls injects its behavior via mapper methods and PlatformViewFactory. The Core layer is generic enough that it could potentially be reused by other consumers in the future.
TabBarControllerManager as shared class Extracted UITabBarController lifecycle (tab selection, view lifecycle events, tab reordering, iOS 18 compatibility) into a reusable manager. Currently used by TabbedViewHandler; the ITabBarManagerDelegate interface makes it possible for other consumers to adopt it in the future if needed.
MauiTabBarController nested subclass Intercepts SelectedViewController for tab selection sync. Overrides ViewDidAppear/ViewDidDisappear/ViewDidLayoutSubviews for lifecycle. Uses WeakReference<TabBarControllerManager> to prevent retain cycles.
NativeSelectionInProgress directional flag Breaks the selection sync loop deterministically. When UIKit fires SelectedViewController (user tap), the flag is true → sync native→virtual (read index, set CurrentPage). When CurrentPage changes programmatically, flag is false → sync virtual→native (set SelectedViewController).
Incremental collection changes Uses _previousPages HashSet to track pages across rebuilds. Add/Remove operations create/disconnect only affected pages. Full rebuild diffs against _previousPages to find removed pages. Avoids O(n) teardown+rebuild on every change.
PlatformViewFactory bridge The handler's CreatePlatformView() throws — the Controls layer overrides it via PlatformViewFactory to create and fully configure the TabBarControllerManager before the handler sees the platform view. This avoids circular assembly references.
Handler is unconditional default No feature flag. Registered in AppHostBuilderExtensions.cs. TabbedRenderer still exists for manual opt-out if needed.

ITabBarManagerDelegate Interface (7 methods)

Method Purpose
OnTabSelected(int index) User tapped a tab — sync native→virtual
OnTabsReordered(UIViewController[]) User reordered tabs in "More" tab editor
GetCurrentPageViewController() Returns current page's VC for status bar / home indicator
OnViewDidAppear() UIKit appeared — fire SendAppearing()
OnViewDidDisappear() UIKit disappeared — fire SendDisappearing()
OnViewDidLayoutSubviews() Layout pass — propagate frame via view.Arrange()
OnTraitCollectionDidChange() Trait collection changed — resize tab bar icons

Key Behaviors

Behavior Implementation
Tab selection sync Bidirectional via NativeSelectionInProgress flag — no implicit UIKit no-op reliance
Collection updates Incremental Add/Remove for simple changes; full rebuild with _previousPages diff for Reset/Replace/Move
Tab bar appearance iOS 15+ UITabBarAppearance API, fallback for older versions; supports color, brush, gradient, translucency
iOS 18 compatibility DisableiOS18ToolbarTabs() in MauiTabBarController constructor + UpdateTabBarVisibility() for MacCatalyst
Tab reordering disabled CustomizableViewControllers = null in ViewControllers setter (re-applied after every VC update)
Status bar / Home indicator MauiTabBarController overrides ChildViewControllerForStatusBarHidden/HomeIndicatorAutoHidden via injected GetCurrentPageViewControllerFunc callback
Cleanup Three-phase: OnHandlerChangingPartial (Controls events + UITabBarAppearance), DisconnectHandler (manager disposal), Manager.Dispose (UITabBarController disposal)

Feature Parity

Full parity with TabbedRenderer including:

  • Tab creation with title, icon, accessibility ID
  • Tab selection (user tap + programmatic CurrentPage)
  • Dynamic add/insert/remove pages at runtime
  • Bar background (color, brush, gradient)
  • Bar text color + selected/unselected tab colors
  • Translucency mode (Translucent / Opaque / Default)
  • iOS 15+ UITabBarAppearance styling
  • "More" tab for 5+ tabs (with reordering disabled)
  • Status bar hidden / Home indicator auto-hidden per page
  • Tab bar icon auto-resize on trait collection change
  • iOS 18 toolbar tab suppression
  • MacCatalyst tab bar visibility

Shared Infrastructure Summary

Component Current Consumer Reusable For Scope
TabBarControllerManager TabbedViewHandler Potentially reusable UITabBarController lifecycle, selection, reordering, iOS 18 fixes
MauiTabBarController Same Same Lifecycle callbacks, WeakReference delegate
ITabBarManagerDelegate Same Same 7-method interface bridging Core ↔ consumer

Note: The Core infrastructure (TabBarControllerManager, ITabBarManagerDelegate) is generic enough to be consumed by other components (e.g., a future Shell iOS handler) if needed, but no specific reuse scenario is currently planned. The interface may need adjustments based on future consumer requirements.

Handler as Default

The handler is registered as the unconditional default in AppHostBuilderExtensions.cs — no feature flag or opt-in required.

What Changes By Default

  • TabbedPage uses TabbedViewHandler instead of TabbedRenderer on iOS/MacCatalyst
  • Tab selection sync uses an explicit NativeSelectionInProgress flag instead of relying on UIKit's implicit setter no-op
  • Collection changes are incremental (Add/Remove) instead of always full rebuild
  • Cleanup is three-phase (Controls → Handler → Manager) instead of single-phase Dispose

How to Opt Out

If an app needs to fall back to the renderer, register it manually in MauiProgram.cs:

builder.ConfigureMauiHandlers(handlers =>
{
    handlers.AddHandler<TabbedPage, TabbedRenderer>();
});

TabbedRenderer remains in the Compatibility layer and continues to work.

Migration Guidance for Custom Renderer Users

If you subclassed TabbedRenderer or accessed its internals, this section maps the old patterns to the new handler architecture.

Old → New Mapping

Old (Renderer) New (Handler) Notes
TabbedRenderer (IS a UITabBarController) TabbedViewHandler (HAS a UITabBarController via TabBarControllerManager) Handler is a ViewHandler<ITabbedView, UIView>, not a VC
Override ViewDidAppear Subscribe to manager.ViewDidAppear event Fires via MauiTabBarController.ViewDidAppear → delegate → event
Override ViewDidDisappear Subscribe to manager.ViewDidDisappear event Same pattern
OnPropertyChanged switch Mapper methods in TabbedPage.iOS.cs Declarative — one method per property
SetControllers() full rebuild MapItemsSource() with incremental support Add/Remove changes are handled incrementally
this.TabBar (direct access) GetTabBar(handler) via manager Static helper method
this.SelectedViewController manager.SelectedViewController Through TabBarControllerManager
Dispose(bool) cleanup OnHandlerChangingPartial + DisconnectHandler Three-phase cleanup across layers

Adapter Pattern for Custom Renderers

If you must preserve custom renderer logic, keep using the renderer:

// Keep using TabbedRenderer with your customizations
builder.ConfigureMauiHandlers(handlers =>
{
    handlers.AddHandler<TabbedPage, MyCustomTabbedRenderer>();
});

Testing

  • All existing TabbedPage device tests pass on the handler path
  • Renderer-specific tests continue to work via manual renderer registration

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 36507

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 36507"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-controls-tabbedpage TabbedPage community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration platform/ios platform/macos macOS / Mac Catalyst

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants