[Net11] [iOS/MacCatalyst] Migrate TabbedPage to handler architecture#36507
Draft
Tamilarasan-Paranthaman wants to merge 5 commits into
Draft
[Net11] [iOS/MacCatalyst] Migrate TabbedPage to handler architecture#36507Tamilarasan-Paranthaman wants to merge 5 commits into
Tamilarasan-Paranthaman wants to merge 5 commits into
Conversation
Contributor
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 36507Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 36507" |
This was referenced Jul 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 aUITabBarController) with a layeredTabbedViewHandlerarchitecture 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
Motivation
The
TabbedRendereris a single class that IS aUITabBarController. It owns all tab management, appearance, lifecycle, reordering, and page lifecycle in one file. This makes it:UITabBarControllermanagement can't reuse anything from the rendererWhat Changed
New Files (Core layer —
src/Core/)Handlers/TabbedView/TabbedViewHandler.csViewHandler<ITabbedView, PlatformView>, Mapper + CommandMapperHandlers/TabbedView/TabbedViewHandler.iOS.csITabBarManagerDelegateimplementation,NativeSelectionInProgressdirectional sync flag,ConnectHandler/DisconnectHandlerlifecycleHandlers/TabbedView/ITabbedViewHandler.csIViewHandlerPlatform/iOS/TabBarControllerManager/TabBarControllerManager.csUITabBarControllermanager: nestedMauiTabBarControllersubclass,WeakReferencedelegate pattern, tab reordering disabled, iOS 18DisableiOS18ToolbarTabs()fix,UpdateTabBarVisibility()for MacCatalystPlatform/iOS/TabBarControllerManager/ITabBarManagerDelegate.csOnTabSelected,OnViewDidAppear/Disappear,OnViewDidLayoutSubviews,OnTraitCollectionDidChange,OnTabsReordered,GetCurrentPageViewControllerCore/ITabbedView.csIView)New Files (Controls layer —
src/Controls/)TabbedPage/TabbedPage.Mapper.csRemapForControls()— registers 12 mapper methods +PlatformViewFactoryfor iOS/MacCatalystTabbedPage/TabbedPage.iOS.csMapItemsSource(incremental Add/Remove + full rebuild),MapCurrentPage(bidirectional sync),MapBarBackgroundColor/BarTextColor/SelectedTabColor/UnselectedTabColor,MapTranslucencyMode, status bar/home indicator forwarding, platform view factory, cleanupModified Files
AppHostBuilderExtensions.csAddHandler<TabbedPage, TabbedViewHandler>()for iOS/MacCatalyst (unconditional)TabbedPage.csITabbedView, adds_pendingPagesChangedArgsfor incremental collection sync,OnHandlerChangingCorewiresPagesChanged+ pagePropertyChangedevents,PageTabAdapterinner classTabbedRenderer.csMicrosoft.Maui.Controls.targetsRuntimeHostConfigurationOptionforUseiOSTabbedViewHandler(no longer needed)RuntimeFeature.csUseiOSTabbedViewHandlerfeature switch (handler is now unconditional default)Architecture Overview
Handler Hierarchy
Key Design Decisions
UITabBarControllermanagement with no knowledge ofPage,BarTextColor, or XAML. Controls injects its behavior via mapper methods andPlatformViewFactory. The Core layer is generic enough that it could potentially be reused by other consumers in the future.TabBarControllerManageras shared classUITabBarControllerlifecycle (tab selection, view lifecycle events, tab reordering, iOS 18 compatibility) into a reusable manager. Currently used byTabbedViewHandler; theITabBarManagerDelegateinterface makes it possible for other consumers to adopt it in the future if needed.MauiTabBarControllernested subclassSelectedViewControllerfor tab selection sync. OverridesViewDidAppear/ViewDidDisappear/ViewDidLayoutSubviewsfor lifecycle. UsesWeakReference<TabBarControllerManager>to prevent retain cycles.NativeSelectionInProgressdirectional flagSelectedViewController(user tap), the flag istrue→ sync native→virtual (read index, setCurrentPage). WhenCurrentPagechanges programmatically, flag isfalse→ sync virtual→native (setSelectedViewController)._previousPagesHashSet to track pages across rebuilds.Add/Removeoperations create/disconnect only affected pages. Full rebuild diffs against_previousPagesto find removed pages. Avoids O(n) teardown+rebuild on every change.PlatformViewFactorybridgeCreatePlatformView()throws — the Controls layer overrides it viaPlatformViewFactoryto create and fully configure theTabBarControllerManagerbefore the handler sees the platform view. This avoids circular assembly references.AppHostBuilderExtensions.cs.TabbedRendererstill exists for manual opt-out if needed.ITabBarManagerDelegateInterface (7 methods)OnTabSelected(int index)OnTabsReordered(UIViewController[])GetCurrentPageViewController()OnViewDidAppear()SendAppearing()OnViewDidDisappear()SendDisappearing()OnViewDidLayoutSubviews()view.Arrange()OnTraitCollectionDidChange()Key Behaviors
NativeSelectionInProgressflag — no implicit UIKit no-op relianceAdd/Removefor simple changes; full rebuild with_previousPagesdiff forReset/Replace/MoveUITabBarAppearanceAPI, fallback for older versions; supports color, brush, gradient, translucencyDisableiOS18ToolbarTabs()inMauiTabBarControllerconstructor +UpdateTabBarVisibility()for MacCatalystCustomizableViewControllers = nullinViewControllerssetter (re-applied after every VC update)MauiTabBarControlleroverridesChildViewControllerForStatusBarHidden/HomeIndicatorAutoHiddenvia injectedGetCurrentPageViewControllerFunccallbackOnHandlerChangingPartial(Controls events + UITabBarAppearance),DisconnectHandler(manager disposal),Manager.Dispose(UITabBarController disposal)Feature Parity
Full parity with
TabbedRendererincluding:CurrentPage)UITabBarAppearancestylingShared Infrastructure Summary
TabBarControllerManagerTabbedViewHandlerUITabBarControllerlifecycle, selection, reordering, iOS 18 fixesMauiTabBarControllerWeakReferencedelegateITabBarManagerDelegateHandler as Default
The handler is registered as the unconditional default in
AppHostBuilderExtensions.cs— no feature flag or opt-in required.What Changes By Default
TabbedPageusesTabbedViewHandlerinstead ofTabbedRendereron iOS/MacCatalystNativeSelectionInProgressflag instead of relying on UIKit's implicit setter no-opDisposeHow to Opt Out
If an app needs to fall back to the renderer, register it manually in
MauiProgram.cs:TabbedRendererremains in the Compatibility layer and continues to work.Migration Guidance for Custom Renderer Users
If you subclassed
TabbedRendereror accessed its internals, this section maps the old patterns to the new handler architecture.Old → New Mapping
TabbedRenderer(IS aUITabBarController)TabbedViewHandler(HAS aUITabBarControllerviaTabBarControllerManager)ViewHandler<ITabbedView, UIView>, not a VCViewDidAppearmanager.ViewDidAppeareventMauiTabBarController.ViewDidAppear→ delegate → eventViewDidDisappearmanager.ViewDidDisappeareventOnPropertyChangedswitchTabbedPage.iOS.csSetControllers()full rebuildMapItemsSource()with incremental supportAdd/Removechanges are handled incrementallythis.TabBar(direct access)GetTabBar(handler)via managerthis.SelectedViewControllermanager.SelectedViewControllerTabBarControllerManagerDispose(bool)cleanupOnHandlerChangingPartial+DisconnectHandlerAdapter Pattern for Custom Renderers
If you must preserve custom renderer logic, keep using the renderer:
Testing