14

I'm trying to cut an transparent square in a UIImage, however I honestly have no idea where/how to start.

Any help would be greatly appreciated.

Thanks!

Bartłomiej Semańczyk
  • 59,234
  • 49
  • 233
  • 358
Phillip
  • 1,205
  • 3
  • 15
  • 22
  • 1
    Do you want to cut the hole in the _image_ or in the UIImageView that displays it? – matt May 04 '14 at 03:22
  • 1
    Look at how I punch a circular hole in a UIImageView (and its image) in this code: http://stackoverflow.com/a/8632731/341994 You would do exactly the same except you'd draw a square instead of a circle. – matt May 04 '14 at 03:25
  • @matt: Cutting a hole in the actual UIImageView would actually be perfect! Except, I don't know how to use that code you linked :p what's a CLayer and CGContextRef? Thanks!! – Phillip May 04 '14 at 04:10
  • I'm delighted that you want to learn what's really going on here and not just copy a bunch of code blindly. Here's my full explanation all about how to draw in iOS, including everything involved in that code except the actual "masking the layer" part: http://www.apeth.com/iOSBook/ch15.html Here's my discussion of layers (you need to know about this because punching a hole involves masking a view's layer): http://www.apeth.com/iOSBook/ch16.html (and see especially http://www.apeth.com/iOSBook/ch16.html#_shadows_borders_and_more) – matt May 04 '14 at 04:19
  • @matt: I'll read up on this, thank you so much! Tons of useful information – Phillip May 04 '14 at 04:29

5 Answers5

21

Presume that your image is being displayed in a view - probably a UIImageView. Then we can punch a rectangular hole in that view by masking the view's layer. Every view has a layer. We will apply to this view's layer a mask which is itself a layer containing an image, which we will generate in code. The image will be black except for a clear rectangle somewhere in the middle. That clear rectangle will cause the hole in the image view.

So, let self.iv be this UIImageView. Try running this code:

CGRect r = self.iv.bounds;
CGRect r2 = CGRectMake(20,20,40,40); // adjust this as desired!
UIGraphicsBeginImageContextWithOptions(r.size, NO, 0);
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextAddRect(c, r2);
CGContextAddRect(c, r);
CGContextEOClip(c);
CGContextSetFillColorWithColor(c, [UIColor blackColor].CGColor);
CGContextFillRect(c, r);
UIImage* maskim = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CALayer* mask = [CALayer layer];
mask.frame = r;
mask.contents = (id)maskim.CGImage;
self.iv.layer.mask = mask;

For example, in this image, the white square is not a superimposed square, it is a hole, showing the white of the window background behind it:

enter image description here

EDIT: I feel obligated, since I mentioned it in a comment, to show how to do the same thing with a CAShapeLayer. The result is exactly the same:

CGRect r = self.iv.bounds;
CGRect r2 = CGRectMake(20,20,40,40); // adjust this as desired!
CAShapeLayer* lay = [CAShapeLayer layer];
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, nil, r2);
CGPathAddRect(path, nil, r);
lay.path = path;
CGPathRelease(path);
lay.fillRule = kCAFillRuleEvenOdd;
self.iv.layer.mask = lay;
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • This worked perfectly, thanks a ton Matt! I'm still reading up on the stuff you linked, but its awesome to know I have working code to reference :D Thanks again man, you're awesome! :) – Phillip May 04 '14 at 04:29
  • 1
    The really cool part (one of many) is that this works on _any_ UIView. You can punch a hole in _anything_. Moreover, instead of punching a pure hole, you can punch a semi-transparent hole. Also, what we did here is not at all the only way; we could have used a CAShapeLayer as our mask, instead of making an image. – matt May 04 '14 at 04:36
  • That /is/ pretty cool!! I'll be sure to keep this in mind if I ever need to do something like this again :D – Phillip May 04 '14 at 04:40
  • 1
    Edited my answer to show how to do it with a shape layer. You might find that easier when getting started. Also (get this) a shape layer lets you animate the shape it displays, so I think you could probably move or otherwise alter the hole in an animated way. – matt May 04 '14 at 04:43
  • That's so cool! And yeah, that makes a bit more sense, however I still have quite a bit of learning to do before all of this 'clicks' :P thanks again man! :D – Phillip May 04 '14 at 04:50
14

Here's a simple Swift function cut#hole#inView to copy and paste for 2017

func cut(hole: CGRect, inView view: UIView) {
    let path: CGMutablePath = CGMutablePath()
    path.addRect(view.bounds)
    path.addRect(hole)

    let shapeLayer = CAShapeLayer()
    shapeLayer.path = path
    shapeLayer.fillRule = .evenOdd

    view.layer.mask = shapeLayer
}
Kiryl Bielašeŭski
  • 2,663
  • 2
  • 28
  • 40
Fattie
  • 27,874
  • 70
  • 431
  • 719
3

Just needed the Version from @Fattie, thanks again! Here is the updated Code for Swift 5.1:

private func cut(holeRect: CGRect, inView view: UIView) {
    let combinedPath = CGMutablePath()
    combinedPath.addRect(view.bounds)
    combinedPath.addRect(holeRect)

    let maskShape = CAShapeLayer()
    maskShape.path = combinedPath
    maskShape.fillRule = .evenOdd

    view.layer.mask = maskShape
}

If you want the cutout to have rounded corners you can replace combinedPath.addRect(holeRect) with rectanglePath.addRoundedRect(in: holeRect, cornerWidth: 8, cornerHeight: 8).

palme
  • 2,499
  • 2
  • 21
  • 38
0

Here's the updated code to cut a hole in an UIImage (instead of UIView) using Swift:

func cut(hole: CGRect, inView image: UIImage) -> UIImage? {
        UIGraphicsBeginImageContext(image.size)
        image.draw(at: CGPoint.zero)
        let context = UIGraphicsGetCurrentContext()!
        let bez = UIBezierPath(rect: hole)
        context.addPath(bez.cgPath)
        context.clip()
        context.clear(CGRect(x:0,y:0,width: image.size.width,height: image.size.height))
        let newImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        
        return newImage
    }
AnupamChugh
  • 1,779
  • 1
  • 25
  • 36
0

If you are working with a graphics context, the answer is this simple:

Use the "clear" command which exists for the purpose.

context.clear(someRect)

That's the whole thing. .clear

(For example, here's a red image with a square hole.)

let frame = CGRect(origin: .zero, size: sz)
let fmt = UIGraphicsImageRendererFormat()
fmt.scale = 1
let image = UIGraphicsImageRenderer(size: sz, format: fmt).image { rc in

    .red.setFill()
    rc.fill(frame)
    
    rc.cgContext.clear(CGRect(x: 10, y: 10, width: 10, height: 10))

    let att = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 10.0)]
    "test".draw(in: frame, withAttributes: att)
}

try! image.pngData()!.write(to: "red.png")
Fattie
  • 27,874
  • 70
  • 431
  • 719