See the following image of Whatsapp, and especially the text that is inside the red circle:
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.
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.
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
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.
I've created some dummy test project which I suppose answers on your question.
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:
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