Search code examples
swiftuikitnsattributedstringuigraphicscontext

How to draw text in trapezoid (or circle)?


Let's imagine that I have some text: "Some attributed text in trapezoid."

I have NSAttributedString extension, which returns me UIImage with attributed text:

extension NSAttributedString {
    func asImage() -> UIImage? {
        defer {
            UIGraphicsEndImageContext()
        }
        let size = boundingRect(with: CGSize.zero, options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil).size
        UIGraphicsBeginImageContext(size)
        draw(at: CGPoint.zero)
        return UIGraphicsGetImageFromCurrentImageContext()
    }
}

But this function returns me text in one line, because of using boundingRect:

------------------------------------
|Some attributed text in trapezoid.|
------------------------------------

If I would use custom rect for drawing text it won't help much...

UIGraphicsBeginImageContext(CGRect(x: 0, y: 0, width: 100, height: 30))
draw(at: CGPoint.zero)

...because of text will be in rectangle:

--------------
|Some attribu|
|ted text in |
|trapezoid.  |
--------------

What i need, is to draw text in a trapezoid with known corner positions (or in a circle with known radius). So each new line of text should start with a little offset, see example: enter image description here

So I want to see something like that:

---------------
\Some attribut/
 \ed text in /
  \trapezoid/
   ---------

How can I achieve this result?


Solution

  • You'll have to drop down to CoreText levels here. The good new is, you will be able to draw text in just about any shape you wish!

    extension NSAttributedString {
        public func draw(in path: CGPath) {
            let context = UIGraphicsGetCurrentContext()!
    
            let transform = CGAffineTransform(scaleX: +1, y: -1)
    
            let flippedPath = CGMutablePath()
            flippedPath.addPath(path, transform: transform)
    
            let range = CFRange(location: 0, length: 0)
            let framesetter = CTFramesetterCreateWithAttributedString(self)
            let frame = CTFramesetterCreateFrame(framesetter, range, flippedPath, nil)
    
            context.saveGState()
    
            // Debug: fill path.
            context.setFillColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.5)
            context.beginPath()
            context.addPath(path)
            context.fillPath()
    
            context.concatenate(transform)
    
            CTFrameDraw(frame, context)
    
            context.restoreGState()
        }
    }
    

    And you can use it like so:

    let string = NSAttributedString(string: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
    
    let bounds = CGRect(x: 0, y: 0, width: 120, height: 120)
    
    let path = CGMutablePath()
    path.move(to: CGPoint(x: 10, y: 10))
    path.addLine(to: CGPoint(x: 110, y: 10))
    path.addLine(to: CGPoint(x: 90, y: 110))
    path.addLine(to: CGPoint(x: 30, y: 110))
    path.closeSubpath()
    
    UIGraphicsBeginImageContextWithOptions(bounds.integral.size, true, 0)
    defer { UIGraphicsEndImageContext() }
    
    let context = UIGraphicsGetCurrentContext()!
    context.setFillColor(UIColor.white.cgColor)
    context.fill(.infinite)
    
    string.draw(in: path)
    
    let image = UIGraphicsGetImageFromCurrentImageContext()!
    

    The bad news is, this solution does not give you an ellipsis at the end. If you really want to have that, you may need to make some adjustments to the last line the framesetter gives you.