21

I found this answer on how to draw rotated text with NSString drawInRect:, but I'm not sure how it works since it only sort of works for me: https://discussions.apple.com/thread/1779814?start=0&tstart=0

My code looks like:

           CGContextSaveGState(context);
           CGContextDrawLinearGradient(context, gradient, CGPointMake(0, centY - halfWidth), CGPointMake(0, centY + halfWidth), 0);

            // Add text                
            CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor); 
            NSString *str = @"some test string";

            CGAffineTransform transform1 = CGAffineTransformMakeRotation(M_PI/4);

            CGContextConcatCTM(context, transform1);
            CGContextTranslateCTM(context, 0, 0);
            UIFont *font = [UIFont systemFontOfSize:16.0];

            [str drawInRect:CGRectMake(0, 0, 200, 100) withFont:font lineBreakMode:UILineBreakModeWordWrap alignment:UIBaselineAdjustmentNone];

So when I use this, I see text being drawn 45 degrees below the x-axis. I want to draw the text vertically along my lineargradient. So I thought I could do that by using M_PI/2 for 90 degrees. I do not see my text though. I have tried different transforms for the rotation, and only some seem to work like M_PI/4 and M_PI/8. I would think that if I used -M_PI/4 it would have the text 45 degrees above the x-axis and M_PI/2 would be 90 degrees below the x-axis. But both turn up with nothing.

Any thoughts? thanks.

Kristian Glass
  • 37,325
  • 7
  • 45
  • 73
Crystal
  • 28,460
  • 62
  • 219
  • 393

7 Answers7

19

I solve this problem in next way.

1) Declare category on NSString

@interface NSString (NMSchemeItemDraw)
-(void)  drawWithBasePoint:(CGPoint)basePoint
                    andAngle:(CGFloat)angle
                     andFont:(UIFont*)font;
@end

This category will draw text with given central point, in one line and with given font and angle.

2) Implementation of this category is looks like:

@implementation NSString (NMSchemeItemDraw)

    -(void)  drawWithBasePoint:(CGPoint)basePoint
                     andAngle:(CGFloat)angle
                      andFont:(UIFont *)font{
    CGSize  textSize    =   [self   sizeWithFont:font];

    CGContextRef    context =   UIGraphicsGetCurrentContext();
    CGAffineTransform   t   =   CGAffineTransformMakeTranslation(basePoint.x, basePoint.y);
    CGAffineTransform   r   =   CGAffineTransformMakeRotation(angle);


    CGContextConcatCTM(context, t);
    CGContextConcatCTM(context, r);

    [self   drawAtPoint:CGPointMake(-1 * textSize.width / 2, -1 * textSize.height / 2)
               withFont:font];

    CGContextConcatCTM(context, CGAffineTransformInvert(r));
    CGContextConcatCTM(context, CGAffineTransformInvert(t));
    }
@end

3) Now i can use it in my [UIView drawRect:] method. For example, in a next way:

 -(void)drawRect:(CGRect)rect{
 NSString* example = @"Title";
 [example drawWithBasePoint:CGPointMake(0.0f, 0.0f)
                   andAngle:M_PI
                    andFont:[UIFont boldSystemFontOfSize:16.0]];
 }
sschale
  • 5,168
  • 3
  • 29
  • 36
KoNEW
  • 211
  • 2
  • 2
  • Nice solution... Much easier to pass in a few parameters and let the function handle everything else – jsetting32 Nov 25 '14 at 14:52
  • 1
    Very elegant! sizeWithFont and drawAtPoint:withFont are now deprecated however. Replacing your CGSize textSize... line of code with this code: NSDictionary *attributes = @{NSFontAttributeName: specifiedFont}; CGSize textSize = [self sizeWithAttributes:attributes]; works as advertised. The drawAtPoint method call needs to be updated as well. Simply replace 'withFont:font' with 'withAttributes:attributes'. Thanks again for this clean solution. – Scooter Dec 31 '16 at 02:51
13

I think what's happening is that you're rotating the text to a location outside of the view because it rotates the context by pivoting on the origin.

After your rotation you need to do a translation to move the context back into the view. In the example below, I rotate the context counterclockwise 90 degrees. Then I translate the tx of the context the distance of the height.

- (void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext(); 

    CGContextSaveGState(context);

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

    // Create the gradient 
    CGColorRef startColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0].CGColor; 
    CGColorRef endColor = [UIColor colorWithRed:200.0/255.0 green:200.0/255.0 blue:200.0/255.0 alpha:1.0].CGColor;
    NSArray *colors = [NSArray arrayWithObjects:(id)startColor, (id)endColor, nil];
    CGFloat locations[] = { 0.0, 1.0 };
    CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (CFArrayRef) colors, locations);
    CGContextDrawLinearGradient(context, gradient, CGPointMake(rect.origin.x + rect.size.width / 2, rect.origin.y), CGPointMake(rect.origin.x + rect.size.width / 2, rect.origin.y + rect.size.height), 0);

    // Create text              
    CGContextSetFillColorWithColor(context, [UIColor blackColor].CGColor); 
    NSString *string = @"some test string";
    UIFont *font = [UIFont systemFontOfSize:16.0];

    // Rotate the context 90 degrees (convert to radians)
    CGAffineTransform transform1 = CGAffineTransformMakeRotation(-M_PI_2);
    CGContextConcatCTM(context, transform1); 

    // Move the context back into the view 
    CGContextTranslateCTM(context, -rect.size.height, 0);

    // Draw the string 
    [string drawInRect:rect withFont:font lineBreakMode:UILineBreakModeWordWrap alignment:UITextAlignmentLeft];

    // Clean up 
    CGGradientRelease(gradient);
    CGColorSpaceRelease(colorSpace);

    CGContextRestoreGState(context);
}

Note that you can also get the size of how the string will be rendered, which can help in doing calculations for the alignment of the text.

    CGSize stringSize = [string sizeWithFont:font]; 
JZAU
  • 3,550
  • 31
  • 37
Chris Livdahl
  • 4,662
  • 2
  • 38
  • 33
  • Thanks a lot. but if I change the -M_PI_2 to M_PI_2. this string will disappear. WHY? – JZAU Jun 08 '15 at 04:11
  • 1
    I got it, if change -M_PI_2 to M_PI_2 , it's also need to change CGContextTranslateCTM(context, -rect.size.height, 0); to CGContextTranslateCTM(context, 0, -rect.size.height); – JZAU Jun 08 '15 at 05:46
13

Here'a an updated and simplified version of KoNEW's answer. It preserves the state of the graphics context as a bonus. This is also a simple function instead of a String extension.

func drawRotatedText(_ text: String, at p: CGPoint, angle: CGFloat, font: UIFont, color: UIColor) {
  // Draw text centered on the point, rotated by an angle in degrees moving clockwise.
  let attrs = [NSFontAttributeName: font, NSForegroundColorAttributeName: color]
  let textSize = text.size(attributes: attrs)
  let c = UIGraphicsGetCurrentContext()!
  c.saveGState()
  // Translate the origin to the drawing location and rotate the coordinate system.
  c.translateBy(x: p.x, y: p.y)
  c.rotate(by: angle * .pi / 180)
  // Draw the text centered at the point.
  text.draw(at: CGPoint(x: -textSize.width / 2, y: -textSize.height / 2), withAttributes: attrs)
  // Restore the original coordinate system.
  c.restoreGState()
}
Todd Fincannon
  • 131
  • 1
  • 4
6

KoNew's Solution in Swift:

extension NSString {

    func drawWithBasePoint(basePoint:CGPoint, angle:CGFloat, font:UIFont) {

        var attrs: NSDictionary = [
            NSFontAttributeName: font
        ]

        var textSize:CGSize = self.sizeWithAttributes(attrs as [NSObject : AnyObject])

        // sizeWithAttributes is only effective with single line NSString text 
        // use boundingRectWithSize for multi line text

        var context: CGContextRef =   UIGraphicsGetCurrentContext()

        var t:CGAffineTransform   =   CGAffineTransformMakeTranslation(basePoint.x, basePoint.y)
        var r:CGAffineTransform   =   CGAffineTransformMakeRotation(angle)


        CGContextConcatCTM(context, t)
        CGContextConcatCTM(context, r)


        self.drawAtPoint(CGPointMake(-1 * textSize.width / 2, -1 * textSize.height / 2), withAttributes: attrs as [NSObject : AnyObject])


        CGContextConcatCTM(context, CGAffineTransformInvert(r))
        CGContextConcatCTM(context, CGAffineTransformInvert(t))


    }


}

To use:

let fieldFont = UIFont(name: "Helvetica Neue", size: 14)

myNSString.drawWithBasePoint(CGPointMake(bounds.width/2, bounds.height/2), angle: CGFloat(-M_PI_2), font: fieldFont!)
DanielGibbs
  • 9,910
  • 11
  • 76
  • 121
John D.
  • 548
  • 7
  • 19
1

Thank for this post, and Chris's answer, and Arkadiusz's answer at another post.

Finally, I solve this problem by a UILabel subclass, named MyVerticalLabel, and make it draw vertical text.

@implementation MyVerticalLabel

// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
    // Drawing code

    CGContextRef context = UIGraphicsGetCurrentContext();

    CGAffineTransform transform = CGAffineTransformMakeRotation(-M_PI_2);
    CGContextConcatCTM(context, transform);
    CGContextTranslateCTM(context, -rect.size.height, 0);

    // The drawing rect should be applied with transform to.
    CGRect newRect = CGRectApplyAffineTransform(rect, transform);
    newRect.origin = CGPointZero;

    NSMutableParagraphStyle *textStyle = [[NSMutableParagraphStyle defaultParagraphStyle] mutableCopy];
    textStyle.lineBreakMode = self.lineBreakMode;
    textStyle.alignment = self.textAlignment;

    // Apply current label attributes for drawing function.
    NSDictionary *attributeDict =
    @{
      NSFontAttributeName : self.font,
      NSForegroundColorAttributeName : self.textColor,
      NSParagraphStyleAttributeName : textStyle,
      };
    [self.text drawInRect:newRect withAttributes:attributeDict];
}

@end
Community
  • 1
  • 1
AechoLiu
  • 17,522
  • 9
  • 100
  • 118
1

I found the answer by Todd above to be really helpful. However, I often wanted to position and rotate the text relative to one of the edges of the text (top, left, etc). So, I extended his answer. Also, used attributed text, so could independently deal with font, color, etc.

enum StringOrigin: Int { case
    upperLeft, upperCenter, upperRight,
    centerLeft, centerCenter, centerRight,
    lowerLeft, lowerCenter, lowerRight }

func drawRotatedAttrString(_ s: NSAttributedString, p: CGPoint, angle: CGFloat, origin: StringOrigin) {
    let width = s.size().width
    let height = s.size().height

    let c = UIGraphicsGetCurrentContext()!
    c.saveGState()
    // Translate the origin to the drawing location and rotate the coordinate system.
    c.translateBy(x: p.x, y: p.y)
    c.rotate(by: angle * .pi / 180)
    // Draw the text offset to handle "origin" choice
    switch origin {
    case .upperLeft:
        s.draw(at: .zero)
    case .upperCenter:
        s.draw(at: CGPoint(x: -0.5 * width, y: 0))
    case .upperRight:
        s.draw(at: CGPoint(x: -width, y: 0))
    case .centerLeft:
        s.draw(at: CGPoint(x: 0, y: -0.5 * height))
    case .centerCenter:
        s.draw(at: CGPoint(x: -0.5 * width, y: -0.5 * height))
    case .centerRight:
        s.draw(at: CGPoint(x: -width, y: -0.5 * height))
    case .lowerLeft:
        s.draw(at: CGPoint(x: 0, y: -height))
    case .lowerCenter:
        s.draw(at: CGPoint(x: -0.5 * width, y: -height))
    case .lowerRight:
        s.draw(at: CGPoint(x: -width, y: -height))
    }
    // Restore the original coordinate system.
    c.restoreGState()
}
anorskdev
  • 1,867
  • 1
  • 15
  • 18
0

Swift draw rotated text on UIImage as an example

usage:

self.someImageView.image = UIImage(named: "icon")?.addTopRightVerticalText("Hello World")
extension UIImage {
    func addTopRightVerticalText(_ text: String, fontSize: CGFloat = 23) -> UIImage {
        let screenWidth = UIScreen.main.bounds.size.width
        let imageWidth = self.size.width
        let k = imageWidth / screenWidth
        let fontSize: CGFloat = fontSize * k
        let font = UIFont(name: "Courier", size: fontSize) ?? UIFont.systemFont(ofSize: fontSize)
        let attributes = [
            NSAttributedString.Key.font: font,
            NSAttributedString.Key.foregroundColor: UIColor.orange,
        ] as [NSAttributedString.Key : Any]
        let textSize = text.size(withAttributes: attributes)

        let x = self.size.width - (textSize.height / 2)
        let y = textSize.width / 2
        let centerPoint = CGPointMake(x, y)

        return addText(text, attributes: attributes, textCenterPoint: centerPoint, angle: -90)
    }
    
    func addText(
        _ text: String,
        attributes: [NSAttributedString.Key : Any],
        textCenterPoint: CGPoint? = nil,
        angle: CGFloat = CGFloat(0)
    ) -> UIImage {
        let textSize = text.size(withAttributes: attributes)
        
        let centerPoint: CGPoint
        if let textCenterPoint = textCenterPoint {
            centerPoint = textCenterPoint
        } else {
            centerPoint = CGPoint(x: textSize.width / 2, y: textSize.height / 2)
        }
        
        let format = UIGraphicsImageRendererFormat()
        format.scale = self.scale //is 1 for common cases
        let renderer = UIGraphicsImageRenderer(size: self.size, format: format)
        let resultImage = renderer.image { rendererContext in

            let cgContext = rendererContext.cgContext
            cgContext.saveGState()
            
            //draw image
            self.draw(in: CGRect(origin: CGPoint.zero, size: self.size))
            
            //move anchor to point(center) and rotate
            cgContext.translateBy(x: centerPoint.x, y: centerPoint.y)
            cgContext.rotate(by: angle * .pi / 180)
            // Draw the text centered at the point.
            let textLeftTopPoint = CGPoint(x: -textSize.width / 2, y: -textSize.height / 2)
            
            //draw text
            //anchor + textLeftTopPoint
            text.draw(at: textLeftTopPoint, withAttributes: attributes)
            // Restore the original coordinate system.
            cgContext.restoreGState()
        }
        
        return resultImage
    }
}

It is possible to get next error when work with renderer.image. It is possible if you skip a scale or specify custom scale or use UIScreen.main.scale(which is 3 for retina). In this case Renderer for image(100x100 px) generates image (300x300 px). You can check is using image.cgImage!.width, image.cgImage!.height

EXC_RESOURCE (RESOURCE_TYPE_MEMORY: high watermark memory limit exceeded) (limit=2098 MB)

yoAlex5
  • 29,217
  • 8
  • 193
  • 205