Skip to content

Commit d8cf48d

Browse files
VladislavAntonyukTheCodeTravelerne0rrmatrix
authored
Fix Snackbar layout #1901 (#2456)
* Fix Snackbar layout #1901 * fix build * Update variable names * Show `DisplayCustomSnackbarButton` without an achor `DisplayCustomSnackbarButtonWithAndchor` is used to demonstrate setting an Anchor on a Snackbar * Add missed constraint logic in AlertView * Move `DisplaySnackbarButtonAnchoredToButtonOnBottom` to Bottom of Page, Update Displayed text * Extract `bool shouldFillAndExpandHorizontally` from `Initialize()` * Fix Button.Text copy/paste error * Update samples/CommunityToolkit.Maui.Sample/Pages/Alerts/SnackbarPage.xaml.cs * Fix exception on constraint, fix modal navigation --------- Co-authored-by: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Co-authored-by: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Co-authored-by: James Crutchley <ne0rmatrix@gmail.com>
1 parent a4a50e9 commit d8cf48d

File tree

7 files changed

+121
-80
lines changed

7 files changed

+121
-80
lines changed

samples/CommunityToolkit.Maui.Sample/Pages/Alerts/SnackbarPage.xaml

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
<pages:BasePage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
33
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
44
xmlns:pages="clr-namespace:CommunityToolkit.Maui.Sample.Pages"
5+
xmlns:alertPages="clr-namespace:CommunityToolkit.Maui.Sample.Pages.Alerts"
56
xmlns:mct="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
6-
x:Class="CommunityToolkit.Maui.Sample.Pages.Alerts.SnackbarPage"
77
xmlns:vm="clr-namespace:CommunityToolkit.Maui.Sample.ViewModels.Alerts"
8+
x:Class="CommunityToolkit.Maui.Sample.Pages.Alerts.SnackbarPage"
89
x:TypeArguments="vm:SnackbarViewModel"
910
x:DataType="vm:SnackbarViewModel">
1011

@@ -14,26 +15,34 @@
1415
</ResourceDictionary>
1516
</pages:BasePage.Resources>
1617

17-
<VerticalStackLayout Spacing="12">
18-
19-
<Label Text="The Snackbar is a timed alert that appears at the bottom of the screen by default. It is dismissed after a configurable duration of time. Snackbar is fully customizable and can be anchored to any IView."
20-
LineBreakMode = "WordWrap" />
18+
<Grid RowSpacing="12"
19+
RowDefinitions="70,20,40,40,40,20">
20+
<Label Grid.Row="0"
21+
Text="The Snackbar is a timed alert that appears at the bottom of the screen by default. It is dismissed after a configurable duration of time. Snackbar is fully customizable and can be anchored to any IView."
22+
HorizontalTextAlignment="Justify"
23+
LineBreakMode = "WordWrap" />
2124

22-
<Label Text="Windows uses toast notifications to display snackbar. Make sure you switched off Focus Assist."
25+
<Label Grid.Row="1"
26+
Text="NOTE: Windows uses toast notifications to display snackbar. Be sure you've switched off Focus Assist."
2327
IsVisible="{OnPlatform Default='false', WinUI='true'}"/>
2428

25-
<Button Clicked="DisplayDefaultSnackbarButtonClicked"
26-
Text="Display Default Snackbar"/>
29+
<Button Grid.Row="2"
30+
Clicked="DisplayDefaultSnackbarButtonClicked"
31+
Text = "Display Default Snackbar"/>
2732

28-
<Button x:Name="DisplayCustomSnackbarButton"
29-
Clicked="DisplayCustomSnackbarButtonClicked"
30-
TextColor="{Binding Source={RelativeSource Self}, Path=BackgroundColor, Converter={StaticResource ColorToColorForTextConverter}, x:DataType=Button}"/>
33+
<Button Grid.Row="3"
34+
x:Name="DisplayCustomSnackbarButtonAnchoredToButton"
35+
Clicked="DisplayCustomSnackbarAnchoredToButtonClicked"
36+
Text="{x:Static alertPages:SnackbarPage.DisplayCustomSnackbarText}"
37+
TextColor="{Binding Source={RelativeSource Self}, Path=BackgroundColor, Converter={StaticResource ColorToColorForTextConverter}, x:DataType=Button}"/>
3138

32-
<Button x:Name="DisplaySnackbarInModalButton"
39+
<Button Grid.Row="4"
40+
x:Name="DisplaySnackbarInModalButton"
3341
Text="Show Snackbar in Modal Page"
3442
Clicked="DisplaySnackbarInModalButtonClicked"/>
3543

36-
<Label x:Name="SnackbarShownStatus" />
37-
</VerticalStackLayout>
44+
<Label Grid.Row="5"
45+
x:Name="SnackbarShownStatus" />
46+
</Grid>
3847

3948
</pages:BasePage>

samples/CommunityToolkit.Maui.Sample/Pages/Alerts/SnackbarPage.xaml.cs

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,21 @@ namespace CommunityToolkit.Maui.Sample.Pages.Alerts;
1212

1313
public partial class SnackbarPage : BasePage<SnackbarViewModel>
1414
{
15-
const string displayCustomSnackbarText = "Display a Custom Snackbar, Anchored to this Button";
15+
public const string DisplayCustomSnackbarText = "Display Custom Snackbar";
1616
const string dismissCustomSnackbarText = "Dismiss Custom Snackbar";
17-
readonly IReadOnlyList<Color> colors = typeof(Colors)
17+
18+
readonly IReadOnlyList<Color> colors = [.. typeof(Colors)
1819
.GetFields(BindingFlags.Static | BindingFlags.Public)
1920
.ToDictionary(c => c.Name, c => (Color)(c.GetValue(null) ?? throw new InvalidOperationException()))
20-
.Values.ToList();
21+
.Values];
2122

2223
ISnackbar? customSnackbar;
2324

2425
public SnackbarPage(SnackbarViewModel snackbarViewModel) : base(snackbarViewModel)
2526
{
2627
InitializeComponent();
2728

28-
DisplayCustomSnackbarButton.Text = displayCustomSnackbarText;
29+
DisplayCustomSnackbarButtonAnchoredToButton.Text = DisplayCustomSnackbarText;
2930

3031
Snackbar.Shown += Snackbar_Shown;
3132
Snackbar.Dismissed += Snackbar_Dismissed;
@@ -34,9 +35,9 @@ public SnackbarPage(SnackbarViewModel snackbarViewModel) : base(snackbarViewMode
3435
async void DisplayDefaultSnackbarButtonClicked(object? sender, EventArgs args) =>
3536
await this.DisplaySnackbar("This is a Snackbar.\nIt will disappear in 3 seconds.\nOr click OK to dismiss immediately");
3637

37-
async void DisplayCustomSnackbarButtonClicked(object? sender, EventArgs args)
38+
async void DisplayCustomSnackbarAnchoredToButtonClicked(object? sender, EventArgs args)
3839
{
39-
if (DisplayCustomSnackbarButton.Text is displayCustomSnackbarText)
40+
if (DisplayCustomSnackbarButtonAnchoredToButton.Text is DisplayCustomSnackbarText)
4041
{
4142
var options = new SnackbarOptions
4243
{
@@ -52,20 +53,20 @@ async void DisplayCustomSnackbarButtonClicked(object? sender, EventArgs args)
5253
"This is a customized Snackbar",
5354
async () =>
5455
{
55-
await DisplayCustomSnackbarButton.BackgroundColorTo(colors[Random.Shared.Next(colors.Count)], length: 500);
56-
DisplayCustomSnackbarButton.Text = displayCustomSnackbarText;
56+
await DisplayCustomSnackbarButtonAnchoredToButton.BackgroundColorTo(colors[Random.Shared.Next(colors.Count)], length: 500);
57+
DisplayCustomSnackbarButtonAnchoredToButton.Text = DisplayCustomSnackbarText;
5758
},
5859
FontAwesomeIcons.Microsoft,
5960
TimeSpan.FromSeconds(30),
6061
options,
61-
DisplayCustomSnackbarButton);
62+
DisplayCustomSnackbarButtonAnchoredToButton);
6263

6364
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
6465
await customSnackbar.Show(cts.Token);
6566

66-
DisplayCustomSnackbarButton.Text = dismissCustomSnackbarText;
67+
DisplayCustomSnackbarButtonAnchoredToButton.Text = dismissCustomSnackbarText;
6768
}
68-
else if (DisplayCustomSnackbarButton.Text is dismissCustomSnackbarText)
69+
else if (DisplayCustomSnackbarButtonAnchoredToButton.Text is dismissCustomSnackbarText)
6970
{
7071
if (customSnackbar is not null)
7172
{
@@ -75,11 +76,11 @@ async void DisplayCustomSnackbarButtonClicked(object? sender, EventArgs args)
7576
customSnackbar.Dispose();
7677
}
7778

78-
DisplayCustomSnackbarButton.Text = displayCustomSnackbarText;
79+
DisplayCustomSnackbarButtonAnchoredToButton.Text = DisplayCustomSnackbarText;
7980
}
8081
else
8182
{
82-
throw new NotSupportedException($"{nameof(DisplayCustomSnackbarButton)}.{nameof(ITextButton.Text)} Not Recognized");
83+
throw new NotSupportedException($"{nameof(DisplayCustomSnackbarButtonAnchoredToButton)}.{nameof(ITextButton.Text)} Not Recognized");
8384
}
8485
}
8586

@@ -97,6 +98,20 @@ async void DisplaySnackbarInModalButtonClicked(object? sender, EventArgs e)
9798
{
9899
if (Application.Current?.Windows[0].Page is Page mainPage)
99100
{
101+
var button = new Button()
102+
.CenterHorizontal()
103+
.Text("Display Snackbar");
104+
button.Command = new AsyncRelayCommand(token => button.DisplaySnackbar(
105+
"This Snackbar is anchored to the button on the bottom to avoid clipping the Snackbar on the top of the Page.",
106+
() => { },
107+
"Close",
108+
TimeSpan.FromSeconds(5), token: token));
109+
110+
var backButton = new Button()
111+
.CenterHorizontal()
112+
.Text("Back to Snackbar MainPage");
113+
backButton.Command = new AsyncRelayCommand(mainPage.Navigation.PopModalAsync);
114+
100115
await mainPage.Navigation.PushModalAsync(new ContentPage
101116
{
102117
Content = new VerticalStackLayout
@@ -105,19 +120,11 @@ await mainPage.Navigation.PushModalAsync(new ContentPage
105120

106121
Children =
107122
{
108-
new Button { Command = new AsyncRelayCommand(static token => Snackbar.Make("Snackbar in a Modal MainPage").Show(token)) }
109-
.Top().CenterHorizontal()
110-
.Text("Display Snackbar"),
123+
button,
111124

112-
new Label()
113-
.Center().TextCenter()
114-
.Text("This is a Modal MainPage"),
115-
116-
new Button { Command = new AsyncRelayCommand(mainPage.Navigation.PopModalAsync) }
117-
.Bottom().CenterHorizontal()
118-
.Text("Back to Snackbar MainPage")
125+
backButton
119126
}
120-
}.Center()
127+
}
121128
}.Padding(12));
122129
}
123130
}

samples/CommunityToolkit.Maui.Sample/Pages/Base/BasePage.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
using System.Diagnostics;
22
using CommunityToolkit.Maui.Sample.ViewModels;
3+
using Microsoft.Maui.Controls.PlatformConfiguration;
4+
using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;
35

46
namespace CommunityToolkit.Maui.Sample.Pages;
57

6-
public abstract class BasePage<TViewModel>(TViewModel viewModel) : BasePage(viewModel)
8+
public abstract class BasePage<TViewModel>(TViewModel viewModel, bool shouldUseSafeArea = true) : BasePage(viewModel, shouldUseSafeArea)
79
where TViewModel : BaseViewModel
810
{
911
public new TViewModel BindingContext => (TViewModel)base.BindingContext;
1012
}
1113

1214
public abstract class BasePage : ContentPage
1315
{
14-
protected BasePage(object? viewModel = null)
16+
protected BasePage(object? viewModel = null, bool shouldUseSafeArea = true)
1517
{
1618
BindingContext = viewModel;
1719
Padding = 12;
1820

21+
On<iOS>().SetUseSafeArea(shouldUseSafeArea);
22+
1923
if (string.IsNullOrWhiteSpace(Title))
2024
{
2125
Title = GetType().Name;

src/CommunityToolkit.Maui.Core/Views/Alert/Alert.macios.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace CommunityToolkit.Maui.Core.Views;
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
namespace CommunityToolkit.Maui.Core.Views;
24

35
/// <summary>
46
/// Popup for iOS + MacCatalyst
@@ -10,9 +12,10 @@ public class Alert
1012
/// <summary>
1113
/// Initialize Alert
1214
/// </summary>
13-
public Alert()
15+
/// <param name="shouldFillAndExpandHorizontally">Should stretch container horizontally to fit the screen</param>
16+
public Alert(bool shouldFillAndExpandHorizontally = false)
1417
{
15-
AlertView = [];
18+
AlertView = new AlertView(shouldFillAndExpandHorizontally);
1619

1720
AlertView.ParentView.AddSubview(AlertView);
1821
AlertView.ParentView.BringSubviewToFront(AlertView);
@@ -48,7 +51,7 @@ public Alert()
4851
/// </summary>
4952
public void Dismiss()
5053
{
51-
if (timer != null)
54+
if (timer is not null)
5255
{
5356
timer.Invalidate();
5457
timer.Dispose();
@@ -62,6 +65,7 @@ public void Dismiss()
6265
/// <summary>
6366
/// Show the <see cref="Alert"/> on the screen
6467
/// </summary>
68+
[MemberNotNull(nameof(timer))]
6569
public void Show()
6670
{
6771
AlertView.AnchorView = Anchor;

src/CommunityToolkit.Maui.Core/Views/Alert/AlertView.macios.cs

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,46 @@
1-
using System.Diagnostics.CodeAnalysis;
21
using CommunityToolkit.Maui.Core.Extensions;
32

43
namespace CommunityToolkit.Maui.Core.Views;
54

65
/// <summary>
76
/// <see cref="UIView"/> for <see cref="Alert"/>
87
/// </summary>
9-
public class AlertView : UIView
8+
/// <param name="shouldFillAndExpandHorizontally">Should stretch container horizontally to fit the screen</param>
9+
public class AlertView(bool shouldFillAndExpandHorizontally) : UIView
1010
{
11+
const int defaultSpacing = 10;
1112
readonly List<UIView> children = [];
1213

1314
/// <summary>
1415
/// Parent UIView
1516
/// </summary>
16-
public static UIView ParentView => Microsoft.Maui.Platform.UIApplicationExtensions.GetKeyWindow(UIApplication.SharedApplication) ?? throw new InvalidOperationException("KeyWindow is not found");
17+
public UIView ParentView { get; } = Microsoft.Maui.Platform.UIApplicationExtensions.GetKeyWindow(UIApplication.SharedApplication) ?? throw new InvalidOperationException("KeyWindow is not found");
1718

1819
/// <summary>
1920
/// PopupView Children
2021
/// </summary>
2122
public IReadOnlyList<UIView> Children => children;
22-
23-
/// <summary>
24-
/// <see cref="UIView"/> on which Alert will appear. When null, <see cref="AlertView"/> will appear at bottom of screen.
25-
/// </summary>
26-
public UIView? AnchorView { get; set; }
27-
23+
2824
/// <summary>
2925
/// <see cref="AlertViewVisualOptions"/>
3026
/// </summary>
3127
public AlertViewVisualOptions VisualOptions { get; } = new();
3228

29+
/// <summary>
30+
/// <see cref="UIView"/> on which Alert will appear. When null, <see cref="AlertView"/> will appear at bottom of screen.
31+
/// </summary>
32+
public UIView? AnchorView { get; set; }
33+
3334
/// <summary>
3435
/// Container of <see cref="AlertView"/>
3536
/// </summary>
36-
protected UIStackView? Container { get; set; }
37+
protected UIStackView Container { get; } = new()
38+
{
39+
Alignment = UIStackViewAlignment.Fill,
40+
Distribution = UIStackViewDistribution.EqualSpacing,
41+
Axis = UILayoutConstraintAxis.Horizontal,
42+
TranslatesAutoresizingMaskIntoConstraints = false
43+
};
3744

3845
/// <summary>
3946
/// Dismisses the Popup from the screen
@@ -44,58 +51,66 @@ public class AlertView : UIView
4451
/// Adds a <see cref="UIView"/> to <see cref="Children"/>
4552
/// </summary>
4653
/// <param name="child"></param>
47-
public void AddChild(UIView child) => children.Add(child);
54+
public void AddChild(UIView child)
55+
{
56+
children.Add(child);
57+
Container.AddArrangedSubview(child);
58+
}
4859

4960
/// <summary>
5061
/// Initializes <see cref="AlertView"/>
5162
/// </summary>
5263
public void Setup()
5364
{
5465
Initialize();
55-
ConstraintInParent();
66+
SetParentConstraints();
5667
}
5768

58-
void ConstraintInParent()
69+
/// <inheritdoc />
70+
public override void LayoutSubviews()
5971
{
60-
_ = Container ?? throw new InvalidOperationException($"{nameof(AlertView)}.{nameof(Initialize)} not called");
61-
62-
const int defaultSpacing = 10;
72+
base.LayoutSubviews();
73+
6374
if (AnchorView is null)
6475
{
6576
this.SafeBottomAnchor().ConstraintEqualTo(ParentView.SafeBottomAnchor(), -defaultSpacing).Active = true;
6677
this.SafeTopAnchor().ConstraintGreaterThanOrEqualTo(ParentView.SafeTopAnchor(), defaultSpacing).Active = true;
6778
}
79+
else if (AnchorView.Superview is not null
80+
&& AnchorView.Superview.ConvertRectToView(AnchorView.Frame, null).Top < Container.Frame.Height + SafeAreaLayoutGuide.LayoutFrame.Bottom)
81+
{
82+
var top = AnchorView.Superview.Frame.Top + AnchorView.Frame.Height + defaultSpacing;
83+
this.SafeTopAnchor().ConstraintEqualTo(ParentView.TopAnchor, top).Active = true;
84+
}
6885
else
6986
{
70-
this.SafeBottomAnchor().ConstraintEqualTo(AnchorView.SafeTopAnchor(), -defaultSpacing).Active = true;
87+
this.SafeBottomAnchor().ConstraintEqualTo(AnchorView.SafeTopAnchor(), 0).Active = true;
7188
}
89+
}
7290

73-
this.SafeLeadingAnchor().ConstraintGreaterThanOrEqualTo(ParentView.SafeLeadingAnchor(), defaultSpacing).Active = true;
74-
this.SafeTrailingAnchor().ConstraintLessThanOrEqualTo(ParentView.SafeTrailingAnchor(), -defaultSpacing).Active = true;
91+
void SetParentConstraints()
92+
{
93+
if (shouldFillAndExpandHorizontally)
94+
{
95+
this.SafeLeadingAnchor().ConstraintEqualTo(ParentView.SafeLeadingAnchor(), defaultSpacing).Active = true;
96+
this.SafeTrailingAnchor().ConstraintEqualTo(ParentView.SafeTrailingAnchor(), -defaultSpacing).Active = true;
97+
}
98+
else
99+
{
100+
this.SafeLeadingAnchor().ConstraintGreaterThanOrEqualTo(ParentView.SafeLeadingAnchor(), defaultSpacing).Active = true;
101+
this.SafeTrailingAnchor().ConstraintLessThanOrEqualTo(ParentView.SafeTrailingAnchor(), -defaultSpacing).Active = true;
102+
}
103+
75104
this.SafeCenterXAnchor().ConstraintEqualTo(ParentView.SafeCenterXAnchor()).Active = true;
76105

77106
Container.SafeLeadingAnchor().ConstraintEqualTo(this.SafeLeadingAnchor(), defaultSpacing).Active = true;
78107
Container.SafeTrailingAnchor().ConstraintEqualTo(this.SafeTrailingAnchor(), -defaultSpacing).Active = true;
79108
Container.SafeBottomAnchor().ConstraintEqualTo(this.SafeBottomAnchor(), -defaultSpacing).Active = true;
80109
Container.SafeTopAnchor().ConstraintEqualTo(this.SafeTopAnchor(), defaultSpacing).Active = true;
81110
}
82-
83-
[MemberNotNull(nameof(Container))]
111+
84112
void Initialize()
85113
{
86-
Container = new UIStackView
87-
{
88-
Alignment = UIStackViewAlignment.Fill,
89-
Distribution = UIStackViewDistribution.EqualSpacing,
90-
Axis = UILayoutConstraintAxis.Horizontal,
91-
TranslatesAutoresizingMaskIntoConstraints = false
92-
};
93-
94-
foreach (var view in Children)
95-
{
96-
Container.AddArrangedSubview(view);
97-
}
98-
99114
TranslatesAutoresizingMaskIntoConstraints = false;
100115
AddSubview(Container);
101116

0 commit comments

Comments
 (0)