DuoExtensions.Maui
0.2.0-preview.1
dotnet add package DuoExtensions.Maui --version 0.2.0-preview.1
NuGet\Install-Package DuoExtensions.Maui -Version 0.2.0-preview.1
<PackageReference Include="DuoExtensions.Maui" Version="0.2.0-preview.1" />
<PackageVersion Include="DuoExtensions.Maui" Version="0.2.0-preview.1" />
<PackageReference Include="DuoExtensions.Maui" />
paket add DuoExtensions.Maui --version 0.2.0-preview.1
#r "nuget: DuoExtensions.Maui, 0.2.0-preview.1"
#:package DuoExtensions.Maui@0.2.0-preview.1
#addin nuget:?package=DuoExtensions.Maui&version=0.2.0-preview.1&prerelease
#tool nuget:?package=DuoExtensions.Maui&version=0.2.0-preview.1&prerelease
<img src="assets/logo.png" alt="DuoExtensions.Maui logo" width="96" height="96" />
DuoExtensions.Maui
Adaptive toolbars in .NET MAUI Shell, edge-to-edge CollectionView scrolling, and window-aware hinge monitoring for iPhone Duo.
DuoExtensions.Maui makes selected UIKit APIs easier to use from MAUI through attached properties and a dependency-injected hinge service. Use XAML and C# to configure toolbar preferences, observe the vertical bar edge, and react to hinge status and angle without repeating the native plumbing in your app.
Experimental: this is a playground library, so use it with caution. APIs and implementation may change. If you encounter an issue, please get in touch or open an issue! Feedback and simpler approaches are welcome. Future .NET MAUI support may replace parts of this library; if useful, some of the functionality might eventually fit in the .NET MAUI Community Toolkit.
Features
| Feature | API | Scope |
|---|---|---|
| Toolbar item axis | ToolbarItemAxisBehavior.AxisBehavior |
Individual Shell toolbar action |
| Vertical bars | ToolbarLayout.VerticalBehavior |
Entire Shell |
| Bar compression | ToolbarLayout.CompressionBehavior |
Shell page |
| Vertical bar edge | ToolbarLayout.VerticalEdge (read-only) |
Shell page |
| Edge-to-edge scrolling | UseEdgeToEdgeCollectionView() |
App-wide iOS CollectionView handler |
| Hinge status and angle | IHingeService.ForWindow(Window) |
Any MAUI window; no Shell dependency |
The toolbar settings are system preferences, not custom toolbar rendering. UIKit determines the final layout.
Requirements
- .NET 10 and the .NET MAUI workload.
- The .NET for iOS SDK exposing the iOS 27.1 APIs.
- Xcode 27.1 and a compatible simulator or device for native testing.
- An iOS app targeting
net10.0-ios27.1to select the library's native implementation.
The library targets net10.0 and net10.0-ios27.1. Its iOS deployment minimum is 15.0, with new native APIs guarded at runtime. Toolbar extensions have no effect before iOS 27.1, and hinge monitoring reports unsupported.
The shared target keeps public types available in cross-platform apps, but Android, Mac Catalyst and Windows have no native implementation. Hinge API support indicates OS capability, not the presence of hinge hardware or an available reading.
Installation and setup
Install the latest NuGet package using:
dotnet add package DuoExtensions.Maui
Enable the integration in MauiProgram.cs:
using DuoExtensions.Maui;
builder
.UseMauiApp<App>()
.UseDuoExtensions();
This registers the iOS Shell renderer and the singleton IHingeService. You do not need to register or attach the hinge observer yourself.
For hinge monitoring without Shell (or without replacing a custom Shell renderer), use builder.UseHingeMonitoring(). For only Shell toolbars, use builder.UseDuoShellToolbars(). UseDuoExtensions() combines both.
Edge-to-edge CollectionView scrolling is a separate opt-in: add .UseEdgeToEdgeCollectionView() as shown below.
For XAML examples, declare:
xmlns:duo="clr-namespace:DuoExtensions.Maui;assembly=DuoExtensions.Maui"
The toolbar integration replaces the iOS
Shellrenderer registration. If your app already uses a custom Shell renderer, the implementations must be combined; registering multiple renderers does not compose them.
Toolbar item axis
Configure an individual action:
<ContentPage.ToolbarItems>
<ToolbarItem x:Name="ShareAction"
Text="Share"
IconImageSource="share.png"
duo:ToolbarItemAxisBehavior.AxisBehavior="Automatic" />
</ContentPage.ToolbarItems>
| Value | Meaning |
|---|---|
Automatic |
Let UIKit choose the action's placement. |
HorizontalOnly |
Restrict the action to horizontal bars. |
VerticalPreferred |
Support both axes, preferring a vertical bar when available. |
Change the preference at runtime:
ToolbarItemAxisBehavior.SetAxisBehavior(
ShareAction, ToolbarAxisBehavior.VerticalPreferred);
Explicitly set an initial value, including Automatic, on actions you want to update later. This opts the item into native tracking when Shell creates its toolbar. A first assignment made only after the toolbar is created is not guaranteed to be observed until Shell rebuilds it.
The tracker matches Shell-created native buttons using AutomationId, assigning one if necessary. Caller-supplied identifiers should be unique.
For explicitly configured actions, the tracker also copies ToolbarItem.Text to the native button title, including later text changes. This preserves labels in UIKit's overflow menu when the action has an icon.
HorizontalOnly specifies an axis, not top or bottom. On the tested iOS 27.1 Duo simulator, initial configuration and runtime transitions can produce different placement. Do not rely on an exact bar position.
Vertical bar behavior
Set the preference on your Shell:
<Shell ...
xmlns:duo="clr-namespace:DuoExtensions.Maui;assembly=DuoExtensions.Maui"
duo:ToolbarLayout.VerticalBehavior="Automatic">
Or change it in code:
ToolbarLayout.SetVerticalBehavior(
Shell.Current, ToolbarVerticalBehavior.Disabled);
Automaticlets UIKit choose whether vertical bars are appropriate.Disabledopts the entire Shell out of vertical bars.
This is Shell-wide, not page-local. A change remains active when switching tabs. Automatic does not force a vertical layout.
Vertical bar compression
Choose which group should retain space when tabs and actions share a crowded vertical bar:
<ContentPage ...
xmlns:duo="clr-namespace:DuoExtensions.Maui;assembly=DuoExtensions.Maui"
duo:ToolbarLayout.CompressionBehavior="PrefersToolbarItems">
ToolbarLayout.SetCompressionBehavior(
this, ToolbarVerticalCompressionBehavior.PrefersTabBar);
| Value | Meaning |
|---|---|
Automatic |
Let UIKit balance actions and tabs. |
PrefersToolbarItems |
Favor toolbar actions; tabs can be compressed first. |
PrefersTabBar |
Favor tabs; actions can be compressed first. |
This preference belongs to the page's navigation item. Compare it in a vertical layout with limited space: roomy or horizontal layouts may show no difference. UIKit decides the final overflow presentation.
Observe the vertical edge
VerticalEdge reports UIKit's trait; it does not move the bar.
<ContentPage x:Name="PageRoot" ...
xmlns:duo="clr-namespace:DuoExtensions.Maui;assembly=DuoExtensions.Maui">
<Label Text="{Binding Source={x:Reference PageRoot},
Path=(duo:ToolbarLayout.VerticalEdge)}" />
</ContentPage>
For code-based UI, read the current value and observe the page's PropertyChanged event for ToolbarLayout.VerticalEdgeProperty.PropertyName:
var edge = ToolbarLayout.GetVerticalEdge(this);
LeadingandTrailingare logical edges; account for right-to-left flow when drawing a physical direction.Unspecifiedmeans UIKit is not reporting a vertical edge for that page. Do not infer a side from screen orientation.
The tracker observes each page's native traits, including changes while a tab is cached.
Edge-to-edge CollectionView scrolling
On iPhone Duo, the title bar scrolls out of the way as the list moves up. With the page's top safe-area edge set to Container, the first item starts safely below the title, but a small gap can remain at the top while scrolling. Using None allows the list to reach the edge, but can leave the first item behind the title when the page opens.
The solution separates the viewport from the content insets: the viewport extends behind the system bars, while UIKit's automatic inset adjustment keeps the content's initial and final positions safe. Items can scroll behind the title and reach the screen edge as it disappears, without hard-coded bar heights or scroll-driven margins.
Enable the handler in MauiProgram.cs:
using DuoExtensions.Maui;
builder
.UseMauiApp<App>()
.UseDuoExtensions()
.UseEdgeToEdgeCollectionView();
UseEdgeToEdgeCollectionView() registers a handler for all iOS CollectionView controls in the app; it is not included in UseDuoExtensions(). On other targets, the extension method has no effect. If you already register a custom CollectionView handler, the implementations must be combined.
Use the following page layout, keeping the CollectionView as the page's direct content:
<ContentPage
...
SafeAreaEdges="Container, None"
BackgroundColor="White"
Shell.BackgroundColor="Transparent">
<CollectionView BackgroundColor="Transparent" ... >
...
</CollectionView>
</ContentPage>
SafeAreaEdges="Container, None" retains the horizontal container safe area and removes the vertical page safe area. The native scroll view's automatic insets provide the safe content position instead.
Hinge monitoring
Hinge monitoring works with or without Shell. Inject the singleton IHingeService into your page and obtain the observer for its window when the page appears:
using DuoExtensions.Maui;
public sealed class HingePage : ContentPage
{
private readonly IHingeService _service;
private readonly Label _reading = new();
private IHingeObserver? _observer;
public HingePage(IHingeService service)
{
_service = service;
Content = _reading;
}
protected override void OnAppearing()
{
base.OnAppearing();
_observer = _service.ForWindow(Window
?? throw new InvalidOperationException("The page must belong to a window."));
_observer.StateChanged += OnStateChanged;
ShowReading(_observer.CurrentState);
}
protected override void OnDisappearing()
{
if (_observer is not null)
{
_observer.StateChanged -= OnStateChanged;
_observer = null;
}
base.OnDisappearing();
}
private void OnStateChanged(object? sender, HingeStateChangedEventArgs e) =>
ShowReading(e.State);
private void ShowReading(HingeState? state)
{
_reading.Text = !_service.IsSupported
? "Hinge API unsupported."
: state is null
? "No hinge reading available."
: $"{state.Status}: {state.AngleDegrees:F1}\u00B0";
}
}
Reading semantics and lifetime
HingeStatecontainsStatusandAngleDegrees. Status isUnknown,Closed,PartiallyOpenorFullyOpen.CurrentStateis nullable: no reading is not equivalent to a closed hinge or a zero-degree angle.StateChangedalso fires when a previous reading becomes unavailable, withState = null.- Call
ForWindowon that window's UI thread. Events are delivered on that thread. - The same window receives the same shared observer. Consumers unsubscribe from their own events; they do not dispose the observer.
- Monitoring continues across page navigation and ends when the window is destroyed. Read
CurrentStateafter subscribing so a returning page immediately displays the latest state.
One physical hinge, why one observer per window?
UIKit delivers hinge information through UIHingeInteraction, attached to a view hierarchy, rather than a global device sensor. Two windows can describe the same physical hinge while having different data availability and lifetimes.
The singleton service provides a convenient app-wide access point, while ForWindow(Window) preserves that context. An unavailable reading from one window must not erase a valid reading for another. Passing the window explicitly also avoids an ambiguous global "current window" in multi-window apps.
Demo
The DuoExtensions-Demo app references the library and contains six Shell tabs:
- Axis: change a Share action's axis preference.
- Compression: compare preferences using eight distinct action icons.
- Vertical: toggle the Shell-wide vertical bar preference.
- Edge: display the live edge with a direction arrow.
- Hinge: display status and angle using constructor injection.
- Edge-to-Edge: scroll a CollectionView behind the title bar with automatic content insets.
The demo targets the iOS 27.1 SDK but supports deployment to iOS 15.0 and later.
How it works
- A custom iOS Shell renderer supplies the Shell-wide vertical preference.
- A Shell page tracker applies compression to
UINavigationItem, maps configured toolbar actions to existingUIBarButtonIteminstances, and observes page traits. - The hinge service attaches a
UIHingeInteractionto each observed native window and manages handler replacement and destruction. - The edge-to-edge CollectionView handler enables automatic UIKit content insets and extends the owning controller's top layout behind opaque bars, restoring that layout when the view unloads.
The library uses native UIKit behavior. Toolbar integration currently depends on MAUI's compatibility Shell renderer infrastructure.
Feedback
Found an unexpected layout, a lifecycle issue, or a simpler integration? Please open an issue with the .NET MAUI, iOS and Xcode versions, the device or simulator configuration, and steps to reproduce it. Contributions and suggestions are welcome!
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-ios27.1 is compatible. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Maui.Controls (>= 10.0.110)
-
net10.0-ios27.1
- Microsoft.Maui.Controls (>= 10.0.110)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.2.0-preview.1 | 0 | 11/11/2026 |
| 0.1.0-preview.1 | 93 | 11/3/2026 |