The beauty of Cocoa is the sheer number of ways things can be drawn on the screen - each with their pros, cons and unique set of circumstances for employing each technique.

Challenge

Present rotated text on the screen.

Analysis

One approach would be to create a new view and start drawing using some combination of: -(void)drawRect:, CoreGraphics, CoreText, and maybe CoreAnimation if you need or expect some flash. For this simple case, I would like the text to remain selectable and also have the text easily placed in Interface Builder, even if it does not show the transformation. So I am going to manipulate the trusty control: NSTextField.

For reference this is the final result. As you can see the text has been rotated anticlockwise and is nicely centred:

Process

Let's start simple. I've created a new window and slapped an NSTextField with the selectable behaviour inside a container NSView.

There is only a small handful of APIs on NSView for performing transformations, but these aren't neat enough to do what we need to achieve. I'm looking towards CoreAnimation and layer-backing to do the heavy lifting.

In order to make the text field 'layer-backed' wantsLayer = YES must be specified on the view itself or a superview. For this case it is vital to do this on a superview (for example the window's contentView) otherwise the text will get clipped if it rotates outside its initial bounds:1

containerView.wantsLayer = YES;

CGFloat angle = radians(10.0);  
CGAffineTransform transform = CGAffineTransformMakeRotation(angle);  
textField.layer.affineTransform = transform;  

The result of this is unsurprisingly the same result as if we used -setFrameCenterRotation:. The text has rotated at its origin and not the centre as might have been expected.1

To fix this we are going to define the anchor point manually:

// The anchor point is defined as a percentage of the width and height. 0.5, 0.5 is therefore the center.
CGPoint anchorPoint = CGPointMake(0.5, 0.5);  

Applying this straight away will move the text even more out of position, specifically it'll position the text perfectly centred on the original anchor point {0, 0}. This is because a layer-backed view's frame is a function of the anchorPoint and position properties of its layer. So now we need to calculate the position to set so that when the anchorPoint is set, the view will be perfectly centred:

+ (CGPoint)positionWithAnchorPoint:(CGPoint)anchorPoint forView:(NSView *)view
{
    CGPoint newPoint = CGPointMake(
        NSWidth(view.bounds) * anchorPoint.x,
        NSHeight(view.bounds) * anchorPoint.y
    );
    CGPoint oldPoint = CGPointMake(
        NSWidth(view.bounds) * view.layer.anchorPoint.x,
        NSHeight(view.bounds) * view.layer.anchorPoint.y
    );

    newPoint = CGPointApplyAffineTransform(
        newPoint, view.layer.affineTransform
    );
    oldPoint = CGPointApplyAffineTransform(
        oldPoint, view.layer.affineTransform
    );

    CGPoint position = view.layer.position;

    position.x -= oldPoint.x;
    position.x += newPoint.x;

    position.y -= oldPoint.y;
    position.y += newPoint.y;

    return position;
}

This method is a staple of my helper geometry classes. First we set the position and then set the anchorPoint, the order is important:

CGPoint anchorPoint = CGPointMake(0.5, 0.5);  
textField.layer.position =  
    [Geometry positionWithAnchorPoint:anchorPoint forView:textField];
textField.layer.anchorPoint = anchorPoint;  

If you run the code now, the textfield should still be sat perfectly in the centre. Now we perform the translation:

CGFloat angle = radians(10.0);  
CGAffineTransform transform = CGAffineTransformMakeRotation(angle);  
textField.layer.affineTransform = transform;  

And the textfield is now nicely rotated in the centre:

Are we there yet?

Sadly not, go ahead and set the text programatically after doing your hard work. Before or after, this will happen:

This is because by setting the text, we also unintentionally update the layers back to their default. So we need to get some code into the NSTextView's -updateLayer method. Let's make a RotatableTextField class:

// RotatableTextField.h

#import <Cocoa/Cocoa.h>

@interface RotatableTextField : NSTextField

@property (nonatomic, assign) CGFloat rotationDegrees;
@property (nonatomic, assign) CGPoint anchorPoint;

@end
// RotatableTextField.m

#import "RotatableTextField.h"

#import "Geometry"

@implementation RotatableTextField

- (void)updateLayer
{
    self.layer.position = 
        [Geometry positionWithAnchorPoint:self.anchorPoint forView:self];
    self.layer.anchorPoint = self.anchorPoint;

    CGFloat angle = radians(self.rotationDegrees);
    CGAffineTransform transform = CGAffineTransformMakeRotation(angle);
    self.layer.affineTransform = transform;
}

@end

Set the custom class of the textfield to the RotatableTextField class in Interface Builder and in the File Owner's class file all we need to do now to achieve the rotation is:

containerView.wantsLayer = YES;  
textField.anchorPoint = CGPointMake(0.5, 0.5);  
textField.rotationDegrees = 10.0;  
textField.stringValue = @"Test Case, Please Ignore.";  

Voila, a center aligned, rotated, selectable textfield:

Conclusion

So here is a classic example of where something seemingly simple can quickly turn into a bigger job than first meets the eye, but with some helper methods stashed away in helper classes (geometry for calculating radians and position) and a tidy general purpose subclass (RotatableTextField), we can make the implementation look as simple as the problem.

  1. Before the advent of autolayout, -setFrameCenterRotation: did an excellent job of rotating text at its center. Newcomers most likely reach for this 10.5 api in combination with the newer 10.8 autolayout api and rather than figure out why the view has appeared to have been rotated about the origin, simply adjust view's constraints until it's back where it was. It's tough keeping IB files looking tidy as it is without having strangely placed views that are in completely different positions at runtime.

  2. Making a view layer-backed will cause its subviews to also be layer-backed and draw on its backing layer.