maui icon indicating copy to clipboard operation
maui copied to clipboard

ScrollView frozen in iOS when data is loaded async

Open stankovski opened this issue 2 years ago • 7 comments

Description

ScrollView populated via an async operation does not scroll in iOS. It scrolls fine on Maccatalyst and Windows using mouse scroller.

.NET Version: 6.0.400-preview.22330.6

Steps to Reproduce

  1. Create a default maui project via command line dotnet new maui -n "maui-repro"
  2. In MainPage.cs add the following code:
using System.Collections.ObjectModel;

namespace maui_repro;
public partial class MainPage : ContentPage
{
    public ObservableCollection<AnimalItem> AnimalItems { get; private set; } = new ObservableCollection<AnimalItem>();

    public MainPage()
    {
        InitializeComponent();
        LoadMauiAsset();
        BindingContext = this;
    }

    async Task LoadMauiAsset()
    {
        await Task.Delay(500);
        for (int i = 0; i < 30; i++)
        {
            var item = new AnimalItem();
            item.EnglishContent = "FOR the most wild, yet most homely narrative which I am about to pen, I neither expect nor solicit belief. Mad indeed would I be to expect it, in a case where my very senses reject their own evidence. Yet, mad am I not -- and very surely do I not dream. But to-morrow I die, and to-day I would unburthen my soul. My immediate purpose is to place before the world, plainly, succinctly, and without comment, a series of mere household events. In their consequences, these events have terrified -- have tortured -- have destroyed me. Yet I will not attempt to expound them. To me, they have presented little but Horror -- to many they will seem less terrible than barroques. Hereafter, perhaps, some intellect may be found which will reduce my phantasm to the common-place -- some intellect more calm, more logical, and far less excitable than my own, which will perceive, in the circumstances I detail with awe, nothing more than an ordinary succession of very natural causes and effects.";
            AnimalItems.Add(item);
        }
    }
}

public class AnimalItem 
{
    public string EnglishContent { get; set; }
}
  1. In MainPage.xaml add the following XAML
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="maui_repro.MainPage">
    <ScrollView Grid.Row="0" VerticalOptions="FillAndExpand">
        <VerticalStackLayout BindableLayout.ItemsSource="{Binding AnimalItems}">
            <BindableLayout.ItemTemplate>
                <DataTemplate>
                    <Frame CornerRadius="8" Margin="10,5">
                        <Label Text="{Binding EnglishContent}" TextColor="Black"/>
                    </Frame>
                </DataTemplate>
            </BindableLayout.ItemTemplate>
        </VerticalStackLayout>
    </ScrollView>
</ContentPage>

Version with bug

6.0.400

Last version that worked well

Unknown/Other

Affected platforms

iOS

Affected platform versions

iOS 15

Did you find any workaround?

No. I've tried running LoadMauiAsset() inside of OnNavigated event as well as by wrapping it inside of MainThread.BeginInvokeOnMainThread but neither worked.

Relevant log output

No response

stankovski avatar Aug 05 '22 00:08 stankovski

Have you tried wrapping only AnimalItems.Add(item); line inside of MainThread.BeginInvokeOnMainThread?

Symbai avatar Aug 05 '22 07:08 Symbai

Just tested that. Wrapping AnimalItems.Add(item); line inside of MainThread.BeginInvokeOnMainThread made no difference.

stankovski avatar Aug 05 '22 16:08 stankovski

Have you tried to make an ICommand with the loading function, and then instead of loading it from the constructor doing this

protected override void OnAppearing()
{
	base.OnAppearing();
        LoadMauiAssetCommand.Execute(null);
}

3steve3 avatar Aug 05 '22 21:08 3steve3

Calling async logic from inside OnAppearing instead of ctor did not make a difference. I'm not sure how using ICommand would help as it's just another .NET class.

I've also experimented with ConfigureAwait and blocking the thread with GetAwaiter().GetResult() but the result was the deadlock where the splash screen never goes away.

stankovski avatar Aug 05 '22 22:08 stankovski

Did you try Task.Run(LoadMauiAsset); instead of LoadMauiAsset() ? I've seen that construct in a demo somewhere and it works for me.

kajetan-kazimierczak avatar Aug 08 '22 20:08 kajetan-kazimierczak

Using Task.Run(LoadMauiAsset) and forcing the method to be enqueued in a thread pool has a very bizarre and unexpected consequences. The page sometimes crashes, at other times the ScrollView has no visual elements, and at other times it has only some elements. I would be curious to understand why that is happening.

stankovski avatar Aug 09 '22 15:08 stankovski

Upon further investigation, this seems to be a bug in ScrollView as I was able to get CollectionView and ListView to scroll just fine. Unfortunately CollectionView seems to also have bugs with cell sizing while ListView ViewCell does not work as expected on iOS :/

stankovski avatar Aug 09 '22 19:08 stankovski

I could reproduce the error, the workaround was to use CollectionView, but I need to wrap it on the Grid control, because using StackLayout or VerticalStackLayout will sizes the ColloectionView wrong, given it too much height.

pictos avatar Aug 11 '22 22:08 pictos

A workaround that I found is to reset the Content on the scrollview. I tested with this example by giving the ScrollView a name of "scrollView" and modifying LoadMauiAsset to

    async Task LoadMauiAsset()
    {
        await Task.Delay(500);
        for (int i = 0; i < 30; i++)
        {
            var item = new AnimalItem();
            item.EnglishContent = "FOR the most wild, yet most homely narrative which I am about to pen, I neither expect nor solicit belief. Mad indeed would I be to expect it, in a case where my very senses reject their own evidence. Yet, mad am I not -- and very surely do I not dream. But to-morrow I die, and to-day I would unburthen my soul. My immediate purpose is to place before the world, plainly, succinctly, and without comment, a series of mere household events. In their consequences, these events have terrified -- have tortured -- have destroyed me. Yet I will not attempt to expound them. To me, they have presented little but Horror -- to many they will seem less terrible than barroques. Hereafter, perhaps, some intellect may be found which will reduce my phantasm to the common-place -- some intellect more calm, more logical, and far less excitable than my own, which will perceive, in the circumstances I detail with awe, nothing more than an ordinary succession of very natural causes and effects.";
            AnimalItems.Add(item);
        }
        var content = scrollView.Content;
        scrollView.Content = null;
        scrollView.Content = content;
    }

I had to set the Content to null first because the set doesn't do anything if the Content value isn't changing from its current value.

johnl188 avatar Oct 10 '22 14:10 johnl188

This case is more critical and more generic. This is not due to a "data loaded async", but to a "content changed at runtime" main thread whatever. If you change the real size of content at runtime then the scrollview doesn't take it into consideration, but keeps the allowed scroll limits from in-before, blocking the scrolling. Was on iOS 16 real device. The content size was changed by 2 different means, they both result in same bug demonstrated. 1 - one of the children is a stacklayout, starting with isvisible=false, then toggle (on ui thread) to true. 2 - one of the children is a stacklayout inside border, with a bindablelayout.itemssource changing (on ui thread) by an "add to observablecollection". Scrollview is a base control and not being able to use it in real case scenarios makes maui remain in beta state, beware.

taublast avatar Nov 04 '22 06:11 taublast

The workaround is here, hoping this will get fixed in no time now.

#if IOS
            Microsoft.Maui.Handlers.ScrollViewHandler.Mapper.AppendToMapping("ContentSize", (handler, view) =>
            {
                    handler.PlatformView.UpdateContentSize(handler.VirtualView.ContentSize);
                    PlatformArrange(handler.PlatformView.Frame.ToRectangle());
       	    });
#endif

Have seen some posted issues of a bugged scrollView on Catalyst, maybe the cause is same..

taublast avatar Nov 04 '22 07:11 taublast

Updated solution above to fix another not obvious bug: gestures where not passed to children that was wrongly estimated to be outside of the visible boundaries, after the content height was changed dynamically.

The native maui ios handler is actually not handling dynamic changes, but is focusing on recreating content from scratch. So this is rather not a bug but an unimplemented feature.. Integrating the code above to the native mapper would be the final fix.

taublast avatar Nov 04 '22 10:11 taublast

@taublast Thanks for posting a solution! I'm having problems implementing it. I'm trying to add it here.

public partial class App : Application
{
  Public App()
  {
    //Insert before MainPage = new AppShell();
  }
}

Is this where it goes? If so, I'm getting "does not exist in the current context" for PlatformArrange error. I'm targeting iOS 15.2.

faceoffers28 avatar Nov 04 '22 17:11 faceoffers28

One can customize the mapper or add a custom handler (and customize the maper inside and do more if needed). Custom handler:

MauiProgram.cs

public static MauiApp CreateMauiApp()
	{
		var builder = MauiApp.CreateBuilder();
		builder
			.UseMauiApp<App>()
			.whatever..

#if IOS
				builder.ConfigureMauiHandlers(collection => collection
					.AddHandler(typeof(ScrollView), typeof(YourNamespace.Platforms.iOS.FixedScrollViewHandler))
				);
#endif

                return builder.Build();
        }

Platforms/iOS/FixedScrollViewHandler.cs

using Foundation;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using UIKit;

namespace YourNamespace.Platforms.iOS
{
    public class FixedScrollViewHandler : ScrollViewHandler
    {
        private static bool Initialized;

        protected override UIScrollView CreatePlatformView()
        {
            if (!Initialized)
            {
                //fixed BUG https://github.com/dotnet/maui/issues/9209
                Microsoft.Maui.Handlers.ScrollViewHandler.Mapper.AppendToMapping("ContentSize", (handler, view) =>
                {
                    // native UIScrollView.contentSize != maui ScrollView.ContentSize.. Why?..
                    handler.PlatformView.UpdateContentSize(handler.VirtualView.ContentSize);
                    PlatformArrange(handler.PlatformView.Frame.ToRectangle());
                });
                Initialized = true;
            }

            return base.CreatePlatformView();
        }
    }
}

taublast avatar Nov 05 '22 06:11 taublast

@taublast thanks for the workaround, I would say we can simplify your fix using just the AppendToMapping

public static MauiApp CreateMauiApp()
	{
		var builder = MauiApp.CreateBuilder();
		builder
			.UseMauiApp<App>()
			.ConfigureFonts(fonts =>
			{
				fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
				fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
			});
#if IOS
		ScrollViewHandler.Mapper.AppendToMapping("ContentSize", (handler, view) =>
		{
			handler.PlatformView.UpdateContentSize(handler.VirtualView.ContentSize);
			handler.PlatformArrange(handler.PlatformView.Frame.ToRectangle());
		});
#endif
		return builder.Build();
	}

pictos avatar Nov 05 '22 19:11 pictos

The AppendToMapping implementation worked for me! Thanks!

faceoffers28 avatar Nov 07 '22 21:11 faceoffers28

Hey guys, I'm getting a lot of compiler errors for "UpdateContentSize" and "Frame" when using Microsoft.Maui.Handlers.ScrollViewHandler type. Should I be using a different type? Or missing a reference?

PhDave13 avatar Nov 10 '22 01:11 PhDave13

If your ScrollView specifies a Padding, you'll also need to take that in account in the AppendToMapping workaround:

        ScrollViewHandler.Mapper.AppendToMapping("ContentSize", (handler, view) =>
        {
            var contentSize = handler.VirtualView.ContentSize;
            var size = new Size(contentSize.Width + view.Padding.HorizontalThickness, contentSize.Height + view.Padding.VerticalThickness);
            handler.PlatformView.UpdateContentSize(size);
            handler.PlatformArrange(handler.PlatformView.Frame.ToRectangle());
        });

paulvarache avatar Nov 25 '22 10:11 paulvarache