6

See the following image of Whatsapp, and especially the text that is inside the red circle:

enter image description here

On the image there is Lorum text and a timestamp of the timestamp of the message. I am wondering how Whatsapp has managed to do this.

I think Whatsapp is using a UITextView, but I am not sure about that. I am wondering how I can make such kind of cell. I tried adding the timestamp with the attributedText property, but I am having a lot of trouble calculating the right sizes for that. Maybe there is an easy solution.

Note: No xibs/storyboards, just code.

Note 2: as seen in the image, the text inside the UITextView is wrapped around the timestamp. This is the behavior I want to replicate.

Community
  • 1
  • 1
J. Doe
  • 12,159
  • 9
  • 60
  • 114
  • 2
    Don't think it's a `UITextView` may be this is a simple `UILabel` with `attributedText` property and right check is a special character – Shehata Gamal May 12 '19 at 19:00
  • Expanding on Khan's comment, I think you could achieve this by using tab stops https://stackoverflow.com/questions/31945333/nsattributedstring-with-tabs – EmilioPelaez May 12 '19 at 19:01
  • @Sh_Khan Hmm yeah maybe, but they are using a lot of build-in UITextView properies (like tappable links, phone-numbers etc.). But one way or another, maybe there is a generic solution for both label and textview – J. Doe May 12 '19 at 19:02
  • @EmilioPelaez Yes I came across that one also, but they are using fixed tabs. The aligment should always be right, regardless the tab size – J. Doe May 12 '19 at 19:03
  • You could also use the `tailIndent` property of the paragraph style class, I think. – EmilioPelaez May 12 '19 at 19:03
  • 2
    Can't it be another label, and using exclusion path to avoid text above it? – Larme May 12 '19 at 21:31
  • @Larme I tried it. Solution wasn't really CPU friendly though, and I had trouble laying it out efficient. I put a bounty on this question atm. – J. Doe May 14 '19 at 19:19
  • For clarity, can you provide a sample image of the expected behaviour if the last line of text were longer, and collided with the timestamp? Would you expect the timestamp to bump down to a new line? What is the expected left padding of the timestamp? It's highly likely that the designer who provided that image did not consider the complexity they were implying. What is specified in that image is _non-trivial_ to implement. Apart from "This is what I was told to build" can you articulate what the customer benefit of such a design is? – cleverbit May 20 '19 at 21:16

5 Answers5

5

To achieve the layout of WhatsApp chat row i.e. Message + Time, I had use following strategy. Using 2 Label. 1st for Message and 2nd Date. For 1st I had constrainted view to top, bottom, left and right. For 2nd I had constraint it to right and bottom of View.

Now to avoid overlapping of 1st and 2nd I had appended "blank spaces" at the end of chat message. For example, if the message was "Hi" I made it to "Hi ". This ensure my Time and ticks do not overlap.

nitinkumarp
  • 2,120
  • 1
  • 21
  • 30
  • Mixed up with Labels and UITextView as I am from Android background. I had used 2 Labels not UITextView. Edited my answer. – nitinkumarp May 16 '19 at 08:02
  • Interesting solution, maybe this is the most CPU friendly way (comparing to exclusion paths). – J. Doe May 16 '19 at 15:03
  • Chosen this answer because... it just works. No hard calculations, cpu friendly and very easy! – J. Doe May 21 '19 at 05:48
  • Have you encountered the problem as described here: https://stackoverflow.com/questions/56298892/extra-whitespace-that-causes-a-newline-by-uitextview-at-end-is-ignored? – J. Doe May 24 '19 at 20:05
  • @J.Doe I'm also running into the issue where some lines are truncated. Even the special separator `‏‏‎ ` (chosen answer) does not work. Have you found anything that works? – Niklas Dec 13 '21 at 12:42
3

I have tried various options and found calculating manually serve your requirement very well instead of using exclusions path or NSAttributedString.

Here is a screenshot that I was able to make WhatsApp like chat view without using any Xib/storyboard enter image description here

and here is code and wherever needed added comments:

func createChatView() {

    let msgViewMaxWidth = UIScreen.main.bounds.width * 0.7 // 70% of screen width

    let message = "Filler text is text that shares some characteristics of a real written text, but is random or otherwise generated. It may be used to display a sample of fonts, generate text for testing, or to spoof an e-mail spam filter."

    // Main container view
    let messageView = UIView(frame: CGRect(x: UIScreen.main.bounds.width * 0.1, y: 150, width: msgViewMaxWidth, height: 0))
    messageView.backgroundColor = UIColor(red: 0.803, green: 0.99, blue: 0.780, alpha: 1)
    messageView.clipsToBounds = true
    messageView.layer.cornerRadius = 5

    let readStatusImg = UIImageView()
    readStatusImg.image = UIImage(named: "double-tick-indicator.png")
    readStatusImg.frame.size = CGSize(width: 12, height: 12)

    let timeLabel = UILabel(frame: CGRect(x: 0, y: 0, width: messageView.bounds.width, height: 0))
    timeLabel.font = UIFont.systemFont(ofSize: 10)
    timeLabel.text = "12:12 AM"
    timeLabel.sizeToFit()
    timeLabel.textColor = .gray

    let textView = UITextView(frame: CGRect(x: 0, y: 0, width: messageView.bounds.width, height: 0))
    textView.isEditable = false
    textView.isScrollEnabled = false
    textView.showsVerticalScrollIndicator =  false
    textView.showsHorizontalScrollIndicator = false
    textView.backgroundColor = .clear
    textView.text = message

    // Wrap time label and status image in single view
    // Here stackview can be used if ios 9 below are not support by your app.
    let rightBottomView = UIView()
    let rightBottomViewHeight: CGFloat = 16
    // Here 7 pts is used to keep distance between timestamp and status image
    // and 5 pts is used for trail space.
    rightBottomView.frame.size = CGSize(width: readStatusImg.frame.width + 7 + timeLabel.frame.width + 5, height: rightBottomViewHeight)
    rightBottomView.addSubview(timeLabel)
    readStatusImg.frame.origin = CGPoint(x: timeLabel.frame.width + 7, y: 0)
    rightBottomView.addSubview(readStatusImg)

    // Fix right and bottom margin
    rightBottomView.autoresizingMask = [.flexibleLeftMargin, .flexibleTopMargin]

    messageView.addSubview(textView)
    messageView.addSubview(rightBottomView)

    // Update textview height
    textView.sizeToFit()
    // Update message view size with textview size
    messageView.frame.size = textView.frame.size

    // Keep at right bottom in parent view
    rightBottomView.frame.origin = CGPoint(x: messageView.bounds.width - rightBottomView.bounds.width, y: messageView.bounds.height - rightBottomView.bounds.height)

    // Get glyph index in textview, make sure there is atleast one character present in message
    let lastGlyphIndex = textView.layoutManager.glyphIndexForCharacter(at: message.count - 1)
    // Get CGRect for last character
    let lastLineFragmentRect = textView.layoutManager.lineFragmentUsedRect(forGlyphAt: lastGlyphIndex, effectiveRange: nil)

    // Check whether enough space is avaiable to show in last line of message, if not add extra height for timestamp
    if lastLineFragmentRect.maxX > (textView.frame.width - rightBottomView.frame.width) {
        // Subtracting 5 to reduce little top spacing for timestamp
        messageView.frame.size.height += (rightBottomViewHeight - 5)
    }
    self.view.addSubview(messageView)
}

I hope this will solve your problem.

Challenges I faced during creating chat view with other methods are:

  • UITextView exclusionPaths - It does not work in all conditions, I have also noticed problems like sometimes it gives more extra space around exclusion path then needed. It also fails when text exactly occupies UITextView space, in this case timestamp should go to new line but this does not happen.

  • NSAttributedString - Actually, I was not able to make this chat view with NSAttributedString but while trying I found that it made me write a lot of code plus it was very hard to manage/update.

Sunil Sharma
  • 2,653
  • 1
  • 25
  • 36
1

I've created some dummy test project which I suppose answers on your question.

enter image description here

I put textView as a text placeholder and added some imageView. Nothing special with constraints. You can see a picture.

Then all magic happens in a tableView delegate method: cellForRowAt indexPath: where I exclude imageView frame using UIBezierPath from textView. So in your case you have to set rect for UIBezierPath as a right bottom corner.

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let contentOfCell = data[indexPath.row]
    guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as? CustomCell else {
        return UITableViewCell()
    }
    cell.descriptionTextView.text = contentOfCell
    let textContainerPath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: cell.CellImageView.frame.width, height: cell.CellImageView.frame.height))
    cell.descriptionTextView.textContainer.exclusionPaths = [textContainerPath]
    return cell
}

And a final result looks like that:

enter image description here

Volodymyr
  • 1,192
  • 21
  • 42
0

You can make a .xib file and put text in textView, put time in label, and for checkboxes use a ImageView. Something like this:

enter image description here

fphilipe
  • 9,739
  • 1
  • 40
  • 52
  • Thanks for your answer. It doesn't actually answer the question. The timestamp is under the text, as clearly seen in the image I provided, the timestamp is sometimes next to the text, and the text is wrapped around the timestamp if there is lots of texts. – J. Doe May 15 '19 at 07:06
  • In your example, the time label just below the text. Here you have to play around with the constraints and everything will turn out. – Dmitriy Stepanov May 15 '19 at 07:55
  • No, that's just not true. The timestamp is on the same line as the last (or only) line in the message text view/label. It will not work with your example. The timestamp of the message is always under the text. – J. Doe May 15 '19 at 10:28
0

The main issue for that task is to tell the TextView to avoid typing text above the timestamp.

You can easily do it by using textView.textContainer.exclusionPaths:

CGFloat width = [UIScreen mainScreen].bounds.size.width - self.textViewLeading.constant - self.textViewTrailing.constant;
UIBezierPath * exclusionPath = [UIBezierPath bezierPathWithRect:CGRectMake(width - self.timestampContainerWidth.constant, 0, self.timestampContainerWidth.constant, self.timestampContainerHeight.constant)];
self.textView.textContainer.exclusionPaths = @[exclusionPath];

And that's it. Of course, you should add timestamp container view at the bottom right corner.

Docs: https://developer.apple.com/documentation/uikit/nstextcontainer/1444569-exclusionpaths

Renatus
  • 1,123
  • 13
  • 20