Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Chat Bubble - a TextKit Story

Chat Bubble - a TextKit Story

a story on the Textkit implementation when creating a Chat Bubble for an iOS messaging application

Bobby Prabowo

November 08, 2017
Tweet

More Decks by Bobby Prabowo

Other Decks in Programming

Transcript

  1. Challenge • Capability for custom “Emoji” :D => • Timestamp

    position based on Message Last Line Width • Handle Tap on Certain Word / URL
  2. Glyph • Line based on glyph • Glyph for index

    • Size for Glyph in range • Glyph for Touch Point
  3. Textkit Answer • Fast scrolling, mean must perform calculate Message

    Height without creating the View Use NSLayoutManager Rect Calculation • Capability for custom “Emoji” Use NSTextAttachment • URL Link Handling Use NSLayoutManager Character Index for Point • Timestamp Layout based on Last Line Width Use NSLayoutManager Glyph Calculation
  4. Get Frame Size func frameSize(maxWidth: CGFloat, font: UIFont) -> CGRect

    { let textStorage = NSTextStorage(attributedString: self) let textContainer = NSTextContainer(size: CGSize(width: maxWidth, height: CGFloat.greatestFiniteMagnitude)) let layoutManager = NSLayoutManager() layoutManager.addTextContainer(textContainer) textStorage.addLayoutManager(layoutManager) textStorage.addAttribute(.font, value: font, range: NSMakeRange(0, textStorage.length)) textContainer.lineFragmentPadding = 0.0 layoutManager.glyphRange(for: textContainer) let size = layoutManager.usedRect(for: textContainer) return CGRect(x: size.origin.x, y: size.origin.y, width: ceil(size.width), height: ceil(size.height)) }
  5. Custom Emoji func emojiTransform(emojiCode: String, emoticonImage: UIImage) -> NSAttributedString {

    let firstEmojiCode = emojiCode[emojiCode.index(emojiCode.startIndex, offsetBy: 0)] let secondEmojiCode = emojiCode[emojiCode.index(emojiCode.startIndex, offsetBy: 1)] let stringWithEmoji = NSMutableAttributedString() let stringLength = self.characters.count; let emojiAttachment = NSTextAttachment() emojiAttachment.image = emoticonImage emojiAttachment.bounds = CGRect(x: 0, y: -4, width: emoticonImage.size.width, height: emoticonImage.size.height) var index = 1 var buffer : String = "" while (index < stringLength) { let prevChar = self[self.index(self.startIndex, offsetBy: index - 1)] let currentChar = self[self.index(self.startIndex, offsetBy: index)] if (prevChar == firstEmojiCode && currentChar == secondEmojiCode) { if (buffer.characters.count > 0) { let characterAttributed = NSAttributedString(string: buffer) stringWithEmoji.insert(characterAttributed, at: stringWithEmoji.length) buffer = "" } let emojiAttributedString = NSAttributedString(attachment: emojiAttachment) stringWithEmoji.insert(emojiAttributedString, at: stringWithEmoji.length) index += 1 } else { buffer.append(prevChar) } if (index == stringLength - 1) { let tailChar = self[self.index(self.startIndex, offsetBy: index)] buffer.append(tailChar) let characterAttributed = NSAttributedString(string: buffer) stringWithEmoji.insert(characterAttributed, at: stringWithEmoji.length) } index += 1 } return stringWithEmoji; }
  6. Click on certain Word @objc func handleGesture(sender: UITapGestureRecognizer) { let

    cell = sender.view as! ChatCell let messageLabel = cell.messageWithTimestampView.messageLabel let stringToCheck = messageLabel.attributedText?.string let clickRange = stringToCheck?.range(of: "Click here") if (clickRange == nil) { return } let startPos = stringToCheck?.distance(from: (stringToCheck?.startIndex)!, to: (clickRange?.lowerBound)!).advanced(by: 0) let endPos = stringToCheck?.distance(from: (stringToCheck?.startIndex)!, to: (clickRange?.upperBound)!).advanced(by: 0) let point = sender.location(in: messageLabel) let textStorage = NSTextStorage(attributedString: messageLabel.attributedText!) let textContainer = NSTextContainer(size: CGSize(width: messageLabel.bounds.width, height: messageLabel.bounds.height)) let layoutManager = NSLayoutManager() layoutManager.addTextContainer(textContainer) textStorage.addLayoutManager(layoutManager) textStorage.addAttribute(.font, value: messageLabel.font, range: NSMakeRange(0, textStorage.length)) textContainer.lineFragmentPadding = 0.0 let characterIndex = layoutManager.characterIndex(for: point, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil) if ((characterIndex >= startPos!) && (characterIndex <= endPos!)) { print(characterIndex) print(messageLabel.attributedText?.string[clickRange!]) } }
  7. Get Last Line Width func lastLineFrameSize(maxWidth: CGFloat, font: UIFont) ->

    CGRect { let textStorage = NSTextStorage(attributedString: self) let textContainer = NSTextContainer(size: CGSize(width:maxWidth, height: CGFloat.greatestFiniteMagnitude)) let layoutManager = NSLayoutManager() layoutManager.addTextContainer(textContainer) textStorage.addLayoutManager(layoutManager) textStorage.addAttribute(.font, value: font, range: NSMakeRange(0, textStorage.length)) textContainer.lineFragmentPadding = 0.0 var index = 0 let numberOfGlyphs : Int = layoutManager.numberOfGlyphs var lineRange = NSMakeRange(NSNotFound, 0) while index < numberOfGlyphs { layoutManager.lineFragmentRect(forGlyphAt: index, effectiveRange: &lineRange) index = NSMaxRange(lineRange); } let size = layoutManager.boundingRect(forGlyphRange: lineRange, in: textContainer) return CGRect(x: size.origin.x, y: size.origin.y, width: ceil(size.width), height: ceil(size.height)) }
  8. Exclusion Path let steveImagePath = UIBezierPath(rect: steveImageView.frame) let iPodImagePath =

    UIBezierPath(rect: iPodImageView.frame) textView.textContainer.exclusionPaths = [steveImagePath, iPodImagePath]
  9. TextKit Note • For custom drawing, use UITextView because it

    has integrated TextContainer inside • Always ceil any value from a calculated frame method • Be careful with AutoLayout, the Frame calculation will be wrong for a few first time • For Automatic row height calculation, choose estimatedRowSize smaller and as close with the XIB view height