Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Snackbar background color not working on iOS 13.3.1. #1160

Closed
2 tasks done
cat0363 opened this issue Apr 24, 2023 · 15 comments · Fixed by #1234
Closed
2 tasks done

[BUG] Snackbar background color not working on iOS 13.3.1. #1160

cat0363 opened this issue Apr 24, 2023 · 15 comments · Fixed by #1234
Labels
bug Something isn't working unverified

Comments

@cat0363
Copy link
Contributor

cat0363 commented Apr 24, 2023

Is there an existing issue for this?

  • I have searched the existing issues

Did you read the "Reporting a bug" section on Contributing file?

Current Behavior

Even if you specify the Snackbar's BackgroundColor, it will not be
colored in iOS 13.3.1. Works as expected on iOS 16.3.

Expected Behavior

Even in iOS 13.3.1, if Snackbar's BackgroundColor is specified,
it must be colored.

Steps To Reproduce

Please follow the steps below to reproduce.

  1. Press the Show Snackbar button on MainPage.

iOS 13.3.1 displays a snackbar with a transparent background color.

Link to public reproduction project repository

https://github.com/cat0363/MauiComm-IssueSnackbar.git

Environment

- .NET MAUI CommunityToolkit: 5.1.0
- OS: Windows 11 Build 22621.1555
- .NET MAUI: 7.0.81
- Target Device OS: iOS 13.3.1

Anything else?

I tried to show a snackbar with background color set like below, but the
background color doesn't work on iOS 13.3.1.

// Create Snackbar
var snackbar = Snackbar.Make(
    "This is test message.",
    action: null,
    actionButtonText: string.Empty,
    duration: new TimeSpan(0, 0, 0, 0, 3500),
    visualOptions: new CommunityToolkit.Maui.Core.SnackbarOptions() {
        BackgroundColor = Colors.Red,
        TextColor = Colors.White 
    },
    anchor: null
);

// Display Snackbar
snackbar.Show();

The above code works as intended on Android, but not on iOS 13.3.1.

In iOS 13.3.1, the background color is displayed as transparent,
but in iOS 16.3, it is displayed with the specified background color.

[iOS 13.3.1]
IMG_0258

[iOS 16.3]
IMG_0336

@cat0363 cat0363 added bug Something isn't working unverified labels Apr 24, 2023
@cat0363
Copy link
Contributor Author

cat0363 commented Jun 2, 2023

I analyzed the source code and found out why the background color does not change.
AlertView's Container is RoundedStackView, which inherits from UIStackView.
Changing the BackgroundColor of UIStackView is supported for iOS 14 or higher.
This is probably the reason.

The site below says that Community Toolkit is supported on iOS 10 or higher,
but is iOS 13.3.1 not supported?

https://learn.microsoft.com/en-us/dotnet/communitytoolkit/maui/

At least some of Snackbar's options don't work with the current structure.

@cat0363
Copy link
Contributor Author

cat0363 commented Jun 2, 2023

By writing the following after calling AlertView.Setup() in the Show method of the Alert class,
you can intentionally resemble it, but since it only decorates another View, it is a fundamental
solution not. The Layer's CornerRadius is NFloat, not CGRect, so you can't specify 4 corners.

(AlertView.Subviews.First() as RoundedStackView).Subviews.First().Layer.BackgroundColor = AlertView.VisualOptions.BackgroundColor.CGColor;
(AlertView.Subviews.First() as RoundedStackView).Subviews.First().Layer.CornerRadius = AlertView.VisualOptions.CornerRadius.Top;

IMG_0270

@cat0363
Copy link
Contributor Author

cat0363 commented Jun 5, 2023

Similarly, the CornerRadius of the four corners cannot be specified, but the following is also possible.

var subView = new UIView(AlertView.VisualOptions.CornerRadius);           
subView.BackgroundColor = AlertView.VisualOptions.BackgroundColor;
subView.Layer.CornerRadius = AlertView.VisualOptions.CornerRadius.Top;   // <= not perfect code
subView.AutoresizingMask = UIViewAutoresizing.FlexibleHeight | UIViewAutoresizing.FlexibleWidth;
AlertView.Subviews.First().InsertSubview(subView, 0);

If I were to add the above code, it would be after the AlertView.Setup() call in the Alert class.
This one is closer to ideal than the previous post.

In full swing, it is necessary to mask the four corners using UIBezierPath and CAShapeLayer.

@VladislavAntonyuk
Copy link
Collaborator

@cat0363 Thank you for the investigation. Would you like to contribute and create a bug fix PR?

@cat0363
Copy link
Contributor Author

cat0363 commented Jun 5, 2023

Hi, @VladislavAntonyuk
I'm busy migrating my app from Xamarin.Forms to .NET MAUI, so it would be
helpful if someone could take care of this issue. I can investigate and provide
information, but I'm sorry that I can't help you. CornerRadius support isn't
perfect in my code, so I need to do more research. If I find out anything additional,
I'll provide that information in this issue. I'm still fumbling, so I'll let you
know when I can contribute to a bug fix PR.

@cat0363
Copy link
Contributor Author

cat0363 commented Jun 6, 2023

Hi, @VladislavAntonyuk
It took me some time to find a way to draw the individual corners, but I solved it below.
There was an implementation hint in RoundedStackView. I solved drawing the corners
by overriding the Draw method similar to RoundedStackView. Changed Alert.macios.cs
and newly added RoundedView.macios.cs. I put the code for the solution after the
AlertView.Setup() call, but you might as well just put it inside the Setup method.

[Alert.macios.cs] <= Modify

namespace CommunityToolkit.Maui.Core.Views;

public class Alert
{
    NSTimer? timer;

    /// <summary>
    /// Initialize Alert
    /// </summary>
    public Alert()
    {
        AlertView = new AlertView();

        AlertView.ParentView.AddSubview(AlertView);
        AlertView.ParentView.BringSubviewToFront(AlertView);
    }

    /// <summary>
    /// Duration of time before <see cref="Alert"/> disappears
    /// </summary>
    public TimeSpan Duration { get; set; }

    /// <summary>
    /// <see cref="UIView"/> on which Alert will appear. When null, <see cref="Alert"/> will appear at bottom of screen.
    /// </summary>
    public UIView? Anchor { get; set; }

    /// <summary>
    /// Action to execute on popup dismissed
    /// </summary>
    public Action? OnDismissed { get; set; }

    /// <summary>
    /// Action to execute on popup shown
    /// </summary>
    public Action? OnShown { get; set; }

    /// <summary>
    /// <see cref="UIView"/> for <see cref="Alert"/>
    /// </summary>
    protected AlertView AlertView { get; }

    /// <summary>
    /// Dismiss the <see cref="Alert"/> from the screen
    /// </summary>
    public void Dismiss()
    {
        if (timer != null)
        {
            timer.Invalidate();
            timer.Dispose();
            timer = null;
        }

        AlertView.Dismiss();
        OnDismissed?.Invoke();
    }

    /// <summary>
    /// Show the <see cref="Alert"/> on the screen
    /// </summary>
    public void Show()
    {
        AlertView.AnchorView = Anchor;

        AlertView.Setup();

        // Solution prior to iOS 14 --- Start
        var subView = new RoundedView(
            AlertView.VisualOptions.CornerRadius.X,
            AlertView.VisualOptions.CornerRadius.Y,
            AlertView.VisualOptions.CornerRadius.Width,
            AlertView.VisualOptions.CornerRadius.Height
        )
        {
            BackgroundColor = AlertView.VisualOptions.BackgroundColor,
            AutoresizingMask = UIViewAutoresizing.FlexibleHeight | UIViewAutoresizing.FlexibleWidth
        };
        AlertView.Subviews.First().InsertSubview(subView, 0);
        // Solution prior to iOS 14 --- End

        timer = NSTimer.CreateScheduledTimer(Duration, t =>
        {
            Dismiss();
        });

        OnShown?.Invoke();
    }
}

[RoundedView.cs] <= Add

namespace CommunityToolkit.Maui.Core.Views;

/// <summary>
/// A rounded <see cref="UIView"/>
/// </summary>
public class RoundedView : UIView
{
    /// <summary>
    /// Initialize <see cref="RoundedView"/>
    /// </summary>
    public RoundedView(NFloat leftPadding, NFloat topPadding, NFloat rightPadding, NFloat bottomPadding)
    {
        LeftPadding = leftPadding;
        TopPadding = topPadding;
        RightPadding = rightPadding;
        BottomPadding = bottomPadding;
    }

    /// <summary>
    /// Left Padding
    /// </summary>
    public NFloat LeftPadding { get; }

    /// <summary>
    /// Top Padding
    /// </summary>
    public NFloat TopPadding { get; }

    /// <summary>
    /// Right Padding
    /// </summary>
    public NFloat RightPadding { get; }

    /// <summary>
    /// Bottom Padding
    /// </summary>
    public NFloat BottomPadding { get; }

    /// <inheritdoc />
    public override void Draw(CGRect rect)
    {
        ClipsToBounds = true;

        var path = GetRoundedPath(rect, LeftPadding, TopPadding, RightPadding, BottomPadding);
        var maskLayer = new CAShapeLayer
        {
            Frame = rect,
            Path = path
        };

        Layer.Mask = maskLayer;
        Layer.MasksToBounds = true;
    }

    static CGPath? GetRoundedPath(CGRect rect, NFloat left, NFloat top, NFloat right, NFloat bottom)
    {
        var path = new UIBezierPath();
        path.MoveTo(new CGPoint(rect.Width - right, rect.Y));

        path.AddArc(new CGPoint(rect.X + rect.Width - right, rect.Y + right), right, (NFloat)(Math.PI * 1.5), (NFloat)Math.PI * 2, true);
        path.AddLineTo(new CGPoint(rect.Width, rect.Height - bottom));

        path.AddArc(new CGPoint(rect.X + rect.Width - bottom, rect.Y + rect.Height - bottom), bottom, 0, (NFloat)(Math.PI * .5), true);
        path.AddLineTo(new CGPoint(left, rect.Height));

        path.AddArc(new CGPoint(rect.X + left, rect.Y + rect.Height - left), left, (NFloat)(Math.PI * .5), (NFloat)Math.PI, true);
        path.AddLineTo(new CGPoint(rect.X, top));

        path.AddArc(new CGPoint(rect.X + top, rect.Y + top), top, (NFloat)Math.PI, (NFloat)(Math.PI * 1.5), true);

        path.ClosePath();

        return path.CGPath;
    }
}

Below is the execution result on iOS 13.3.1. (iPad)

[Case 1]

var snackbar = Snackbar.Make(
    "This is test message1." + Environment.NewLine + "This is test message2.",
    action: null,
    actionButtonText: string.Empty,
    duration: new TimeSpan(0, 0, 0, 0, 3500),
    visualOptions: new CommunityToolkit.Maui.Core.SnackbarOptions() {
        BackgroundColor = Colors.Red,
        TextColor = Colors.White,
        CornerRadius = new CornerRadius(20,5,10,0)
    },
    anchor: null
);

IMG_0274

[Case 2]

var snackbar = Snackbar.Make(
    "This is test message1." + Environment.NewLine + "This is test message2.",
    action: null,
    actionButtonText: string.Empty,
    duration: new TimeSpan(0, 0, 0, 0, 3500),
    visualOptions: new CommunityToolkit.Maui.Core.SnackbarOptions() {
        BackgroundColor = Colors.Red,
        TextColor = Colors.White,
        CornerRadius = new CornerRadius(5)
    },
    anchor: null
);

IMG_0275

Below is the execution result on iOS 16.4. (iPhone)

[Case 1]

0004

[Case 2]

0003

Below is the result before correction on IOS 16.4. (iPhone)

[Case 1]

0002

[Case 2]

0001

iOS 14 and above look the same before and after the fix.

In order to reduce the impact, it may be better to run it only before iOS 14.

Could you please confirm the following points?

  1. Whether or not to include a solution code by specifying the version
  2. Whether to include the solution code in the Setup method

Could you please check if there are any problems?
This is my first time creating a bug fix PR, so I need a lot of information.

@VladislavAntonyuk
Copy link
Collaborator

Thank you for detailed response.
If the code is not necessary on iOS 14 and later let's wrap it with if (operationSystem.IsIosVersionAtLeast(14)).
I would keep all layouts together, so let's put it in setup method.
The only question I have, do we need RoundStackView if we have RoundView? Can we achieve the same result for all iOS versions with the same code?

@cat0363
Copy link
Contributor Author

cat0363 commented Jun 6, 2023

Hi, @VladislavAntonyuk
Thank you for the immediate reply.
Requires a RoundedStackView. Because RoundedStackView is used to arrange message and action button horizontally.
Overriding the Draw method of RoundedStackView may no longer be necessary. If you don't need to reimplement the
Draw method on RoundedStackView, you don't need to specify the version. This is because the background is drawn
twice for each RoundedStackView and RoundedView.

I partially copied and modified the CommunityToolkit source to study the solution. If I download the complete
CommunityToolkit source code from nuget, how do I build it and link it to my app for testing? Once I know how
to do that, I can do a more detailed verification.

The iOS versions that I can check on the simulator and the actual machine are as follows.

iOS 13.3.1
iOS 13.7
iOS 14.5
iOS 15.5
iOS 16.4

I will report back here once the verification is complete.

This is the current code. The code further changes depending on whether the Draw method of RoundedStackView
needs to be reimplemented.

[AlertView.macios.cs] <= Modify

using System.Diagnostics.CodeAnalysis;
using CommunityToolkit.Maui.Core.Extensions;

namespace CommunityToolkit.Maui.Core.Views;

/// <summary>
/// <see cref="UIView"/> for <see cref="Alert"/>
/// </summary>
public class AlertView : UIView
{
    readonly List<UIView> children = Enumerable.Empty<UIView>().ToList();

    /// <summary>
    /// Parent UIView
    /// </summary>
    public static UIView ParentView => Microsoft.Maui.Platform.UIApplicationExtensions.GetKeyWindow(UIApplication.SharedApplication) ?? throw new InvalidOperationException("KeyWindow is not found");

    /// <summary>
    /// PopupView Children
    /// </summary>
    public IReadOnlyList<UIView> Children => children;

    /// <summary>
    /// <see cref="UIView"/> on which Alert will appear. When null, <see cref="AlertView"/> will appear at bottom of screen.
    /// </summary>
    public UIView? AnchorView { get; set; }

    /// <summary>
    /// <see cref="AlertViewVisualOptions"/>
    /// </summary>
    public AlertViewVisualOptions VisualOptions { get; } = new();

    /// <summary>
    /// Container of <see cref="AlertView"/>
    /// </summary>
    protected UIStackView? Container { get; set; }

    /// <summary>
    /// Dismisses the Popup from the screen
    /// </summary>
    public void Dismiss() => RemoveFromSuperview();

    /// <summary>
    /// Adds a <see cref="UIView"/> to <see cref="Children"/>
    /// </summary>
    /// <param name="child"></param>
    public void AddChild(UIView child) => children.Add(child);

    /// <summary>
    /// Initializes <see cref="AlertView"/>
    /// </summary>
    public void Setup()
    {
        Initialize();
        ConstraintInParent();
    }

    void ConstraintInParent()
    {
        _ = Container ?? throw new InvalidOperationException($"{nameof(AlertView)}.{nameof(Initialize)} not called");

        const int defaultSpacing = 10;
        if (AnchorView is null)
        {
            this.SafeBottomAnchor().ConstraintEqualTo(ParentView.SafeBottomAnchor(), -defaultSpacing).Active = true;
            this.SafeTopAnchor().ConstraintGreaterThanOrEqualTo(ParentView.SafeTopAnchor(), defaultSpacing).Active = true;
        }
        else
        {
            this.SafeBottomAnchor().ConstraintEqualTo(AnchorView.SafeBottomAnchor(), -defaultSpacing).Active = true;
        }

        this.SafeLeadingAnchor().ConstraintGreaterThanOrEqualTo(ParentView.SafeLeadingAnchor(), defaultSpacing).Active = true;
        this.SafeTrailingAnchor().ConstraintLessThanOrEqualTo(ParentView.SafeTrailingAnchor(), -defaultSpacing).Active = true;
        this.SafeCenterXAnchor().ConstraintEqualTo(ParentView.SafeCenterXAnchor()).Active = true;

        Container.SafeLeadingAnchor().ConstraintEqualTo(this.SafeLeadingAnchor(), defaultSpacing).Active = true;
        Container.SafeTrailingAnchor().ConstraintEqualTo(this.SafeTrailingAnchor(), -defaultSpacing).Active = true;
        Container.SafeBottomAnchor().ConstraintEqualTo(this.SafeBottomAnchor(), -defaultSpacing).Active = true;
        Container.SafeTopAnchor().ConstraintEqualTo(this.SafeTopAnchor(), defaultSpacing).Active = true;
    }

    [MemberNotNull(nameof(Container))]
    void Initialize()
    {
        Container = new RoundedStackView(
            VisualOptions.CornerRadius.X,
            VisualOptions.CornerRadius.Y,
            VisualOptions.CornerRadius.Width,
            VisualOptions.CornerRadius.Height)
        {
            Alignment = UIStackViewAlignment.Fill,
            Distribution = UIStackViewDistribution.EqualSpacing,
            Axis = UILayoutConstraintAxis.Horizontal,
            BackgroundColor = VisualOptions.BackgroundColor,
            TranslatesAutoresizingMaskIntoConstraints = false
        };

        foreach (var view in Children)
        {
            Container.AddArrangedSubview(view);
        }

        TranslatesAutoresizingMaskIntoConstraints = false;
        AddSubview(Container);

        if (OperatingSystem.IsIOSVersionAtLeast(14) == false)
        {
            var subView = new RoundedView(
                VisualOptions.CornerRadius.X,
                VisualOptions.CornerRadius.Y,
                VisualOptions.CornerRadius.Width,
                VisualOptions.CornerRadius.Height
            )
            {                
                BackgroundColor = VisualOptions.BackgroundColor,
                AutoresizingMask = UIViewAutoresizing.FlexibleHeight | UIViewAutoresizing.FlexibleWidth,
            };
            Subviews.First().InsertSubview(subView, 0);
        }
    }
}

[RoundedView.cs] <= Add

using System.Runtime.InteropServices;
using CoreAnimation;

namespace CommunityToolkit.Maui.Core.Views;

/// <summary>
/// A rounded <see cref="UIView"/>
/// </summary>
public class RoundedView : UIView
{
    /// <summary>
    /// Initialize <see cref="RoundedView"/>
    /// </summary>
    public RoundedView(NFloat leftPadding, NFloat topPadding, NFloat rightPadding, NFloat bottomPadding)
    {
        LeftPadding = leftPadding;
        TopPadding = topPadding;
        RightPadding = rightPadding;
        BottomPadding = bottomPadding;
    }

    /// <summary>
    /// Left Padding
    /// </summary>
    public NFloat LeftPadding { get; }

    /// <summary>
    /// Top Padding
    /// </summary>
    public NFloat TopPadding { get; }

    /// <summary>
    /// Right Padding
    /// </summary>
    public NFloat RightPadding { get; }

    /// <summary>
    /// Bottom Padding
    /// </summary>
    public NFloat BottomPadding { get; }

    /// <inheritdoc />
    public override void Draw(CGRect rect)
    {
        ClipsToBounds = true;

        var path = GetRoundedPath(rect, LeftPadding, TopPadding, RightPadding, BottomPadding);
        var maskLayer = new CAShapeLayer
        {
            Frame = rect,
            Path = path
        };

        Layer.Mask = maskLayer;
        Layer.MasksToBounds = true;
    }

    static CGPath? GetRoundedPath(CGRect rect, NFloat left, NFloat top, NFloat right, NFloat bottom)
    {
        var path = new UIBezierPath();
        path.MoveTo(new CGPoint(rect.Width - right, rect.Y));

        path.AddArc(new CGPoint(rect.X + rect.Width - right, rect.Y + right), right, (NFloat)(Math.PI * 1.5), (NFloat)Math.PI * 2, true);
        path.AddLineTo(new CGPoint(rect.Width, rect.Height - bottom));

        path.AddArc(new CGPoint(rect.X + rect.Width - bottom, rect.Y + rect.Height - bottom), bottom, 0, (NFloat)(Math.PI * .5), true);
        path.AddLineTo(new CGPoint(left, rect.Height));

        path.AddArc(new CGPoint(rect.X + left, rect.Y + rect.Height - left), left, (NFloat)(Math.PI * .5), (NFloat)Math.PI, true);
        path.AddLineTo(new CGPoint(rect.X, top));

        path.AddArc(new CGPoint(rect.X + top, rect.Y + top), top, (NFloat)Math.PI, (NFloat)(Math.PI * 1.5), true);

        path.ClosePath();

        return path.CGPath;
    }
}

Thank you.

@cat0363
Copy link
Contributor Author

cat0363 commented Jun 6, 2023

Hi, @VladislavAntonyuk
I was able to build and link the source code downloaded from nuget to my app.
Now I can modify the source code and verify it.
I would like to verify what I posted earlier. Thank you.

@cat0363
Copy link
Contributor Author

cat0363 commented Jun 6, 2023

Hi, @VladislavAntonyuk
From what I've verified, you don't need to override the RoundedStackView's Draw method.
The AlertView's container doesn't have to be a RoundedStackView, a UIStackView is enough.
For this reason, the RoundedView that inherits from UIView only needs to draw the background.

Given the above, below is the solution I wrote.
Changed AlertView.macios.cs, added RoundedView.macios.cs, and removed RoundedStackView.macios.cs.

[AlertView.macios.cs] <= Modify

using System.Diagnostics.CodeAnalysis;
using CommunityToolkit.Maui.Core.Extensions;

namespace CommunityToolkit.Maui.Core.Views;

/// <summary>
/// <see cref="UIView"/> for <see cref="Alert"/>
/// </summary>
public class AlertView : UIView
{
    readonly List<UIView> children = Enumerable.Empty<UIView>().ToList();

    /// <summary>
    /// Parent UIView
    /// </summary>
    public static UIView ParentView => Microsoft.Maui.Platform.UIApplicationExtensions.GetKeyWindow(UIApplication.SharedApplication) ?? throw new InvalidOperationException("KeyWindow is not found");

    /// <summary>
    /// PopupView Children
    /// </summary>
    public IReadOnlyList<UIView> Children => children;

    /// <summary>
    /// <see cref="UIView"/> on which Alert will appear. When null, <see cref="AlertView"/> will appear at bottom of screen.
    /// </summary>
    public UIView? AnchorView { get; set; }

    /// <summary>
    /// <see cref="AlertViewVisualOptions"/>
    /// </summary>
    public AlertViewVisualOptions VisualOptions { get; } = new();

    /// <summary>
    /// Container of <see cref="AlertView"/>
    /// </summary>
    protected UIStackView? Container { get; set; }

    /// <summary>
    /// Dismisses the Popup from the screen
    /// </summary>
    public void Dismiss() => RemoveFromSuperview();

    /// <summary>
    /// Adds a <see cref="UIView"/> to <see cref="Children"/>
    /// </summary>
    /// <param name="child"></param>
    public void AddChild(UIView child) => children.Add(child);

    /// <summary>
    /// Initializes <see cref="AlertView"/>
    /// </summary>
    public void Setup()
    {
        Initialize();
        ConstraintInParent();
    }

    void ConstraintInParent()
    {
        _ = Container ?? throw new InvalidOperationException($"{nameof(AlertView)}.{nameof(Initialize)} not called");

        const int defaultSpacing = 10;
        if (AnchorView is null)
        {
            this.SafeBottomAnchor().ConstraintEqualTo(ParentView.SafeBottomAnchor(), -defaultSpacing).Active = true;
            this.SafeTopAnchor().ConstraintGreaterThanOrEqualTo(ParentView.SafeTopAnchor(), defaultSpacing).Active = true;
        }
        else
        {
            this.SafeBottomAnchor().ConstraintEqualTo(AnchorView.SafeBottomAnchor(), -defaultSpacing).Active = true;
        }

        this.SafeLeadingAnchor().ConstraintGreaterThanOrEqualTo(ParentView.SafeLeadingAnchor(), defaultSpacing).Active = true;
        this.SafeTrailingAnchor().ConstraintLessThanOrEqualTo(ParentView.SafeTrailingAnchor(), -defaultSpacing).Active = true;
        this.SafeCenterXAnchor().ConstraintEqualTo(ParentView.SafeCenterXAnchor()).Active = true;

        Container.SafeLeadingAnchor().ConstraintEqualTo(this.SafeLeadingAnchor(), defaultSpacing).Active = true;
        Container.SafeTrailingAnchor().ConstraintEqualTo(this.SafeTrailingAnchor(), -defaultSpacing).Active = true;
        Container.SafeBottomAnchor().ConstraintEqualTo(this.SafeBottomAnchor(), -defaultSpacing).Active = true;
        Container.SafeTopAnchor().ConstraintEqualTo(this.SafeTopAnchor(), defaultSpacing).Active = true;
    }

    [MemberNotNull(nameof(Container))]
    void Initialize()
    {
        Container = new UIStackView()
        {
            Alignment = UIStackViewAlignment.Fill,
            Distribution = UIStackViewDistribution.EqualSpacing,
            Axis = UILayoutConstraintAxis.Horizontal,
            TranslatesAutoresizingMaskIntoConstraints = false
        };

        foreach (var view in Children)
        {
            Container.AddArrangedSubview(view);
        }

        TranslatesAutoresizingMaskIntoConstraints = false;
        AddSubview(Container);

        var subView = new RoundedView(
            VisualOptions.CornerRadius.X,
            VisualOptions.CornerRadius.Y,
            VisualOptions.CornerRadius.Width,
            VisualOptions.CornerRadius.Height)
        {
            BackgroundColor = VisualOptions.BackgroundColor,
            AutoresizingMask = UIViewAutoresizing.FlexibleHeight | UIViewAutoresizing.FlexibleWidth,
        };
        Subviews.First().InsertSubview(subView, 0);
    }
}

[RoundedView.cs] <= Add

using System.Runtime.InteropServices;
using CoreAnimation;

namespace CommunityToolkit.Maui.Core.Views;

/// <summary>
/// A rounded <see cref="UIView"/>
/// </summary>
public class RoundedView : UIView
{
    /// <summary>
    /// Initialize <see cref="RoundedView"/>
    /// </summary>
    public RoundedView(NFloat leftPadding, NFloat topPadding, NFloat rightPadding, NFloat bottomPadding)
    {
        LeftPadding = leftPadding;
        TopPadding = topPadding;
        RightPadding = rightPadding;
        BottomPadding = bottomPadding;
    }

    /// <summary>
    /// Left Padding
    /// </summary>
    public NFloat LeftPadding { get; }

    /// <summary>
    /// Top Padding
    /// </summary>
    public NFloat TopPadding { get; }

    /// <summary>
    /// Right Padding
    /// </summary>
    public NFloat RightPadding { get; }

    /// <summary>
    /// Bottom Padding
    /// </summary>
    public NFloat BottomPadding { get; }

    /// <inheritdoc />
    public override void Draw(CGRect rect)
    {
        ClipsToBounds = true;

        var path = GetRoundedPath(rect, LeftPadding, TopPadding, RightPadding, BottomPadding);
        var maskLayer = new CAShapeLayer
        {
            Frame = rect,
            Path = path
        };

        Layer.Mask = maskLayer;
        Layer.MasksToBounds = true;
    }

    static CGPath? GetRoundedPath(CGRect rect, NFloat left, NFloat top, NFloat right, NFloat bottom)
    {
        var path = new UIBezierPath();
        path.MoveTo(new CGPoint(rect.Width - right, rect.Y));

        path.AddArc(new CGPoint(rect.X + rect.Width - right, rect.Y + right), right, (NFloat)(Math.PI * 1.5), (NFloat)Math.PI * 2, true);
        path.AddLineTo(new CGPoint(rect.Width, rect.Height - bottom));

        path.AddArc(new CGPoint(rect.X + rect.Width - bottom, rect.Y + rect.Height - bottom), bottom, 0, (NFloat)(Math.PI * .5), true);
        path.AddLineTo(new CGPoint(left, rect.Height));

        path.AddArc(new CGPoint(rect.X + left, rect.Y + rect.Height - left), left, (NFloat)(Math.PI * .5), (NFloat)Math.PI, true);
        path.AddLineTo(new CGPoint(rect.X, top));

        path.AddArc(new CGPoint(rect.X + top, rect.Y + top), top, (NFloat)Math.PI, (NFloat)(Math.PI * 1.5), true);

        path.ClosePath();

        return path.CGPath;
    }
}

[RoundedStackView.macios.cs] <= Delete

Below is the execution result on iOS 13.3.1. (iPad) [Device]
[Case 1]

iOS13 3 1_1

[Case 2]

iOS13 3 1_2

Below is the execution result on iOS 13.7. (iPhone) [Simulator]
[Case 1]

iOS13 7_1

[Case 2]

iOS13 7_2

Below is the execution result on iOS 14.5. (iPhone) [Simulator]
[Case 1]

iOS14 5_1

[Case 2]

iOS14 5_2

Below is the execution result on iOS 16.4. (iPhone) [Device]
[Case 1]

iOS16 4_1

[Case 2]

iOS16 4_2

The truncated messages are due to the width of the simulator.
This happened with or without the fix.
Below is the result execution of iOS 13.7 before the fix.

iOS13 7_3

The omission of messages is irrelevant to the presence or absence of corrections.
So this is a simulator screen width issue.

Same for iOS 14.5 before the fix.

iOS14 5_3

Operation verification on iOS 15.5 could not be verified by the end of today
because the Simulator is being installed. I will post the test results here later.

@cat0363
Copy link
Contributor Author

cat0363 commented Jun 7, 2023

Below is the execution result on iOS 15.5. (iPhone) [Simulator]
[Case 1]

iOS15 5_1

[Case 2]

iOS15 5_2

Below is the execution result when ActionButton is added.

[iOS 13.3.1]

iOS13 3 1_4

[iOS 13.7]

iOS13 7_4

[iOS 14.5]

iOS14 5_4

[iOS 15.5]

iOS15 5_4

[iOS 16.4]

iOS16 4_4

At least on the version of iOS I was able to verify, it worked as intended.
It works the same even if you don't differentiate between versions.

@cat0363
Copy link
Contributor Author

cat0363 commented Jun 12, 2023

Additional Information:
In iOS 13.7 and iOS 14.5, the end of the message was "...", but this seems to be related to the screen width of the simulator.
When I used the wider iPad Simulator instead of the iPhone Simulator, the messages were no longer omitted.
Therefore, the solution and omitting the message are irrelevant.

If this fix doesn't seem to be a problem, I'll create a bug fix PR, what do you think?

@VladislavAntonyuk
Copy link
Collaborator

I will appreciate if you create a PR. Thank you!

@cat0363
Copy link
Contributor Author

cat0363 commented Jun 12, 2023

Hi, @VladislavAntonyuk
Thank you for your reply. I would like to start creating a PR.

cat0363 added a commit to cat0363/Maui that referenced this issue Jun 12, 2023
@cat0363 cat0363 mentioned this issue Jun 12, 2023
6 tasks
VladislavAntonyuk pushed a commit that referenced this issue Jun 13, 2023
* Fixed code for Issue #1160

* Changed how to refer to Subviews

* Revert and deprecate RoundedStackView
@cat0363
Copy link
Contributor Author

cat0363 commented Jun 14, 2023

Thank you for taking the time to resolve this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working unverified
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants