0

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

  • I know you said it is hard to repro the issue, but have you ever tried not dequeuing the cells and just creating a new one as a quick test. Or how about adding a `UITableView.ReloadData()` button to your toolbar, does the forced refresh paint the the table view properly? – SushiHangover Apr 14 '16 at 15:04
  • I tried `TableView.ReloadData()` and `this.ReloadData()` it did not repaint the table view properly. – Andrew Bonsall Apr 14 '16 at 18:26
  • hmmm, Did you call `ReloadData()` on the main UI thread? (via `InvokeOnMainThread` or `BeginInvokeOnMainThread`) – SushiHangover Apr 14 '16 at 18:41
  • I wasn't but I am now. Still nothing. – Andrew Bonsall Apr 14 '16 at 19:17
  • I used InvokeOnMainThread like this `NavigationItem.RightBarButtonItem = new UIBarButtonItem ("Refresh", UIBarButtonItemStyle.Plain, (object sender, EventArgs e) => { InvokeOnMainThread( () => { TableView.ReloadData(); }); });` – Andrew Bonsall Apr 14 '16 at 19:19

2 Answers2

0

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...

Sreeraj
  • 2,306
  • 2
  • 18
  • 31
0

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.