13

I want to draw text onto my subclass on UIView so that the text is cut out of the shape and the background behind the view shows through, just like in the OSX Mavericks logo found here.

I would say that I'm more of an intermediate/early advanced iOS developer so feel free to throw some crazy solutions at me. I'd expect I'd have to override drawRect in order to do this.

Thanks guys!

EDIT:

I should mention that my first attempt was making the text [UIColor clearColor] which didn't work since that just set the alpha component of the text to 0, which just showed the view through the text.

Community
  • 1
  • 1
barndog
  • 6,975
  • 8
  • 53
  • 105

4 Answers4

18

Disclaimer: I'm writing this without testing, so forgive me if I'm wrong here.

You should achieve what you need by these two steps:

  1. Create a CATextLayer with the size of your view, set the backgroundColor to be fully transparent and foregroundColor to be opaque (by [UIColor colorWithWhite:0 alpha:1] and [UIColor colorWithWhite:0 alpha:0]. Then set the string property to the string you want to render, font and fontSize etc.

  2. Set your view's layer's mask to this layer: myView.layer.mask = textLayer. You'll have to import QuartzCore to access the CALayer of your view.

Note that it's possible that I switched between the opaque and transparent color in the first step.

Edit: Indeed, Noah was right. To overcome this, I used CoreGraphics with the kCGBlendModeDestinationOut blend mode.

First, a sample view that shows that it indeed works:

@implementation TestView

- (id)initWithFrame:(CGRect)frame {
  if (self = [super initWithFrame:frame]) {
    self.backgroundColor = [UIColor clearColor];
  }
  return self;
}

- (void)drawRect:(CGRect)rect {
  [[UIColor redColor] setFill];
  UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:10];
  [path fill];

  CGContextRef context = UIGraphicsGetCurrentContext();
  CGContextSaveGState(context); {
    CGContextSetBlendMode(context, kCGBlendModeDestinationOut);
    [@"Hello!" drawAtPoint:CGPointZero withFont:[UIFont systemFontOfSize:24]];
  } CGContextRestoreGState(context);
}

@end

After adding this to your view controller, you'll see the view behind TestView where Hello! is drawn.

Why does this work:

The blend mode is defined as R = D*(1 - Sa), meaning we need opposite alpha values than in the mask layer I suggested earlier. Therefore, all you need to do is to draw with an opaque color and this will be subtracted from the stuff you've drawn on the view beforehand.

StatusReport
  • 3,397
  • 1
  • 23
  • 31
  • 1
    That’ll just result in masking the view with the text, not cutting the text out of it. Layer masks use the opacity of the mask layer. – Noah Witherspoon Jul 23 '13 at 18:29
  • If you invert the alpha of the layer, it should leave the view and cut the text, no? Then, instead of drawing on the view itself, draw on the mask layer. – StatusReport Jul 23 '13 at 19:00
  • “Invert the alpha of the layer” isn’t a thing. If you set a text layer to have a solid background color and a transparent text color, its alpha will be a filled rectangle. – Noah Witherspoon Jul 23 '13 at 21:22
  • Indeed. I was wrong there, sorry. A working solution is now posted. – StatusReport Jul 24 '13 at 13:57
  • Great work. I Googled this without expecting to find an answer. This should be marked correct. – Adam Waite Nov 20 '13 at 11:16
  • late comment here :/ - can you say what i would need to change in order to stroke the text and still make it 'punch through' ? if i add NSStroke to the attributes in 'drawInRect:withAttributes', the text is filled again :/ thanks – CodingMeSwiftly Jul 27 '14 at 16:44
  • i've found a way but its a bit of a hack: draw the string again with kCGBlendModeMultiply, this time suppling a NSStroke attribute :/ – CodingMeSwiftly Jul 27 '14 at 20:30
  • Thanks for this answer, very useful. – arcdale Feb 13 '19 at 13:59
3

If all you want is a white view with some stuff (text images, etc) cut out, then you can just do

yourView.layer.compositingFilter = "screenBlendMode"

This will leave the white parts white and the black parts will be see-through.

iVentis
  • 993
  • 6
  • 19
1

I actually figured out how to do it on my own surprisingly but @StatusReport's answer is completely valid and works as it stands now.

Here's how I did it:

-(void)drawRect:(CGRect)rect{
    CGContextRef context = UIGraphicsGetCurrentContext();
    [[UIColor darkGrayColor]setFill]; //this becomes the color of the alpha mask
    CGContextFillRect(context, rect);
    CGContextSaveState(context);
    [[UIColor whiteColor]setFill];
    //Have to flip the context since core graphics is done in cartesian coordinates
    CGContextTranslateCTM(context, 0, rect.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    [textToDraw drawInRect:rect withFont:[UIFont fontWithName:@"HelveticaNeue-Thin" size:40];
    CGContextRestoreGState(context);
    CGImageRef alphaMask = CGBitmapContextCreateImage(context);
    [[UIColor whiteColor]setFill];
    CGContextFillRect(context, rect);
    CGContextSaveGState(context);
    CGContentClipToMask(context, rect, alphaMask);
    [backgroundImage drawInRect:rect];
    CGContextRestoreGState(context);
    CGImageRelease(alphaMask);
}

- (void)setTextToDraw:(NSString*)text{
    if(text != textToDraw){
        textToDraw = text;
        [self setNeedsDisplay];
    }
}

I have an @synthesize declaration for textToDraw so I could override the setter and call [self setNeedsDisplay]. Not sure if that's totally necessary or not.

I'm sure this has some typos but I can assure you, the spell checked version runs just fine.

barndog
  • 6,975
  • 8
  • 53
  • 105
1

StatusReport's accepted answer is beautifully written and because I have yet to find a Swift answer to this question, I thought I'd use his answer as the template for the Swift version. I added a little extensibility to the view by allowing the input of the string as a parameter of the view to remind people that that his can be done with all of the view's properties (corner radius, color, etc.) to make the view completely extensible.

The frame is zeroed out in the initializer because you're most likely going to apply constraints. If you aren't using constraints, omit this.

Swift 5

class CustomView: UIView {
    var title: String
    
    init(title: String) {
        self.title = title
        super.init(frame: CGRect.zero)
        config()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func config() {
        backgroundColor = UIColor.clear
    }
    
    override func draw(_ rect: CGRect) { // do not call super
        UIColor.red.setFill()
        let path = UIBezierPath(roundedRect: bounds, cornerRadius: 10)
        path.fill()
        weak var context = UIGraphicsGetCurrentContext() // Apple wants this to be weak
        context?.saveGState()
        context?.setBlendMode(CGBlendMode.destinationOut)
        title.draw(at: CGPoint.zero, withAttributes: [NSAttributedString.Key.font : UIFont.systemFont(ofSize: 24)])
        context?.restoreGState()
    }
}

let customView = CustomView(title: "great success")
trndjc
  • 11,654
  • 3
  • 38
  • 51