maui
maui copied to clipboard
ScrollView frozen in iOS when data is loaded async
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
- Create a default maui project via command line
dotnet new maui -n "maui-repro"
- 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; }
}
- 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
Have you tried wrapping only AnimalItems.Add(item);
line inside of MainThread.BeginInvokeOnMainThread?
Just tested that. Wrapping AnimalItems.Add(item);
line inside of MainThread.BeginInvokeOnMainThread
made no difference.
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);
}
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.
Did you try Task.Run(LoadMauiAsset); instead of LoadMauiAsset() ? I've seen that construct in a demo somewhere and it works for me.
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.
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 :/
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.
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.
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.
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..
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 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.
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 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();
}
The AppendToMapping implementation worked for me! Thanks!
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?
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());
});