Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Position additional text inside UITextView - WhatsApp timestamp copy

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.

like image 515
J. Doe Avatar asked May 12 '19 18:05

J. Doe


3 Answers

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.

like image 146
nitinkumarp Avatar answered Nov 16 '22 15:11

nitinkumarp


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.

like image 22
Sunil Sharma Avatar answered Nov 16 '22 16:11

Sunil Sharma


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

like image 1
Volodymyr Avatar answered Nov 16 '22 15:11

Volodymyr