dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
22.27k stars 1.76k forks source link

[iOS] Editor autoscrolls its text view completely off the screen if you press 'return' too many times, has no constraint against scrolling the text view off screen. Objects lag in resizing around it. #17757

Open jonmdev opened 1 year ago

jonmdev commented 1 year ago

Description

A working Editor is essential for text input in a cross platform and mobile environment. Editor can be used as a field for users to type into such as in this forum box I am typing into now or a WhatsApp or SMS style text entry field.

A typical such Editor function is described by:

            Editor editor = new();
            editor.AutoSize = EditorAutoSizeOption.TextChanges;
            editor.MaximumHeightRequest = 200;

In Windows and Android, Editor functions properly with such a setting. However in iOS it is quite broken, exhibiting at least three problems:

  1. Editor in iOS will not scroll down automatically as you add new lines once it is at its max size (in Windows and Android, as you add more lines, the Editor automatically scrolls down to show them).

  2. Editor in iOS does not respond to touch and drag to scroll manually (in Android, you can touch and drag to scroll and Windows use the mouse scroll wheel to scroll).

  3. Parent element of Editor in iOS resizes with a one frame lag behind the Editor as the Editor expands, so as you add new lines and Editor resizes, Editor will momentarily overlap its parent object each time (in Android and Windows the resizing of parents are instant).

Android and Windows exhibit all the obvious desirable behaviors.

That is: (i) A text field must automatically scroll down to always show the current line of text being written to, (ii) touching and dragging is the only way to manually scroll a text field in mobile and must work as expected, and (iii) resizing of the Editor should not introduce visual glitches and resize lags.

These problems only exist in iOS. Behavior of Editor in Android and Windows appears perfect.

Steps to Reproduce

  1. Create a new blank MAUI project in Visual Studio 2022 using .NET 7.0 by File > New.

  2. Copy and paste the following to replace the default class in App.xaml.cs:

    public partial class App : Application {
    public App() {
        InitializeComponent();
    
        MainPage = new ContentPage();
    
        VerticalStackLayout vert = new();
        (MainPage as ContentPage).Content = vert;
    
        Border border = new();
        border.StrokeThickness = 4;
        border.BackgroundColor = Colors.DarkBlue;
        border.Stroke = Colors.Red;
        border.Padding = 10;
        vert.Children.Add(border);
    
        Editor editor = new();
        editor.BackgroundColor = Colors.White;
        editor.AutoSize = EditorAutoSizeOption.TextChanges;
        editor.MaximumHeightRequest = 200;
        border.Content = editor;
    
        Label testLabel = new Label();
        testLabel.Text = "Editor iOS Bug Demonstration:\n- Editor does not autoscroll downward as you fill the Editor with text (new lines past max height go down off screen)\n- Editor does not scroll with click and drag once past maxHeight in size.\n- There is a frame lag after resizing Editor and before resizing the parent so the Editor goes 'out of bounds' of its parent momentarily each time it grows vertically.";
        vert.Children.Add(testLabel);
    
    }
    }
  3. Run the project in Windows and Android and you will observe the following CORRECT BEHAVIOR:

  1. Run the project in iOS and observe by contrast the following WRONG BEHAVIOR:

Expected behavior again is that Editor in iOS should show the same results as Windows and Android with: (i) automatic downward scrolling on new lines, (ii) manual scrolling on touch and drag (and scroll wheel if available from simulator), and (iii) instant resizing of its parents upon changing size.

Link to public reproduction project repository

https://github.com/jonmdev/Editor-iOS-Bug

Version with bug

7.0.92/8.0

Is this a regression from previous behavior?

Not sure, did not test other versions

Last version that worked well

Unknown/Other

Affected platforms

iOS

Affected platform versions

iOS 16.7 on iPhone XR with Debug build (Hot Restart)

Did you find any workaround?

None.

Relevant log output

No response

ghost commented 1 year ago

Hi @jonmdev. We have added the "s/try-latest-version" label to this issue, which indicates that we'd like you to try and reproduce this issue on the latest available public version. This can happen because we think that this issue was fixed in a version that has just been released, or the information provided by you indicates that you might be working with an older version.

You can install the latest version by installing the latest Visual Studio (Preview) with the .NET MAUI workload installed. If the issue still persists, please let us know with any additional details and ideally a reproduction project provided through a GitHub repository.

This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

tschbc commented 1 year ago

[On MAUI 7.0.86 - iOS]

I've also experienced 1 & 2. In our app we've resorted to using a "go-to-bottom" button that moves your cursor and scrolls to the bottom the editor for you.

With some finagling we were able to get the editor somewhat working in a ScrollView—but it's still pretty broken. After \<some amount> of lines have been entered, you can freely scroll. But before you hit that \<some amount> of lines, you can only initiate a scroll by pulling down first.

jonmdev commented 1 year ago

I tried .NET 8.0 and it appears some of this has been fixed:

1) It now does scroll down as you type. 2) It now does respond to touch and drag.

However the issues are now:

1) Still has a frame lag as you type new lines (with same project code above). 2) Scrolling by touch and drag is not constrained, so you can slide it up all the way out of view. 3) The new line auto-scrolling is glitchy - if you keep pressing enter it will keep moving the white text field higher and higher until it goes out of view.

Very, very strange.

But I suppose an improvement. At least we have touch/drag response of some kind. 👍

Any further help with fixing this?

To see the problem, open my code in .NET 8, build to iOS then start typing lots of new lines in the field. It will lag on resize, start pushing the text field up high out of view, and if you drag it you will find you can slide it up way out of bounds as well without anything to automatically stop it or bring it back into view after you release.

Thanks.

jonmdev commented 1 year ago

I posted a simple demo project of the code I posted above here if it is any help to show the issues:

https://github.com/jonmdev/Editor-iOS-Bug

jonmdev commented 1 year ago

I see this is added to the .NET 8 SR2 milestone and title was changed of post to "Editor causes visible frame lag in resizing parents when it resizes".

I am happy it is being fixed. I hope the other issues of:

Will also be solved.

I just checked and they are still reproduced with my demo project using 8.0.0-rc.2.9373 and it still demonstrates these bugs also.

To clarify, if you open the bug project, click in the touch field for Editor, and start pressing "return" repeatedly in iOS, it will start looking normal like this but eventually the white text field will slide completely off screen:

IMG_0006 IMG_0005 IMG_0007

Once the white field becomes big enough, you can also slide it unconstrained up and down to this degree (even all the way off screen).

I also hope we can have an option to disable being able to pull the text field past its edges at all so the touch/drag function matches better against Android.

Thanks again.

tschbc commented 1 year ago

@jonmdev I've had better luck using a StackLayout instead of VerticalStackLayout for most of my layout issues, so you may be interested in that as a workaround. (I know it's "less performant" but I'd personally rather have it actually work in the first place)

I can't share real xaml/code because my repo isn't public, but I can share the layout structure I use for my Editor:

<ContentPage>
<StackLayout Orientation="Vertical">

    <header views>

    <AbsoluteLayout VerticalOptions="FillAndExpand">
        <Editor
            AbsoluteLayout.LayoutFlags="All"
            AbsoluteLayout.LayoutBounds="0,0,1,1">

            <Editor.Behaviors>
                <toolkit:UserStoppedTypingBehavior />
            </Editor.Behaviors>
        </Editor>

        <ImageButton
            AbsoluteLayout.LayoutFlags="PositionProportional"
            AbsoluteLayout.LayoutBounds="1,1"
            WidthRequest="44"
            HeightRequest="44"
            CornerRadius="22"
            Margin="10"
            Padding="5">

            <ImageButton.Shadow>
                <Shadow />
            </ImageButton.Shadow>

            <ImageButton.Source>
                <FontImageSource />
            </ImageButton.Source>

        </ImageButton>
    </AbsoluteLayout>

</StackLayout>
</ContentPage>

The AbsoluteLayout is only there so I can float a button over top of the Editor. It may help with the layout issues, but I haven't tested without it, so I don't know. We used a Grid before switching to a StackLayout and left the AbsoluteLayout structure unchanged.

With this structure, and on MAUI 8 RC2, I've had zero issues with the Editor:

jonmdev commented 1 year ago

I have been able to identify that the resize lagging behavior for surrounding elements is more generalized and not specifically just due to Editor. I have created a separate repro and bug report then for that here that more easily demonstrates the problem:

https://github.com/dotnet/maui/issues/18204

As this is an Editor related bug thread, I will change the title back to reflect the Editor specific bugs as best I understand the current ones. Hopefully that's okay. Again, I hope all these bugs can be fixed as they are all making things quite dysfunctional and not very usable in iOS.

Perhaps all the bugs are actually the same thing? Ie. Maybe the same resize lag/glitch is what is causing the Editor to go off screen when you keep pressing 'return' as well? I am not sure. Maybe if we get that fixed it will fix everything?

@samhouts @Eilon

jeff-eats-pubsubs commented 11 months ago

@jonmdev I've had better luck using a StackLayout instead of VerticalStackLayout for most of my layout issues, so you may be interested in that as a workaround. (I know it's "less performant" but I'd personally rather have it actually work in the first place)

I can't share real xaml/code because my repo isn't public, but I can share the layout structure I use for my Editor:

<ContentPage>
<StackLayout Orientation="Vertical">

    <header views>

    <AbsoluteLayout VerticalOptions="FillAndExpand">
        <Editor
            AbsoluteLayout.LayoutFlags="All"
            AbsoluteLayout.LayoutBounds="0,0,1,1">

            <Editor.Behaviors>
                <toolkit:UserStoppedTypingBehavior />
            </Editor.Behaviors>
        </Editor>

        <ImageButton
            AbsoluteLayout.LayoutFlags="PositionProportional"
            AbsoluteLayout.LayoutBounds="1,1"
            WidthRequest="44"
            HeightRequest="44"
            CornerRadius="22"
            Margin="10"
            Padding="5">

            <ImageButton.Shadow>
                <Shadow />
            </ImageButton.Shadow>

            <ImageButton.Source>
                <FontImageSource />
            </ImageButton.Source>

        </ImageButton>
    </AbsoluteLayout>

</StackLayout>
</ContentPage>

The AbsoluteLayout is only there so I can float a button over top of the Editor. It may help with the layout issues, but I haven't tested without it, so I don't know. We used a Grid before switching to a StackLayout and left the AbsoluteLayout structure unchanged.

With this structure, and on MAUI 8 RC2, I've had zero issues with the Editor:

  • Editor correctly auto-scrolls when entering newlines past its bottom boundary
  • Editor correctly scrolls to cursor on TextChanged when cursor is outside its viewport
  • Editor correctly scrolls to cursor position when cursor position is set outside its viewport
  • Scrolling behaves as expected compared to other scrolling views like CollectionView

Thanks @tschbc for recommending the absolutelayout. I had a similar overlay editor layout and I was previously using a grid and that was causing some silly issues that seemed to autocorrect after the switch. You're the man!

tj-devel709 commented 11 months ago

Can confirm the behavior below on .NET 7 and .NET 8. Also appears to be completely separate from AutoKeyboardScrollManager scroll logic (for myself and others).

.NET 7 .NET 8
PureWeen commented 9 months ago

Can you test with the latest nightly build? https://github.com/dotnet/maui/wiki/Nightly-Builds

last-Programmer commented 9 months ago

@PureWeen i treid the nightly build and my project is not compiling. now it seems to be that platformview in handlers are object type and not the native type. and color.toPltform() is also broken. so cant test it.

jaosnz-rep commented 9 months ago

Verified this issue with Visual Studio Enterprise 17.10 Preview 1.0, can repro on iOS platform with sample project. DemoProj.zip

bradencohen commented 8 months ago

TLDR: Workaround

Go ahead and drop this handler in (only target your iOS platform), and it should hopefully make things more bearable:

/// <summary>
/// A handler for the <see cref="Editor"/> control.
/// </summary>
internal class EditorHandler : Microsoft.Maui.Handlers.EditorHandler
{
    /// <summary>
    /// Gets the desired size of the Editor.
    /// </summary>
    /// <param name="widthConstraint"></param>
    /// <param name="heightConstraint"></param>
    /// <returns></returns>
    public override Size GetDesiredSize( double widthConstraint, double heightConstraint )
    {
        //
        // Port of the default GetDesiredSizeFromHandler method (since internal):
        // https://github.com/dotnet/maui/blob/c7d1a4ec8d857aba674362d2777d98855f0ca67a/src/Core/src/Handlers/ViewHandlerExtensions.iOS.cs#L66
        //

        if ( PlatformView == null || VirtualView == null )
        {
            return new Size( widthConstraint, heightConstraint );
        }

        // The measurements ran in SizeThatFits percolate down to child views
        // So if MaximumWidth/Height are not taken into account for constraints, the children may have wrong dimensions
        widthConstraint = Math.Min( widthConstraint, VirtualView.MaximumWidth );
        heightConstraint = Math.Min( heightConstraint, VirtualView.MaximumHeight );

        CGSize sizeThatFits = PlatformView.SizeThatFits( new CGSize( ( float ) widthConstraint, ( float ) heightConstraint ) );

        var size = new Size(
            sizeThatFits.Width == float.PositiveInfinity ? double.PositiveInfinity : sizeThatFits.Width,
            sizeThatFits.Height == float.PositiveInfinity ? double.PositiveInfinity : sizeThatFits.Height );

        if ( double.IsInfinity( size.Width ) || double.IsInfinity( size.Height ) )
        {
            PlatformView.SizeToFit();
            size = new Size( PlatformView.Frame.Width, PlatformView.Frame.Height );
        }

        var finalWidth = ResolveConstraints( size.Width, VirtualView.Width, VirtualView.MinimumWidth, VirtualView.MaximumWidth );
        var finalHeight = ResolveConstraints( size.Height, VirtualView.Height, VirtualView.MinimumHeight, VirtualView.MaximumHeight );

        return new Size( finalWidth, finalHeight );
    }

    /// <summary>
    /// Port of the internal ResolveConstraints method.
    /// </summary>
    /// <param name="measured"></param>
    /// <param name="exact"></param>
    /// <param name="min"></param>
    /// <param name="max"></param>
    /// <returns></returns>
    internal static double ResolveConstraints( double measured, double exact, double min, double max )
    {
        var resolved = measured;

        min = Microsoft.Maui.Primitives.Dimension.ResolveMinimum( min );

        if ( Microsoft.Maui.Primitives.Dimension.IsExplicitSet( exact ) )
        {
            // If an exact value has been specified, try to use that
            resolved = exact;
        }

        if ( resolved > max )
        {
            // Apply the max value constraint (if any)
            // If the exact value is in conflict with the max value, the max value should win
            resolved = max;
        }

        if ( resolved < min )
        {
            // Apply the min value constraint (if any)
            // If the exact or max value is in conflict with the min value, the min value should win
            resolved = min;
        }

        return resolved;
    }
}

Research

image

This is the sus code (from the iOS EditorHandler). The problem only occurs for us when the height constraint is coming in as infinity, which also plays into the earlier messages about how VerticalStackLayout was not playing nicely.

Oddly enough, contrary to what the comment says, when I comment out the sus code no exceptions are thrown and everything seems to play nicely:

    public override Size GetDesiredSize( double widthConstraint, double heightConstraint )
    {
        //if ( double.IsInfinity( widthConstraint ) || double.IsInfinity( heightConstraint ) )
        //{
        //    // If we drop an infinite value into base.GetDesiredSize for the Editor, we'll
        //    // get an exception; it doesn't know what do to with it. So instead we'll size
        //    // it to fit its current contents and use those values to replace infinite constraints

        //    PlatformView.SizeToFit();

        //    if ( double.IsInfinity( widthConstraint ) )
        //    {
        //        widthConstraint = PlatformView.Frame.Size.Width;
        //    }

        //    if ( double.IsInfinity( heightConstraint ) )
        //    {
        //        heightConstraint = PlatformView.Frame.Size.Height;
        //    }
        //}

        var desiredSize = base.GetDesiredSize( widthConstraint, heightConstraint );
        Debug.WriteLine( "constraints:" + widthConstraint + "x" + heightConstraint );
        Debug.WriteLine( "size: " + desiredSize.Width + "x" + desiredSize.Height );

        return desiredSize;
    }

The ouput (that I slimmed down) seems to properly respect everything:

[0:] constraints:432x∞
[0:] size: 43.666666666666664x169

[0:] constraints:432x∞
[0:] size: 43.666666666666664x188

[0:] constraints:432x∞
[0:] size: 43.666666666666664x200
  <Editor
          MaximumHeightRequest="200"
          MinimumHeightRequest="100"
          BackgroundColor="Red"
          AutoSize="TextChanges" />

Before (code not commented out): Xamarin Simulator Windows_1qE42tbzuG

After (code commented out): Xamarin Simulator Windows_EcZKvKs4Nm

awattar commented 4 months ago

Any updates?

Please take into account while working on this issue that there can be no AccessoryView or AccessoryView with one or two bars - one with Done button and the oder with Spellcheck (IsSpellCheckEnabled) and/or Suggestions (IsTextPredictionEnabled) when KeyboardFlags for the custom Keyboard type are specified.

https://learn.microsoft.com/en-us/dotnet/maui/user-interface/controls/editor?view=net-maui-8.0#customize-the-keyboard

filyXplor commented 1 month ago

Any update on this issue, I currently have this problem using version 8.0.90.

jonmdev commented 1 week ago

Thank you @jfversluis for encouraging me to check again on the workarounds posted here. I think when I last looked at them, we were still dealing with the major border and other iOS drawing problems and animations that @albyrock87 fixed since. So it was hard to evaluate it properly back then.

But I tried overnight re-implementing @bradencohen 's code here:

https://github.com/dotnet/maui/issues/17757#issuecomment-2025779894

This does seem to work now with no obvious issues I can see.

For anyone unfamiliar with implementing a custom handler, copy and paste this into a document like CustomEditorHandler.cs within your usual project namespace:

namespace PROJECT.NAMESPACE {

#if IOS
using CoreGraphics;

/// <summary>
/// A handler for the <see cref="Editor"/> control. //https://github.com/dotnet/maui/issues/17757
/// </summary>
internal class CustomIOSEditorHandler : Microsoft.Maui.Handlers.EditorHandler {
    /// <summary>
    /// Gets the desired size of the Editor.
    /// </summary>
    /// <param name="widthConstraint"></param>
    /// <param name="heightConstraint"></param>
    /// <returns></returns>
    public override Size GetDesiredSize(double widthConstraint, double heightConstraint) {
        //
        // Port of the default GetDesiredSizeFromHandler method (since internal):
        // https://github.com/dotnet/maui/blob/c7d1a4ec8d857aba674362d2777d98855f0ca67a/src/Core/src/Handlers/ViewHandlerExtensions.iOS.cs#L66
        //

        if (PlatformView == null || VirtualView == null) {
            return new Size(widthConstraint, heightConstraint);
        }

        // The measurements ran in SizeThatFits percolate down to child views
        // So if MaximumWidth/Height are not taken into account for constraints, the children may have wrong dimensions
        widthConstraint = Math.Min(widthConstraint, VirtualView.MaximumWidth);
        heightConstraint = Math.Min(heightConstraint, VirtualView.MaximumHeight);

        CGSize sizeThatFits = PlatformView.SizeThatFits(new CGSize((float)widthConstraint, (float)heightConstraint));

        var size = new Size(
            sizeThatFits.Width == float.PositiveInfinity ? double.PositiveInfinity : sizeThatFits.Width,
            sizeThatFits.Height == float.PositiveInfinity ? double.PositiveInfinity : sizeThatFits.Height);

        if (double.IsInfinity(size.Width) || double.IsInfinity(size.Height)) {
            PlatformView.SizeToFit();
            size = new Size(PlatformView.Frame.Width, PlatformView.Frame.Height);
        }

        var finalWidth = ResolveConstraints(size.Width, VirtualView.Width, VirtualView.MinimumWidth, VirtualView.MaximumWidth);
        var finalHeight = ResolveConstraints(size.Height, VirtualView.Height, VirtualView.MinimumHeight, VirtualView.MaximumHeight);

        return new Size(finalWidth, finalHeight);
    }

    /// <summary>
    /// Port of the internal ResolveConstraints method.
    /// </summary>
    /// <param name="measured"></param>
    /// <param name="exact"></param>
    /// <param name="min"></param>
    /// <param name="max"></param>
    /// <returns></returns>
    internal static double ResolveConstraints(double measured, double exact, double min, double max) {
        var resolved = measured;

        min = Microsoft.Maui.Primitives.Dimension.ResolveMinimum(min);

        if (Microsoft.Maui.Primitives.Dimension.IsExplicitSet(exact)) {
            // If an exact value has been specified, try to use that
            resolved = exact;
        }

        if (resolved > max) {
            // Apply the max value constraint (if any)
            // If the exact value is in conflict with the max value, the max value should win
            resolved = max;
        }

        if (resolved < min) {
            // Apply the min value constraint (if any)
            // If the exact or max value is in conflict with the min value, the min value should win
            resolved = min;
        }

        return resolved;
    }
}
#endif
}

And then in MauiProgram.cs add something like:

builder
                .UseMauiApp<App>()
                .ConfigureFonts(fonts => {
                      //whatever
                })
                .ConfigureMauiHandlers((handlers) => {
#if IOS
                    handlers.AddHandler<Microsoft.Maui.Controls.Editor, CustomIOSEditorHandler>(); //FIX SCROLLING ISSUE https://github.com/dotnet/maui/issues/17757
#endif
                });

I have only briefly tested it so it may need more rigorous examination. But in combination with all the prior Border and iOS animation fixes it appears we may have a working Editor now after all. Thanks again to all. 🙂👍

It seems Maui is getting close to production ready.