Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom UITableViewCell: First Row Has No Content

I am trying to create a two-level UITableView implementation for a comment and reply panel. The first level contains all top-level comments and if there are replies to that comment, there will be an indicator. When you tap on a top-level comment cell, the panel will animate a new UITableView into view. The first cell is the comment the user tapped on and below that is a cell for each reply to that comment.

I accomplish by using two different UITableViews and two different UITableViewSources (but they share the same base class). When the user taps on a top-level comment, the controller that manages the tables (CommentPanelViewController) animates the old view (top-level comments) out of sight and the new view (replies) into sight.

The Issue:

When I tap on the top-level comment, nothing but it's indicator shows up. All of the other replies display fine but the top-level comment has no text, no author and no timestamp on it.

To keep things concise and easy to follow, I will post just the necessary code. The top-level comments view works perfectly, only the reply view is buggy so we will start there:

CommentSource - the base table source

public abstract class CommentSource : UITableViewSource
{
    protected List<Comment> _data;

    public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
    {
        var comment = _data[indexPath.Row];
        var cell = tableView.DequeueReusableCell(CellId)
            as CommentCell ?? new CommentCell(new NSString("CellId"), CommentLineCount,
            comment.Replies != null && comment.Replies.Count > 0);

        cell.SelectionStyle = UITableViewCellSelectionStyle.None;
        cell.LayoutMargins = UIEdgeInsets.Zero;
        cell.SeparatorInset = UIEdgeInsets.Zero;
        cell.SetNeedsUpdateConstraints();
        cell.UpdateConstraintsIfNeeded();
        cell.UpdateCell(comment);
        cell.DrawIndicator(comment);

        DrawAccessories(comment, cell);

        return cell;
    }

    protected virtual void DrawAccessories(Comment comment, CommentCell cell) { }

    protected abstract int CommentLineCount { get; }

    protected abstract string CellId { get; }

    public override nint RowsInSection(UITableView tableview, nint section) => _data?.Count ?? 0;

    public void UpdateData(IEnumerable<Comment> comments)
    {
        _data = OrderComments(comments);
    }

    private static List<Comment> OrderComments(IEnumerable<Comment> comments) =>
        comments?.OrderBy(x => x.CreatedDateTime).ToList();
}

CommentViewSource - the source for top-level comments

public class CommentViewSource : CommentSource
{
    protected override int CommentLineCount => 3;
    protected override string CellId => "CommentCell";

    public Action<Comment, bool> CommentSelected { get; set; }

    public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
    {
        var commentCell = tableView.CellAt(indexPath) as CommentCell;
        CommentSelected(_data[indexPath.Row], commentCell != null && commentCell.IsEllipsed);
    }

    protected override void DrawAccessories(Comment comment, CommentCell cell)
    {
        base.DrawAccessories(comment, cell);
        if (comment.Replies.Count > 0)
        {
            cell.DrawReplyCountIndicator(comment);
        }
    }
}

ReplyViewSource - source for the replies

public class ReplyViewSource : CommentSource
{
    protected override int CommentLineCount => 0;

    protected override string CellId => "ReplyCell";
}

So when a top-level comment is selected, CommentViewSource.RowSelected is called which calls CommentViewSource.CommentSelected which is handled:

CommentPanelViewController.Constructor:

public CommentPanelViewController(CommentViewSource commentSource,
    CommentSource replySource, Action dismissHandler)
{
    _isReplyVisible = false;
    _commentSource = commentSource;
    _commentSource.CommentSelected += (comment, isEllipsed) =>
    {
        if (comment.Replies.Count <= 0 && !isEllipsed) { return; }

        var replies = new List<Comment>(comment.Replies);
        if (!replies.Contains(comment))
        {
            replies.Insert(0, comment);
        }
        _replySource.UpdateData(replies);
        _replyView.Table.ReloadData();
        AnimateReplyView(true);
    };
    _replySource = replySource;

    ..........
 }

And now for the big one, the custom UITableViewCell. This class is used for both the replies and the top-level comments:

CommentCell

public sealed class CommentCell : UITableViewCell
{
    private const string CustomCommentCss =
        "<style>*{{font-family:{0};font-size:{1};color:{2};}}span{{font-weight:600;}}</style>";
    private readonly bool _hasReplies;
    private readonly UILabel _creatorLabel;
    private readonly UILabel _commentLabel;
    private readonly UILabel _dateLabel;
    private readonly UIFont _font;
    private bool _didUpdateConstraints;
    private UIView _indicator;
    private ReplyCountIndicatorView _replyCountIndicator;

    public CommentCell(NSString cellId, int numberOfLines, bool hasReplies) :
        base(UITableViewCellStyle.Default, cellId)
    {
        _hasReplies = hasReplies;
        _didUpdateConstraints = false;
        SelectionStyle = UITableViewCellSelectionStyle.None;

        var textColor = Globals.ColorDark;

        _font = UIFont.FromName(Globals.FontSanFranLight, Globals.FontSizeBody);
        _creatorLabel = new UILabel
        {
            Font = UIFont.FromName(Globals.FontSanFranSemiBold, Globals.FontSizeBody),
            Lines = 1,
            LineBreakMode = UILineBreakMode.TailTruncation,
            TextColor = textColor
        };
        _commentLabel = new UILabel
        {
            Font = _font,
            Lines = numberOfLines,
            LineBreakMode = UILineBreakMode.TailTruncation,
            TextColor = textColor
        };
        _dateLabel = new UILabel
        {
            Font = UIFont.FromName(Globals.FontSanFranLight, Globals.FontSizeSmall),
            TextColor = Globals.ColorDisabled
        };

        ContentView.AddSubviews(_creatorLabel, _commentLabel, _dateLabel);
    }

    public bool IsEllipsed => _commentLabel.Text.StringSize(
        _commentLabel.Font).Width > 3 * _commentLabel.Bounds.Size.Width;

    public override void UpdateConstraints()
    {
        base.UpdateConstraints();
        _creatorLabel.SetContentCompressionResistancePriority(1000, UILayoutConstraintAxis.Vertical);
        _commentLabel.SetContentCompressionResistancePriority(1000, UILayoutConstraintAxis.Vertical);
        _dateLabel.SetContentCompressionResistancePriority(1000, UILayoutConstraintAxis.Vertical);
        _replyCountIndicator?.SetContentCompressionResistancePriority(1000, UILayoutConstraintAxis.Vertical);

        if (_didUpdateConstraints || (_replyCountIndicator == null && _hasReplies)) { return; }

        var leftMargin = AnnotationIndicator.Size.Width + 2 * Globals.MarginGrid;

        if (_replyCountIndicator != null && _hasReplies)
        {
            ContentView.ConstrainLayout(() =>
                _creatorLabel.Frame.Top == ContentView.Frame.Top + Globals.MarginGrid &&
                _creatorLabel.Frame.Left == ContentView.Frame.Left + leftMargin &&
                _creatorLabel.Frame.Right == ContentView.Frame.Right - Globals.MarginGrid &&

                _commentLabel.Frame.Top == _creatorLabel.Frame.Bottom + Globals.MarginGrid / 4 &&
                _commentLabel.Frame.Left == _creatorLabel.Frame.Left &&
                _commentLabel.Frame.Right == _creatorLabel.Frame.Right &&

                _dateLabel.Frame.Top == _commentLabel.Frame.Bottom + Globals.MarginGrid / 4 &&
                _dateLabel.Frame.Left == _creatorLabel.Frame.Left &&
                _dateLabel.Frame.Right == _creatorLabel.Frame.Right &&

                _replyCountIndicator.Frame.Top == _dateLabel.Frame.Bottom + Globals.MarginGrid &&
                _replyCountIndicator.Frame.Left == _dateLabel.Frame.Left &&
                _replyCountIndicator.Frame.Width == Globals.SmallToolbarItemSize &&
                _replyCountIndicator.Frame.Height == Globals.SmallToolbarItemSize &&
                _replyCountIndicator.Frame.Bottom == ContentView.Frame.Bottom - Globals.MarginGrid);   
        }
        else
        {
            ContentView.ConstrainLayout(() =>
                _creatorLabel.Frame.Top == ContentView.Frame.Top + Globals.MarginGrid &&
                _creatorLabel.Frame.Left == ContentView.Frame.Left + leftMargin &&
                _creatorLabel.Frame.Right == ContentView.Frame.Right - Globals.MarginGrid &&

                _commentLabel.Frame.Top == _creatorLabel.Frame.Bottom + Globals.MarginGrid / 4 &&
                _commentLabel.Frame.Left == _creatorLabel.Frame.Left &&
                _commentLabel.Frame.Right == _creatorLabel.Frame.Right &&

                _dateLabel.Frame.Top == _commentLabel.Frame.Bottom + Globals.MarginGrid / 4 &&
                _dateLabel.Frame.Left == _creatorLabel.Frame.Left &&
                _dateLabel.Frame.Right == _creatorLabel.Frame.Right &&
                _dateLabel.Frame.Bottom == ContentView.Frame.Bottom - Globals.MarginGrid);
        }

        _didUpdateConstraints = true;
    }

    public void UpdateCell(Comment comment)
    {
        // update the comment author
        _creatorLabel.Text = string.IsNullOrWhiteSpace(comment.CreatedByUser.FirstName) &&
            string.IsNullOrWhiteSpace(comment.CreatedByUser.LastName) ?
                comment.CreatedByUser.Email :
                $"{comment.CreatedByUser.FirstName} {comment.CreatedByUser.LastName}";

        // update the text
        var attr = new NSAttributedStringDocumentAttributes { DocumentType = NSDocumentType.HTML,  };
        var nsError = new NSError();

        var text = comment.Text.Insert(0, string.Format(CustomCommentCss,
            _font.FontDescriptor.Name, _font.PointSize,
            ColorConverter.ConvertToHex(_commentLabel.TextColor)));
        var mutableString = new NSMutableAttributedString(new NSAttributedString(
            text, attr, ref nsError));
        var mutableParagraph = new NSMutableParagraphStyle
        {
            Alignment = UITextAlignment.Left,
            LineBreakMode = UILineBreakMode.TailTruncation
        };
        mutableString.AddAttribute(UIStringAttributeKey.ParagraphStyle, mutableParagraph,
            new NSRange(0, mutableString.Length));
        mutableString.AddAttribute(UIStringAttributeKey.StrokeColor, Globals.ColorDark,
            new NSRange(0, mutableString.Length));
        _commentLabel.AttributedText = mutableString;

        // update the timestamp
        var localTime = TimeZone.CurrentTimeZone.ToLocalTime(
            comment.LastModifiedDateTime).ToString("g");
        _dateLabel.Text = comment.LastModifiedDateTime == comment.CreatedDateTime ? 
            localTime : $"Modified {localTime}";
    }

    public void DrawIndicator(Comment comment)
    {
        // if we've already drawn the indicator and 
        // the comment has no annotation associated with it
        _indicator?.RemoveFromSuperview();

        // if the comment havs an annotation associated with it,
        // draw the annotation indicator
        if (comment.Annotation != null)
        {
            _indicator = new AnnotationIndicator
            {
                Location = new CGPoint(Globals.MarginGrid, Globals.MarginGrid),
                Number = comment.Annotation.AnnotationNumber,
                FillColor = Color.FromHex(comment.Annotation.FillColorValue).ToUIColor(),
                TextColor = Color.FromHex(comment.Annotation.TextColorValue).ToUIColor()
            };
            AddSubview(_indicator);
        }

        // otherwise, draw the general comment indicator
        else
        {
            var size = comment.IsReply ? ReplyIndicator.DotSize : AnnotationIndicator.Size;
            _indicator = comment.IsReply ?
                new ReplyIndicator
                {
                    Frame = new CGRect(Globals.MarginGrid + size.Width / 2,
                        Globals.MarginGrid + size.Height / 2, ReplyIndicator.DotSize.Width,
                        ReplyIndicator.DotSize.Height)
                } as UIView :
                new UIImageView
                {
                    Image = UIImage.FromFile("general_annotation_indicator.png"),
                    Frame = new CGRect(Globals.MarginGrid, Globals.MarginGrid, size.Width, size.Height)
                };
            AddSubview(_indicator);
        }
    }

    public void DrawReplyCountIndicator(Comment comment)
    {
        if (_replyCountIndicator != null) { return; }

        _replyCountIndicator = new ReplyCountIndicatorView(comment.Replies.Count);
        ContentView.AddSubview(_replyCountIndicator);

        _didUpdateConstraints = false;
        UpdateConstraints();
    }
}

Here are some screenshots of the problem:

Top-level comments work

Replies, not so much

like image 352
kformeck Avatar asked Apr 27 '17 15:04

kformeck


2 Answers

you don't need use two table view you can try with one table by making top level comment as section and all replies as cells for that section.

like image 135
Pradeep Kashyap Avatar answered Oct 14 '22 09:10

Pradeep Kashyap


As Pradeep said, One table view with cells would be right option. But having said that there will be lot of conditions. So, your data structure should be able to handle it.

  1. Create Array of Dictionaries with an enum of type of cell (reply or comment) and populate the values accordingly.
  2. You can show/hide separator inside GetCell method.
  3. Even you can use indentation if you need in future to showcase reply like Facebook does.
like image 1
Saurav Avatar answered Oct 14 '22 09:10

Saurav