Figured it out.
Seems like you have to use CoreText to pull it off though, not just TextKit.
I still have to figure out how to extend the highlights so they cover the bottoms of letters and not so much of the top. And I have to figure out how to move the highlights so they're "behind" the text and not making the font color lighter, but this will get you 90% of the way there.

import UIKit import CoreText import PlaygroundSupport // Sources // https://stackoverflow.com/questions/48482657/catextlayer-render-attributedstring-with-truncation-and-paragraph-style // https://stackoverflow.com/a/52320276/1291940 // https://stackoverflow.com/a/55283002/1291940 // Create a view to display what's going on. var demoView = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500)) demoView.backgroundColor = UIColor.white // Haven't figured out if you can create a boundary around a UIView PlaygroundPage.current.liveView = demoView // Apparently it doesn't matter where we place this code // Calculates height of frame given a string of a certain length extension String { func sizeOfString(constrainedToWidth width: Double, font: UIFont) -> CGSize { let attributes = [NSAttributedString.Key.font : font] let attString = NSAttributedString(string: self, attributes: attributes) let framesetter = CTFramesetterCreateWithAttributedString(attString) return CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRange(location: 0, length: 0), nil, CGSize(width: width, height: .greatestFiniteMagnitude), nil) } } // Unwraps optional so our program doesn't crash in case the user doesn't have the specified font. func unwrappedFont(fontSize: CGFloat) -> UIFont { if let textFont = UIFont(name: "Futura", size: fontSize) { return textFont } else { return UIFont.systemFont(ofSize: fontSize) } } let string = "When you hear or read someone weaving their ideas into a beautiful mosaic of words, try to remember, they are almost certainly wrong." var dynamicHeight = string.sizeOfString(constrainedToWidth: 500, font: unwrappedFont(fontSize: 40)).height // dynamicHeight = 500 let boxSize = CGSize(width: 500, height: dynamicHeight) // let boxSize = CGSize(width: 500, height: 500) var imageBounds : [CGRect] = [] // rectangle highlight let renderer = UIGraphicsImageRenderer(size: boxSize) let img = renderer.image { ctx in // Flipping the coordinate system ctx.cgContext.textMatrix = .identity ctx.cgContext.translateBy(x: 0, y: boxSize.height) // Alternatively y can just be 500. ctx.cgContext.scaleBy(x: 1.0, y: -1.0) // Setting up constraints for quote frame let range = NSRange( location: 0, length: string.count) guard let context = UIGraphicsGetCurrentContext() else { return } let path = CGMutablePath() let bounds = CGRect(x: 0, y: 0, width: boxSize.width, height: boxSize.height) path.addRect(bounds) let attrString = NSMutableAttributedString(string: string) attrString.addAttribute(NSAttributedString.Key.font, value: UIFont(name: "Futura", size: 40)!, range: range ) let framesetter = CTFramesetterCreateWithAttributedString(attrString as CFAttributedString) let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrString.length), path, nil) CTFrameDraw(frame, context) // Setting up variables for highlight creation let lines = CTFrameGetLines(frame) as NSArray var lineOriginsArray : [CGPoint] = [] var contextHighlightRect : CGRect = CGRect() var counter = 0 // Draws a rectangle over each line. for line in lines { let ctLine = line as! CTLine let numOfLines: size_t = CFArrayGetCount(lines) lineOriginsArray = [CGPoint](repeating: CGPoint.zero, count: numOfLines) CTFrameGetLineOrigins(frame, CFRangeMake(0,0), &lineOriginsArray) imageBounds.append(CTLineGetImageBounds(ctLine, context)) // Draw highlights contextHighlightRect = CGRect(x: lineOriginsArray[counter].x, y: lineOriginsArray[counter].y, width: imageBounds[counter].size.width, height: imageBounds[counter].size.height) ctx.cgContext.setStrokeColor(red: 0, green: 0, blue: 0, alpha: 0.5) ctx.cgContext.stroke(contextHighlightRect) ctx.cgContext.setFillColor(red: 1, green: 1, blue: 0, alpha: 0.3) ctx.cgContext.fill(contextHighlightRect) counter = counter + 1 } } // Image layer let imageLayer = CALayer() imageLayer.contents = img.cgImage imageLayer.position = CGPoint(x: 0, y: 0) imageLayer.frame = CGRect(x: 0, y: 0, width: 500, height: dynamicHeight) // Adding layers to view demoView.layer.addSublayer(imageLayer)