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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cell reuse works incorrectly with UILabel text updates #4373

Open
6 tasks
alexkivikoski opened this issue Feb 18, 2022 · 3 comments
Open
6 tasks

Cell reuse works incorrectly with UILabel text updates #4373

alexkivikoski opened this issue Feb 18, 2022 · 3 comments

Comments

@alexkivikoski
Copy link

alexkivikoski commented Feb 18, 2022

馃悰 Bug Report

A simple program that binds an UITableView source to items with multiple texts and binds those texts to UILabels works incorrectly.

When the list is updated and overflowing cells are reused, the reused cells sometimes update their bindings only one frame after the cell is drawn on the screen. This causes visible flickering to the user.

Tested on MvvmCross 8.0.2 and iOS 15.2 emulator and real devices.

Expected behavior

Reused cells update their bindings before they are drawn. Appeared cells should not display old data.

Reproduction steps

Add the following ViewModel to the core and navigate to it.

FlickerTestViewModel:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MvvmCross.ViewModels;

namespace flickertest.Core.ViewModels
{
    public class FlickeringItem
    {
        static string[] titles = { "one", "two", "three", "four", "five", "six", "seven", "eight", "potato", "milk", "carrot", "bread" };
        static Random r = new Random();
        public string Title { get; set; }
        public string TimeText { get; set; }
        public string XExercisesText { get; set; }

        public static FlickeringItem GenerateRandom()
        {
            var item = new FlickeringItem();
            item.Title = GetRandomTitle();
            item.TimeText = GetRandomTitle();
            item.XExercisesText = GetRandomTitle();
            return item;
        }

        private static string GetRandomTitle()
        {
            List<string> words = new List<string>();
            for (int i = 0; i < r.Next(3, 10); i++)
            {
                words.Add(titles[r.Next(0, titles.Count())]);
            }
            return string.Join(" ", words);
        }
    }
    public class FlickerTestViewModel : MvxViewModel
    {
        Random r = new Random();

        private List<FlickeringItem> listItems;
        public List<FlickeringItem> ListItems
        {
            get { return listItems; }
            set
            {
                if (value == listItems) return;
                listItems = value;
                RaisePropertyChanged(() => ListItems);
            }
        }
        public FlickerTestViewModel()
        {
        }
        public override async Task Initialize()
        {
            await base.Initialize();
            _ = SampleLoop();
        }
        private async Task SampleLoop()
        {
            for (int i = 0; i < 20; i++)
            {
                await Task.Delay(3000).ConfigureAwait(true);
                ListItems = GenerateData();
            }
        }
        private List<FlickeringItem> GenerateData()
        {
            var res = new List<FlickeringItem>();
            for (int i = 0; i < r.Next(10, 35); i++)
            {
                res.Add(FlickeringItem.GenerateRandom());
            }
            return res;
        }
    }
}

Here is the code for the view and it's cells.

FlickerTestView:

using MvvmCross.Platforms.Ios.Binding.Views;
using MvvmCross.Platforms.Ios.Views;

using MvvmCross.Binding.BindingContext;
using Foundation;
using UIKit;
using flickertest.Core.ViewModels;

namespace flickertest.iOS.Views
{
    public class FlickerTestView : MvxTableViewController<FlickerTestViewModel>
    {
        public FlickerTestView()
        {
        }
        public override void ViewDidLoad()
        {
            base.ViewDidLoad();
            var Source = new MvxSimpleTableViewSource(TableView, ItemCell.Identifier);
            TableView.RegisterClassForCellReuse(typeof(ItemCell), ItemCell.Identifier);
            TableView.Source = Source;
            TableView.RowHeight = 80;
            var set = this.CreateBindingSet<FlickerTestView, FlickerTestViewModel>();
            set.Bind(Source).For(s => s.ItemsSource).To(s => s.ListItems).OneWay();
            set.Apply();
        }

    }
    public class ItemCell : MvxTableViewCell
    {
        public static NSString Identifier = new NSString(nameof(ItemCell));
        private UILabel _title, _title2, _title3;

        public ItemCell(IntPtr handle) : base(handle)
        {
            Initialize();
        }

        public ItemCell()
        {
            Initialize();
        }

        private void Initialize()
        {
            _title = new UILabel { TranslatesAutoresizingMaskIntoConstraints = false };
            _title2 = new UILabel { TranslatesAutoresizingMaskIntoConstraints = false };
            _title3 = new UILabel { TranslatesAutoresizingMaskIntoConstraints = false };


            ContentView.Add(_title);
            ContentView.Add(_title2);
            ContentView.Add(_title3);

            _title.LeadingAnchor.ConstraintEqualTo(ContentView.LeadingAnchor, 10).Active = true;
            _title.TrailingAnchor.ConstraintEqualTo(ContentView.TrailingAnchor, -10).Active = true;
            _title.CenterYAnchor.ConstraintEqualTo(ContentView.CenterYAnchor).Active = true;

            _title2.LeadingAnchor.ConstraintEqualTo(ContentView.LeadingAnchor, 10).Active = true;
            _title2.TrailingAnchor.ConstraintEqualTo(ContentView.TrailingAnchor, -10).Active = true;
            _title2.BottomAnchor.ConstraintEqualTo(_title.TopAnchor,-5).Active = true;

            _title3.LeadingAnchor.ConstraintEqualTo(ContentView.LeadingAnchor, 10).Active = true;
            _title3.TrailingAnchor.ConstraintEqualTo(ContentView.TrailingAnchor, -10).Active = true;
            _title3.TopAnchor.ConstraintEqualTo(_title.BottomAnchor,5).Active = true;
            

            var set = this.CreateBindingSet<ItemCell, FlickeringItem>();
            set.Bind(_title).To(vm => vm.Title);
            set.Bind(_title2).To(vm => vm.TimeText);
            set.Bind(_title3).To(vm => vm.XExercisesText);
            set.Apply();
        }
    }
}

Configuration

Version: 8.0.2

Platform:

  • [ x] 馃摫 iOS
  • 馃 Android
  • 馃弫 WPF
  • 馃寧 UWP
  • 馃崕 MacOS
  • 馃摵 tvOS
  • 馃悞 Xamarin.Forms
@alexkivikoski
Copy link
Author

I did some further simplification and the problem is even more obvious when the list items are not changed but instead a new list containing the same items is created:

ListItems = new List<FlickeringItem>(ListItems);

The order of the items stays the same, but some of the text labels flicker for a single frame.

This problem is urgent for us and we are looking to find some help on this.

@Cheesebaron
Copy link
Member

If it is super urgent, roll your own DataSource without bindings and do the binding yourself, doesn't take much effort.

@alexkivikoski
Copy link
Author

Thank you, I solved this by keeping the rest of the bindings and setting the problematic values manually in
protected override UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

2 participants