DuoExtensions.Maui 0.2.0-preview.1

This is a prerelease version of DuoExtensions.Maui.
dotnet add package DuoExtensions.Maui --version 0.2.0-preview.1
                    
NuGet\Install-Package DuoExtensions.Maui -Version 0.2.0-preview.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DuoExtensions.Maui" Version="0.2.0-preview.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DuoExtensions.Maui" Version="0.2.0-preview.1" />
                    
Directory.Packages.props
<PackageReference Include="DuoExtensions.Maui" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DuoExtensions.Maui --version 0.2.0-preview.1
                    
#r "nuget: DuoExtensions.Maui, 0.2.0-preview.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DuoExtensions.Maui@0.2.0-preview.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DuoExtensions.Maui&version=0.2.0-preview.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=DuoExtensions.Maui&version=0.2.0-preview.1&prerelease
                    
Install as a Cake Tool

<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.1 to 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 Shell renderer 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);
  • Automatic lets UIKit choose whether vertical bars are appropriate.
  • Disabled opts 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);
  • Leading and Trailing are logical edges; account for right-to-left flow when drawing a physical direction.
  • Unspecified means 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

  • HingeState contains Status and AngleDegrees. Status is Unknown, Closed, PartiallyOpen or FullyOpen.
  • CurrentState is nullable: no reading is not equivalent to a closed hinge or a zero-degree angle.
  • StateChanged also fires when a previous reading becomes unavailable, with State = null.
  • Call ForWindow on 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 CurrentState after 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 existing UIBarButtonItem instances, and observes page traits.
  • The hinge service attaches a UIHingeInteraction to 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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