Do you need help on a specific subject? Use the contact form (Request a blog entry) on the right hand side.

2016-07-20

Drawing a vertical String | A Swift example using NSAffineTransform

I found the String drawing options out-of-the box a little unsatisfying. I don't need much, but even "not much" necessitates much more in-depth knowledge than I would have hoped for.

Case in point: Drawing a string vertically.

Drawing a String in Cocoa is not that difficult, though it is necessary to map the String onto a NSString:

        (str as NSString).drawAtPoint(NSPoint(x: x, y: y), withAttributes: nil)

Specifying nil for the attributes means that the default attributes will be used. Which is sufficient in most cases.

The NSPoint uses double values for x and y, where (0, 0) is the lower left corner of the view. (Unless "flipped" is overridden to return 'true', then (0, 0) is the upper left corner. "Flipped" also affects the operation of NSAffineTransform, thus the rest of this blog assumes a non-flipped view)

To rotate the string we need to specify a transformation. That is where NSAffineTransform comes in.
For the non-flipped view, a positive rotation is counter-clockwise.

I.e. to achieve this:




we have to specify a rotation of 90 degrees.

Since I have to do this more than once, I wrote a little function for this:

class MyView: NSView {

    static let rotateVertically: NSAffineTransform = {
        let trans = NSAffineTransform()
        trans.rotateByDegrees(90)
        return trans
    }()
    
    private func drawVerticalString(str: String, x: CGFloat, y: CGFloat) {
        let context = NSGraphicsContext.currentContext()!.CGContext
        CGContextSaveGState(context)
        let strRect = (str as NSString).boundingRectWithSize(NSSize(width: frame.height, height: frame.height), options: NSStringDrawingOptions.TruncatesLastVisibleLine, attributes: nil, context: nil)
        let nx = y
        let ny = -x - (strRect.size.height / 2)
        MyView.rotateVertically.concat()
        (str as NSString).drawAtPoint(NSPoint(x: nx, y: ny), withAttributes: nil)
        CGContextRestoreGState(context)

    }

I created a static transformation for this, as I will need that again and again. I do not know how much overhead creating a transformation incurs, so that may be unnecessary.

There are three things to keep in mind for the above implementation:

1) The transformation changes the graphics context permanently (at least for the duration of the "drawRect" call). Hence the operation needs to "push" and "pop" the graphic context so as not to affect other drawing operations that might come after.

2) We must apply a reverse transformation on the coordinates before calling the drawing operation itself. That way the drawing ends up in the proper place. It is of course also possible to do that transformation by using the translate operation of the NSAffineTransform object. However that would mean that the transformation would need to be different for each invocation which kind-of messes with the static definition. But your milage may differ.

3) The "- (strRect.size.height / 2)" was added to let the string rotate around the middle of the "height" (which becomes the "width" afterwards). This is because I calculate the mid-point for the string, not its corner point. Again, your milage may differ.

PS: In Swift 3 the context operations will become much more readable... the principles should remain the same though.

Happy coding...

Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH

We don't get the world we wish for... we get the world we pay for.

No comments:

Post a Comment