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.