Skip to content

Commit 2fdb58c

Browse files
Vetle444Copilot
andauthored
Use native iOS navigation bar for BottomSheet and some bug fixes for both platforms (#821)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent e28d3d0 commit 2fdb58c

File tree

10 files changed

+216
-46
lines changed

10 files changed

+216
-46
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## [55.4.0]
2+
- [iOS][BottomSheet] Use native UINavigationBar for bottom sheet header with centered title, system close/back buttons, and proper blur behavior
3+
- [Android][BottomSheet] Fixed edge-to-edge constraints not applying until scroll when start Positioning is Large
4+
- [iOS][BottomSheet] Fixed bottom sheet not automatically scrolling to top when Positioning is Large
5+
16
## [55.3.0]
27
- [SearchPage] Added `ScrollableHeader` property to allow consumers to provide a header that scrolls with search results.
38
- [SearchPage] Replaced `OnLoaded`/`OnUnloaded` event subscriptions with `OnHandlerChanged` override.

src/app/Components/ComponentsSamples/BottomSheets/Sheets/SimpleBottomSheet.xaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
xmlns:dui="http://dips.com/mobile.ui"
44
xmlns:sheets="clr-namespace:Components.ComponentsSamples.BottomSheets.Sheets"
55
xmlns:header="clr-namespace:DIPS.Mobile.UI.Components.BottomSheets.Header;assembly=DIPS.Mobile.UI"
6-
x:Class="Components.ComponentsSamples.BottomSheets.Sheets.SimpleBottomSheetView">
6+
x:Class="Components.ComponentsSamples.BottomSheets.Sheets.SimpleBottomSheetView"
7+
Positioning="Large">
78

89
<dui:BottomSheet.BottomSheetHeaderBehavior>
910
<header:BottomSheetHeaderBehavior IsBackButtonVisible="{Binding Source={x:Reference Switch}, Path=IsToggled}" />

src/library/DIPS.Mobile.UI/Components/BottomSheets/Android/BottomSheetFragment.cs

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ public override Dialog OnCreateDialog(Bundle? savedInstanceState)
102102
if (Build.VERSION.SdkInt >= BuildVersionCodes.VanillaIceCream)
103103
{
104104
m_bottomSheetBehavior.AddBottomSheetCallback(new EdgeToEdgeBottomSheetCallback(this));
105+
106+
// Edge-to-edge is only supported on API 35+. On API 34 and below, the bottom sheet
107+
// must not go over the status bar.
108+
// Fix for edge-to-edge: Remove top insets so BottomSheet can draw behind status bar
109+
// See: https://github.com/material-components/material-components-android/issues/3389
110+
bottomSheetDialog.Window?.AddFlags(WindowManagerFlags.LayoutNoLimits);
105111
}
106112
}
107113

@@ -141,16 +147,6 @@ public Task Show()
141147
public override void OnStart()
142148
{
143149
m_bottomSheet.SendOpen();
144-
145-
// Edge-to-edge is only supported on API 35+. On API 34 and below, the bottom sheet
146-
// must not go over the status bar.
147-
if (Build.VERSION.SdkInt >= BuildVersionCodes.VanillaIceCream)
148-
{
149-
// Fix for edge-to-edge: Remove top insets so BottomSheet can draw behind status bar
150-
// See: https://github.com/material-components/material-components-android/issues/3389
151-
Dialog?.Window?.AddFlags(WindowManagerFlags.LayoutNoLimits);
152-
}
153-
154150
base.OnStart();
155151
}
156152

@@ -214,6 +210,25 @@ private void OnBottomSheetPositioningChanged(Positioning positioning)
214210
}
215211
}
216212

213+
internal void ApplyEdgeToEdgePadding()
214+
{
215+
if (m_bottomSheetLayout == null || m_statusBarHeight == 0 || Dialog is not BottomSheetDialog bottomSheetDialog)
216+
return;
217+
218+
// The BottomSheetDialog exposes the bottom sheet container view via its Behavior's parent
219+
var bottomSheetView = bottomSheetDialog.Behavior?.PeekHeight >= 0
220+
? (m_rootLayout?.Parent as AView)
221+
: null;
222+
if (bottomSheetView == null)
223+
return;
224+
225+
var location = new int[2];
226+
bottomSheetView.GetLocationOnScreen(location);
227+
var bottomSheetTop = location[1];
228+
var overlap = Math.Max(0, m_statusBarHeight - bottomSheetTop);
229+
m_bottomSheetLayout.SetPadding(0, overlap, 0, overlap);
230+
}
231+
217232
private class OnApplyWindowInsetsListener : Java.Lang.Object, IOnApplyWindowInsetsListener
218233
{
219234
private readonly BottomSheetFragment _fragment;
@@ -227,6 +242,11 @@ public WindowInsetsCompat OnApplyWindowInsets(AView v, WindowInsetsCompat insets
227242
{
228243
var statusBarInsets = insets.GetInsets(WindowInsetsCompat.Type.StatusBars());
229244
_fragment.m_statusBarHeight = statusBarInsets.Top;
245+
246+
// Apply padding immediately — when opening in Large positioning the slide/state
247+
// callbacks may have already fired before insets arrived.
248+
_fragment.ApplyEdgeToEdgePadding();
249+
230250
return insets;
231251
}
232252
}
@@ -260,7 +280,14 @@ public override void OnSlide(AView bottomSheet, float slideOffset)
260280

261281
public override void OnStateChanged(AView bottomSheet, int newState)
262282
{
263-
// No action needed on state change
283+
if (_fragment.m_bottomSheetLayout == null || _fragment.m_statusBarHeight == 0)
284+
return;
285+
286+
var location = new int[2];
287+
bottomSheet.GetLocationOnScreen(location);
288+
var bottomSheetTop = location[1];
289+
var overlap = Math.Max(0, _fragment.m_statusBarHeight - bottomSheetTop);
290+
_fragment.m_bottomSheetLayout.SetPadding(0, overlap, 0, overlap);
264291
}
265292
}
266293
}

src/library/DIPS.Mobile.UI/Components/BottomSheets/Header/BottomSheetHeader.cs renamed to src/library/DIPS.Mobile.UI/Components/BottomSheets/Android/BottomSheetHeader.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,8 @@ public BottomSheetHeader(BottomSheet bottomSheet)
2424

2525
BackgroundColor = Colors.GetColor(ColorName.color_surface_default);
2626
ColumnSpacing = Sizes.GetSize(SizeName.content_margin_medium);
27-
28-
var topPadding = Sizes.GetSize(SizeName.content_margin_medium);
29-
30-
#if __ANDROID__
31-
topPadding = 0;
32-
#endif
3327

34-
Padding = new Thickness(Sizes.GetSize(SizeName.content_margin_medium), topPadding, Sizes.GetSize(SizeName.content_margin_medium), Sizes.GetSize(SizeName.content_margin_medium));
28+
Padding = new Thickness(Sizes.GetSize(SizeName.content_margin_medium), 0, Sizes.GetSize(SizeName.content_margin_medium), Sizes.GetSize(SizeName.content_margin_medium));
3529

3630
AddColumnDefinition(new ColumnDefinition(GridLength.Star));
3731
AddColumnDefinition(new ColumnDefinition(GridLength.Auto));

src/library/DIPS.Mobile.UI/Components/BottomSheets/iOS/BottomSheetContainer.cs

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using DIPS.Mobile.UI.API.Library;
2-
using DIPS.Mobile.UI.Components.BottomSheets.Header;
32
using Microsoft.Maui.Platform;
43
using UIKit;
54
using Colors = DIPS.Mobile.UI.Resources.Colors.Colors;
@@ -9,29 +8,24 @@ namespace DIPS.Mobile.UI.Components.BottomSheets.iOS;
98
internal class BottomSheetContainer : Grid
109
{
1110
private readonly BottomSheet m_bottomSheet;
12-
private BottomSheetHeader m_bottomSheetHeader;
1311

1412
public BottomSheetContainer(BottomSheet bottomSheet)
1513
{
1614
m_bottomSheet = bottomSheet;
1715

1816
BackgroundColor = bottomSheet.BackgroundColor;
1917

20-
AddRowDefinition(new RowDefinition(GridLength.Auto));
2118
AddRowDefinition(new RowDefinition(GridLength.Auto));
2219
AddRowDefinition(new RowDefinition(GridLength.Star));
23-
24-
m_bottomSheetHeader = new BottomSheetHeader(m_bottomSheet);
25-
this.Add(m_bottomSheetHeader);
2620

27-
this.Add(bottomSheet, 0, 2);
21+
this.Add(bottomSheet, 0, 1);
2822
}
2923

3024
public void ModifySearchbar(bool add)
3125
{
3226
if (add)
3327
{
34-
this.Add(m_bottomSheet.SearchBar, 0, 1);
28+
this.Add(m_bottomSheet.SearchBar, 0, 0);
3529
}
3630
else
3731
{
@@ -67,9 +61,4 @@ private static void SetConstraints(UIView rootView, UIView uiView)
6761
uiView.HeightAnchor.ConstraintEqualTo(rootView.Frame.Height),
6862
]);
6963
}
70-
71-
public void SetSemanticFocusToHeader()
72-
{
73-
m_bottomSheetHeader.SetSemanticFocus();
74-
}
7564
}

src/library/DIPS.Mobile.UI/Components/BottomSheets/iOS/BottomSheetHandler.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
using Microsoft.Maui.Handlers;
2+
using UIKit;
23

34
namespace DIPS.Mobile.UI.Components.BottomSheets;
45

56
public partial class BottomSheetHandler : ContentViewHandler
67
{
78
public static partial void MapIsInteractiveCloseable(BottomSheetHandler handler, BottomSheet bottomSheet)
89
{
9-
bottomSheet.ViewController.ModalInPresentation = !bottomSheet.IsInteractiveCloseable;
10+
var controller = (UIViewController?)bottomSheet.ViewController.NavigationController ?? bottomSheet.ViewController;
11+
controller.ModalInPresentation = !bottomSheet.IsInteractiveCloseable;
1012
}
1113

1214
private static partial void MapIsDraggable(BottomSheetHandler arg1, BottomSheet arg2)
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
using DIPS.Mobile.UI.Components.BottomSheets.Header;
2+
using DIPS.Mobile.UI.Resources.LocalizedStrings.LocalizedStrings;
3+
using Microsoft.Maui.Platform;
4+
using UIKit;
5+
using Colors = DIPS.Mobile.UI.Resources.Colors.Colors;
6+
7+
namespace DIPS.Mobile.UI.Components.BottomSheets.iOS;
8+
9+
internal class BottomSheetNavigationBarHelper
10+
{
11+
private readonly BottomSheet m_bottomSheet;
12+
private readonly UINavigationItem m_navigationItem;
13+
private readonly WeakReference<UINavigationController?> m_weakNavigationController;
14+
15+
public BottomSheetNavigationBarHelper(BottomSheet bottomSheet, UINavigationItem navigationItem, UINavigationController? navigationController)
16+
{
17+
m_bottomSheet = bottomSheet;
18+
m_navigationItem = navigationItem;
19+
m_weakNavigationController = new WeakReference<UINavigationController?>(navigationController);
20+
21+
if (bottomSheet.BottomSheetHeaderBehavior is not null)
22+
{
23+
bottomSheet.BottomSheetHeaderBehavior.PropertyChanged += OnHeaderBehaviorPropertyChanged;
24+
}
25+
}
26+
27+
public void Configure()
28+
{
29+
m_navigationItem.Title = m_bottomSheet.Title;
30+
31+
var appearance = new UINavigationBarAppearance();
32+
appearance.ConfigureWithOpaqueBackground();
33+
appearance.BackgroundColor = Colors.GetColor(ColorName.color_surface_default).ToPlatform();
34+
appearance.ShadowColor = UIColor.Clear;
35+
appearance.TitleTextAttributes = new UIStringAttributes
36+
{
37+
ForegroundColor = Colors.GetColor(ColorName.color_text_default).ToPlatform()
38+
};
39+
m_navigationItem.StandardAppearance = appearance;
40+
m_navigationItem.ScrollEdgeAppearance = appearance;
41+
42+
UpdateBarItems();
43+
}
44+
45+
public void UpdateTitle()
46+
{
47+
m_navigationItem.Title = m_bottomSheet.Title;
48+
}
49+
50+
private void OnHeaderBehaviorPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
51+
{
52+
if (e.PropertyName is nameof(BottomSheetHeaderBehavior.IsBackButtonVisible)
53+
or nameof(BottomSheetHeaderBehavior.IsCloseButtonVisible)
54+
or nameof(BottomSheetHeaderBehavior.IsVisible)
55+
or nameof(BottomSheetHeaderBehavior.TitleAndBackButtonContainerCommand)
56+
or nameof(BottomSheetHeaderBehavior.CloseButtonCommand))
57+
{
58+
MainThread.BeginInvokeOnMainThread(UpdateBarItems);
59+
}
60+
}
61+
62+
private void UpdateBarItems()
63+
{
64+
var headerBehavior = m_bottomSheet.BottomSheetHeaderBehavior;
65+
m_weakNavigationController.TryGetTarget(out var navigationController);
66+
67+
if (headerBehavior is not null && !headerBehavior.IsVisible)
68+
{
69+
navigationController?.SetNavigationBarHidden(true, false);
70+
return;
71+
}
72+
73+
navigationController?.SetNavigationBarHidden(false, false);
74+
75+
// Close button (right)
76+
if (headerBehavior?.IsCloseButtonVisible ?? true)
77+
{
78+
var closeButton = new UIBarButtonItem( UIBarButtonSystemItem.Close, (_, _) => OnCloseButtonTapped());
79+
closeButton.AccessibilityLabel = DUILocalizedStrings.Close;
80+
m_navigationItem.RightBarButtonItem = closeButton;
81+
}
82+
else
83+
{
84+
m_navigationItem.RightBarButtonItem = null;
85+
}
86+
87+
// Back button (left)
88+
if (headerBehavior?.IsBackButtonVisible ?? false)
89+
{
90+
var backImage = UIImage.GetSystemImage("chevron.left");
91+
var backButton = new UIBarButtonItem(backImage, UIBarButtonItemStyle.Plain, (_, _) =>
92+
{
93+
m_bottomSheet.BottomSheetHeaderBehavior?.TitleAndBackButtonContainerCommand?.Execute(null);
94+
});
95+
backButton.AccessibilityLabel = DUILocalizedStrings.Back;
96+
m_navigationItem.LeftBarButtonItem = backButton;
97+
}
98+
else
99+
{
100+
m_navigationItem.LeftBarButtonItem = null;
101+
}
102+
}
103+
104+
private void OnCloseButtonTapped()
105+
{
106+
if (m_bottomSheet.BottomSheetHeaderBehavior?.CloseButtonCommand is not null)
107+
{
108+
// Pass the close action as parameter — the consumer decides if/when to invoke it
109+
m_bottomSheet.BottomSheetHeaderBehavior.CloseButtonCommand.Execute((Action)(() => m_bottomSheet.Close()));
110+
}
111+
else
112+
{
113+
m_bottomSheet.Close();
114+
}
115+
}
116+
117+
public void Dispose()
118+
{
119+
if (m_bottomSheet.BottomSheetHeaderBehavior is not null)
120+
{
121+
m_bottomSheet.BottomSheetHeaderBehavior.PropertyChanged -= OnHeaderBehaviorPropertyChanged;
122+
}
123+
}
124+
}

src/library/DIPS.Mobile.UI/Components/BottomSheets/iOS/BottomSheetService.cs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,17 @@ internal async static partial Task PlatformOpen(BottomSheet bottomSheet)
1212
try
1313
{
1414
var bottomSheetViewController = new BottomSheetViewController(bottomSheet);
15+
var navigationController = new UINavigationController(bottomSheetViewController);
1516

1617
var currentViewController = Platform.GetCurrentUIViewController();
1718
if (currentViewController is null)
1819
return;
1920

20-
bottomSheetViewController.ModalPresentationStyle = bottomSheet.IsDraggable ? UIModalPresentationStyle.PageSheet : UIModalPresentationStyle.FullScreen;
21+
navigationController.ModalPresentationStyle = bottomSheet.IsDraggable ? UIModalPresentationStyle.PageSheet : UIModalPresentationStyle.FullScreen;
2122

22-
TryAddGrabberAndSetSheetPresentationProperties(bottomSheetViewController, bottomSheetViewController);
23+
TryAddGrabberAndSetSheetPresentationProperties(navigationController, bottomSheetViewController);
2324

24-
await currentViewController.PresentViewControllerAsync(bottomSheetViewController, true);
25+
await currentViewController.PresentViewControllerAsync(navigationController, true);
2526
}
2627
catch (Exception e)
2728
{
@@ -41,6 +42,24 @@ private static void TryAddGrabberAndSetSheetPresentationProperties(UIViewControl
4142
presentationController.PrefersScrollingExpandsWhenScrolledToEdge = true;
4243
presentationController.Delegate = new BottomSheetControllerDelegate { BottomSheetViewController = bottomSheetViewController };
4344
presentationController.PrefersEdgeAttachedInCompactHeight = true; // Makes sure its usable when rotated.
45+
46+
// Set initial detents before presentation so iOS animates directly to the correct position
47+
presentationController.Detents =
48+
[
49+
UISheetPresentationControllerDetent.CreateMediumDetent(),
50+
UISheetPresentationControllerDetent.CreateLargeDetent()
51+
];
52+
53+
switch (bottomSheetViewController.BottomSheet.Positioning)
54+
{
55+
case Positioning.Large:
56+
presentationController.SelectedDetentIdentifier = UISheetPresentationControllerDetentIdentifier.Large;
57+
break;
58+
case Positioning.Medium:
59+
presentationController.SelectedDetentIdentifier = UISheetPresentationControllerDetentIdentifier.Medium;
60+
break;
61+
// Fit: custom detent is set later via SetPositioning when the container is available
62+
}
4463
}
4564

4665
public async static partial Task Close(BottomSheet bottomSheet, bool animated)

0 commit comments

Comments
 (0)