Xamarin custom UITableViewCell overlapping text on reuse

1k Views Asked by At

I'm using the Monotouch.Dialog framework to build forms for user input my app. I have run into an odd problem where the screen displays overlapping text when I subclass UITableViewCell. When I say text overlapping I mean it looks as if one entire cell is placed on top of another, not just labels being pushed together or misplaced.

I followed this example on the Xamarin website here.

Basically I have this UITableViewCell class

public class SAFutureAdCell : UITableViewCell
{
    private UILabel issueAndSizeLabel, mailDateLabel, orderDateLabel, miscLabel, totalLabel;
    private UIImage monthlyImage, ccoImage;

    public SAFutureAdCell(IntPtr p) : base(p) 
    {
        init ();
    }
    public SAFutureAdCell(string resuseIdentifier) : base(UITableViewCellStyle.Default, resuseIdentifier)
    {
        init ();
    }
    public SAFutureAdCell(NSString cellKey): base(UITableViewCellStyle.Default, cellKey)
    {
        init ();
    }

    private void init()
    {
        issueAndSizeLabel = new UILabel() {
            TextColor = UIColor.Black,
            Font = UIFont.BoldSystemFontOfSize(15),
            BackgroundColor = UIColor.Clear,
            ShadowColor = UIColor.Clear,
            TextAlignment = UITextAlignment.Left,
            ClipsToBounds = true
        };

        mailDateLabel = new UILabel() {
            TextColor = UIColor.Black,
            Font = UIFont.BoldSystemFontOfSize(15),
            BackgroundColor = UIColor.Clear,
            ShadowColor = UIColor.Clear,
            TextAlignment = UITextAlignment.Left,
            ClipsToBounds = true
        };

        orderDateLabel = new UILabel() {
            TextColor = UIColor.Gray,
            Font = UIFont.SystemFontOfSize(13),
            BackgroundColor = UIColor.Clear,
            ShadowColor = UIColor.Clear,
            TextAlignment = UITextAlignment.Left,
            ClipsToBounds = true
        };

        miscLabel = new UILabel() {
            TextColor = UIColor.Gray,
            Font = UIFont.SystemFontOfSize(13),
            BackgroundColor = UIColor.Clear,
            ShadowColor = UIColor.Clear,
            TextAlignment = UITextAlignment.Right,
            ClipsToBounds = true
        };

        totalLabel = new UILabel() {
            TextColor = UIColor.Black,
            Font = UIFont.BoldSystemFontOfSize(15),
            BackgroundColor = UIColor.Clear,
            ShadowColor = UIColor.Clear,
            TextAlignment = UITextAlignment.Right,
            ClipsToBounds = true
        };

        monthlyImage = AppearanceManager.MonthlyIndicator; // small blue circle
        ccoImage = AppearanceManager.CcoIndicator; // small red circle
        ImageView.Image = monthlyImage;
        ImageView.ContentMode = UIViewContentMode.ScaleAspectFit;

        ContentView.AddSubviews(issueAndSizeLabel, mailDateLabel, orderDateLabel, miscLabel, totalLabel);

    }

    public override void LayoutSubviews()
    {
        base.LayoutSubviews();

        Accessory = UITableViewCellAccessory.DisclosureIndicator;

        var b = ContentView.Bounds;
        b.Width -= 30;
        var x = 24;

        issueAndSizeLabel.Frame = new RectangleF(x, 5, b.Width / 2, b.Height / 2 - 5).RoundFloats();
        mailDateLabel.Frame = new RectangleF(issueAndSizeLabel.Frame.Right, 5, b.Width / 4, b.Height / 2 - 5).RoundFloats();
        totalLabel.Frame = new RectangleF(mailDateLabel.Frame.Right, 5, b.Width / 4, b.Height / 2 - 5).RoundFloats();
        orderDateLabel.Frame = new RectangleF(x, b.Height / 2 + 5, b.Width / 3, b.Height / 2 - 10).RoundFloats();
        miscLabel.Frame = new RectangleF(totalLabel.Frame.Left, b.Height / 2 + 5, b.Width / 4, b.Height / 2 - 10).RoundFloats();
        ImageView.Frame = new RectangleF(5, 0, AppearanceManager.MonthlyIndicator.Size.Width, ContentView.Frame.Height);
    }

    public void Update(Ad ad)
    {
        issueAndSizeLabel.Text = string.Format("{0} - {1}", ad.IssueCode, ad.AdvertSize);
        mailDateLabel.Text = string.Format("Mail: {0}", ad.MailDate.ToShortDateString());
        orderDateLabel.Text = string.Format("Order: {0}", ad.OrderDate.ToShortDateString());
        miscLabel.Text = string.Format("Misc: {0}", ad.Misc.ToCurrency());
        totalLabel.Text = string.Format("Total: {0}", ad.Total.ToCurrency());

        if (ad.IsCCOPackage)
            ImageView.Image = ccoImage;
        else
            ImageView.Image = monthlyImage;

        ImageView.Alpha = (ad.IsMonthlyBilling || ad.IsCCOPackage) ? 1f : 0f;
    }
}

This cell gets used from this Element.

public class SAFutureAdDetailElement : Element, IElementSizing
{
    public SAFutureAdDetailElement(Ad ad, NSAction tapped) : base("FutureAdDetail")
    {
        this.ad = ad;
        Tapped += tapped;
    }

    public SAFutureAdDetailElement(Ad ad) : base("FutureAdDetail")
    {
        this.ad = ad;
    }

    private static NSString cellKey = new NSString("SAFutureAdDetailElement");
    protected override NSString CellKey
    {
        get
        {
            return cellKey;
        }
    }

    public override UITableViewCell GetCell(UITableView tv)
    {
        var cell = tv.DequeueReusableCell (cellKey) as SAFutureAdCell;
        if (cell == null) {
            cell = new SAFutureAdCell(cellKey);
        }

        cell.Update (ad);

        return cell;
    }

    public override void Selected(DialogViewController dvc, UITableView tableView, NSIndexPath path)
    {
        if (Tapped != null)
            Tapped ();

        tableView.DeselectRow (path, true);
    }

    public float GetHeight(UITableView tableView, NSIndexPath indexPath)
    {
        return cellHeight;
    }

    private Ad ad;
    public event NSAction Tapped;

    private int cellHeight = 60;
}

A collection of these elements are added to an MTD Section like so

Section CreateOnPageSection()
{
    var onPageSection = new Section ("Future On Page Ads");
    onPageSection.AddAll (
        from ad in orderInfo.AdsFuture
        where !Settings.offPageAdSizes.Contains (ad.AdvertSizeID)
        select new SAFutureAdDetailElement (
            ad, 
            () => {
                NavigationController.PushViewController (new FutureAdDetailController (customer.CustID, ad, this), true);
            }
        )
    );
    return onPageSection;
 }

Which is then added to a RootElement as a part of a DialogViewController. The root element has 3 other sections added to it, all with custom elements that have their own custom UITableViewCells and they are all very similar. They basically differ in the placement of the labels and the data that goes into the labels.

The text doesn't overlap until scrolling occurs or until you push a view onto the NavigationController and then pop back to this view. It is often difficult to reproduce on newer iPads as well (this app is iPad only). This issue happens much more frequently on older iPad 2s. Sometimes i can get it to happen on my Air but it is much more difficult to make it happen on that machine.

Here is a screenshot of the overlapping text.

IOS UITableViewCell Overlapping Text

2

There are 2 best solutions below

0
Andrew Bonsall On BEST ANSWER

OK so I found answer to my own question. It looks like there is an issue with how LayoutSubviews works with the cell recycling. Basically what I did was I got rid of the custom cell and moved all of the display loginc into my GetCell method in the custom element. The element class looks like so.

public class SAFutureAdDetailElement : Element, IElementSizing
{
    public SAFutureAdDetailElement(Ad ad, NSAction tapped) : base("FutureAdDetail")
    {
        this.ad = ad;
        Tapped += tapped;
    }

    public SAFutureAdDetailElement(Ad ad) : base("FutureAdDetail")
    {
        this.ad = ad;
    }

    private static NSString cellKey = new NSString("SAFutureAdDetailElement");
    protected override NSString CellKey
    {
        get
        {
            return cellKey;
        }
    }

    public override UITableViewCell GetCell(UITableView tv)
    {
        var cell = tv.DequeueReusableCell (cellKey);
        if (cell == null) {
            cell = new UITableViewCell (UITableViewCellStyle.Default, cellKey);

            cell.IndentationLevel = 0;
            var x = 24;
            var contentWidth = tv.Bounds.Width - 30 - x;
            var issueAndSizeLabel = new UILabel (new RectangleF (x, 5, contentWidth / 2, cellHeight / 2 - 5).RoundFloats ()) {
                TextColor = UIColor.Black,
                Font = UIFont.BoldSystemFontOfSize (15),
                BackgroundColor = UIColor.Clear,
                ShadowColor = UIColor.Clear,
                TextAlignment = UITextAlignment.Left,
                ClipsToBounds = true,
                Text = string.Format ("{0} - {1}", ad.IssueCode, ad.AdvertSize)
            };

            var mailDateLabel = new UILabel (new RectangleF (issueAndSizeLabel.Frame.Right, 5, contentWidth / 4, cellHeight / 2 - 5).RoundFloats ()) {
                TextColor = UIColor.Black,
                Font = UIFont.BoldSystemFontOfSize (15),
                BackgroundColor = UIColor.Clear,
                ShadowColor = UIColor.Clear,
                TextAlignment = UITextAlignment.Left,
                ClipsToBounds = true,
                Text = string.Format ("Mail: {0}", ad.MailDate.ToShortDateString ())
            };

            var orderDateLabel = new UILabel (new RectangleF (x, cellHeight / 2 + 5, contentWidth / 3, cellHeight / 2 - 10).RoundFloats ()) {
                TextColor = UIColor.Gray,
                Font = UIFont.SystemFontOfSize (13),
                BackgroundColor = UIColor.Clear,
                ShadowColor = UIColor.Clear,
                TextAlignment = UITextAlignment.Left,
                ClipsToBounds = true,
                Text = string.Format ("Order: {0}", ad.OrderDate.ToShortDateString ())
            };

            var totalLabel = new UILabel (new RectangleF (mailDateLabel.Frame.Right, 5, contentWidth / 4, cellHeight / 2 - 5).RoundFloats ()) {
                TextColor = UIColor.Black,
                Font = UIFont.BoldSystemFontOfSize (15),
                BackgroundColor = UIColor.Clear,
                ShadowColor = UIColor.Clear,
                TextAlignment = UITextAlignment.Right,
                ClipsToBounds = true,
                Text = string.Format ("Total: {0}", ad.Total.ToCurrency ())
            };

            var miscLabel = new UILabel (new RectangleF (totalLabel.Frame.Left, cellHeight / 2 + 5, contentWidth / 4, cellHeight / 2 - 10).RoundFloats ()) {
                TextColor = UIColor.Gray,
                Font = UIFont.SystemFontOfSize (13),
                BackgroundColor = UIColor.Clear,
                ShadowColor = UIColor.Clear,
                TextAlignment = UITextAlignment.Right,
                ClipsToBounds = true,
                Text = string.Format ("Misc: {0}", ad.Misc.ToCurrency ())
            };

            var indicator = new UIImageView (new RectangleF (5, 0, AppearanceManager.MonthlyIndicator.Size.Width, cellHeight));
            indicator.ContentMode = UIViewContentMode.ScaleAspectFit;
            if (ad.IsCCOPackage) {
                indicator.Image = AppearanceManager.CcoIndicator;
            } else if (ad.IsMonthlyBilling) {
                indicator.Image = AppearanceManager.MonthlyIndicator;
            }

            cell.Accessory = UITableViewCellAccessory.DisclosureIndicator;

            cell.ContentView.AddSubviews (indicator, issueAndSizeLabel, mailDateLabel, orderDateLabel, miscLabel, totalLabel);
        }

        ((UILabel)cell.ContentView.Subviews[1]).Text = string.Format("{0} - {1}", ad.IssueCode, ad.AdvertSize);
        ((UILabel)cell.ContentView.Subviews[2]).Text = string.Format("Mail: {0}", ad.MailDate.ToShortDateString());
        ((UILabel)cell.ContentView.Subviews[3]).Text = string.Format("Order: {0}", ad.OrderDate.ToShortDateString());
        ((UILabel)cell.ContentView.Subviews[4]).Text = string.Format("Misc: {0}", ad.Misc.ToCurrency());
        ((UILabel)cell.ContentView.Subviews[5]).Text = string.Format("Total: {0}", ad.Total.ToCurrency());
        if (ad.IsCCOPackage) {
            ((UIImageView)cell.ContentView.Subviews [0]).Image = AppearanceManager.CcoIndicator;
        } else if (ad.IsMonthlyBilling) {
            ((UIImageView)cell.ContentView.Subviews [0]).Image = AppearanceManager.MonthlyIndicator;
        } else {
            ((UIImageView)cell.ContentView.Subviews [0]).Image = null;
        }
        return cell;
    }

    public override void Selected(DialogViewController dvc, UITableView tableView, NSIndexPath path)
    {
        if (Tapped != null)
            Tapped ();

        tableView.DeselectRow (path, true);
    }

    public float GetHeight(UITableView tableView, NSIndexPath indexPath)
    {
        return cellHeight;
    }

    private Ad ad;
    public event NSAction Tapped;

    private int cellHeight = 60;
}

The only thing I don't like about this is updating the cell values is kind of a pain since I have to grab theUILabel from an array of UIViews and cast them as such like this ((UILabel)cell.ContentView.Subviews[1]), And good luck keeping track of which UILabel is which since you now can't use a variable name to reference the UILabel. Yes I did try making the labels a private member of the element and tried updating them that way which didn't work.

1
Sreeraj On

It has to be a case of cell not getting re-used properly. So my intuition is that a change in GetCell method will do the job.

public override UITableViewCell GetCell(UITableView tv)
{
    var cell = tv.DequeueReusableCell (cellKey);// as SAFutureAdCell;
    if (cell == null) {
        cell = new SAFutureAdCell(cellKey);
    }
    else
        {
            cell.Element = this;
        }

    cell.Update (ad);

    return cell;
}

Can you try changing GetCell method like this ? It might fix it...