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:
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With