14
self.myPath=[UIBezierPath bezierPathWithArcCenter:center 
                                           radius:200 
                                       startAngle:0 
                                         endAngle:180 
                                        clockwise:YES];

(This much I was able to get up running with some web searching).

I have this path. Now I want to fill the reverse of this path, so leaving this portion and filling everything else. How can I finish the coding? I don't have much info on this.

The problem

enter image description here

The area it is showing after using Cemal Answer previously it only showed a circle with red stroke.

Edit

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = [UIColor whiteColor];
        self.punchedOutPath =  
         [UIBezierPath bezierPathWithOvalInRect:CGRectMake(50, 50, 400, 400)];
        self.fillColor = [UIColor redColor];
        self.alpha = 0.8;
    }
    return self;
}

- (void)drawRect:(CGRect)rect
{
    [[self fillColor] set];
    UIRectFill(rect);

    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGContextSetBlendMode(ctx, kCGBlendModeDestinationOut);

    [[self punchedOutPath] fill];

    CGContextSetBlendMode(ctx, kCGBlendModeNormal);
}

enter image description here

halfer
  • 19,824
  • 17
  • 99
  • 186
Shubhank
  • 21,721
  • 8
  • 65
  • 83
  • Will it be a full circle or a open circular shape? Also, can you provide a image of what you are expecting? It does't have to be pretty but it will greatly help people understand what you are trying to do so that they can help you. – David Rönnqvist Jun 28 '12 at 06:52
  • done..was working on providing it only since i made the bounty – Shubhank Jun 28 '12 at 06:53

4 Answers4

21

Use bezierPathByReversingPath. From the docs (iOS 6.0+ only):

Creates and returns a new bezier path object with the reversed contents of the current path.

so to reverse your path, you'd just:

UIBezierPath* aPath = [UIBezierPath bezierPathWithArcCenter:center
                                                     radius:200
                                                 startAngle:0
                                                   endAngle:180
                                                  clockwise:YES];
self.myPath = [aPath bezierPathByReversingPath];
Alex Cio
  • 6,014
  • 5
  • 44
  • 74
adam.wulf
  • 2,149
  • 20
  • 27
13

Here's an alternative that doesn't require reversing the path at all.

You have a portion of a view you essentially want to "clip out":

clipped out

Let's say you want the white area to be [UIColor whiteColor] with 75% alpha. Here's how you do it quickly:

  • You create a new UIView subclass.
  • This view has two properties:

    @property (retain) UIColor *fillColor;
    @property (retain) UIBezierPath *punchedOutPath;
    
  • You override its -drawRect: method to do this:

    - (void)drawRect:(CGRect)rect {
      [[self fillColor] set];
      UIRectFill(rect);
    
      CGContextRef ctx = UIGraphicsGetCurrentContext();
      CGContextSetBlendMode(ctx, kCGBlendModeDestinationOut);
    
      [[self punchedOutPath] fill];
    
      CGContextSetBlendMode(ctx, kCGBlendModeNormal);
    }
    

There's a caveat here: The fillColor of the view must not include the alpha component. So in your case, you'd want that to just be [UIColor whiteColor]. You then apply the alpha bit yourself by calling [myView setAlpha:0.75].

What's going on here: This is using a blend mode called "Destination Out". Mathematically it's defined as R = D*(1 - Sa), but in layman's terms it means "Destination image wherever destination image is opaque but source image is transparent, and transparent elsewhere."

So it's going to use the destination (i.e., what's already in the context) wherever the new stuff is transparent (i.e. outside of the bezier path), and then where the bezier path would be opaque, that stuff is going to become transparent. However, the destination stuff must already be opaque. If it's not opaque, the blending doesn't do what you want. This is why you have to provide an opaque UIColor and then do any transparency you want with the view directly.


I ran this myself, with these circumstances:

  • the window has a [UIColor greenColor] background
  • the fillColor is white
  • the punchedOutPath is a oval that's inset 10 points from the edges of the view.
  • the view has an alpha of 0.75

With the code above, I get this:

enter image description here

The interior is pure green, and the outside has the semi-transparent overlay.


Update

If your covering is an image, then you'll need to create a new image. But the principle is the same:

UIImage* ImageByPunchingPathOutOfImage(UIImage *image, UIBezierPath *path) {
  UIGraphicsBeginImageContextWithOptions([image size], YES, [image scale]);

  [image drawAtPoint:CGPointZero];

  CGContextRef ctx = UIGraphicsGetCurrentContext();
  CGContextSetBlendMode(ctx, kCGBlendModeDestinationOut);
  [path fill];


  UIImage *final = UIGraphicsGetImageFromCurrentImageContext();
  UIGraphicsEndImageContext();
  return final;
}

You would then take the result of this function and put it into a UIImageView.

Dave DeLong
  • 242,470
  • 58
  • 448
  • 498
  • will you be able to provide me with your project..?? Please..! it would be better since i would have a working code..otherwise i will have to clear up questions with you..and that might become annoying for you!. – Shubhank Jun 29 '12 at 07:07
  • @shubhank the code in the answer describes everything you need – Dave DeLong Jun 29 '12 at 13:13
  • hi..can you check my new edit to the ques..i tried your method..i set the background of my view to be white color..yet the oval is is black color..can you help me with it..it should have been white...thanks – Shubhank Jun 29 '12 at 14:49
  • @Shubhank this approach won't work with the `backgroundColor` of a `UIView`. The black you're seeing is the color of the view *behind* the punched-out view (likely the window). – Dave DeLong Jun 29 '12 at 16:24
  • i was going to add the image to the view and have the above functionality over it...if it is not going to work with `UIView` .. how will i be able to crop the image.. ? Kindly give a suggestion..you have helped a lot and it would be great if you can help me solve this..Thanks a lot – Shubhank Jun 29 '12 at 17:16
  • @Shubhank updated answer to show what you'd have to do to punch out part of an image. – Dave DeLong Jun 29 '12 at 17:38
  • I had the same problem of a black color as @Shubank but was able to get this code working over top by setting the `opaque` bool of the overlay UIView in interface builder to NO. I imagine it works programatically too. – bitwit Jan 19 '14 at 05:06
3

You can put this into a single screen app into the view controller: It will make a yellow background view and a blue layer on top of it that has an oval region cut out by a mask.

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    // create a yellow background
    UIView *bg = [[UIView alloc] initWithFrame:self.view.bounds];
    bg.backgroundColor = [UIColor yellowColor];
    [self.view addSubview:bg];    

    // create the mask that will be applied to the layer on top of the 
    // yellow background
    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    maskLayer.fillRule = kCAFillRuleEvenOdd;
    maskLayer.frame = self.view.frame;

    // create the paths that define the mask
    UIBezierPath *maskLayerPath = [UIBezierPath bezierPath];
    [maskLayerPath appendPath:[UIBezierPath bezierPathWithRect:CGRectInset(self.view.bounds, 20, 20)]];
    // here you can play around with paths :)
//    [maskLayerPath appendPath:[UIBezierPath bezierPathWithOvalInRect:(CGRect){{80, 80}, {140, 190}}]];
    [maskLayerPath appendPath:[UIBezierPath bezierPathWithOvalInRect:(CGRect){{100, 100}, {100, 150}}]];
    maskLayer.path = maskLayerPath.CGPath;

    // create the layer on top of the yellow background
    CALayer *imageLayer = [CALayer layer];
    imageLayer.frame = self.view.layer.bounds;
    imageLayer.backgroundColor = [[UIColor blueColor] CGColor];

    // apply the mask to the layer
    imageLayer.mask = maskLayer;
    [self.view.layer addSublayer:imageLayer];

}

this might answer this question as well: UIBezierPath Subtract Path

Community
  • 1
  • 1
user511
  • 154
  • 1
  • 7
  • Could you explain what you are doing and why? Just so the person who asked and other people looking for the answer understand what it is you answered. – Manuel Mar 07 '13 at 12:43
  • Strange, why wouldn't [UIBezierPath bezierPathWithOvalInRect:] draw an oval? Have you copied & pasted exactly that code into an empty single screen project? – user511 Apr 15 '13 at 10:00
2

I have two solution for you.

  1. Draw this path on a CALayer. And use that CALayer as a mask layer for you actual CALayer.

  2. Draw a rectangle with the sizes of you frame before adding arc.

    UIBezierPath *path = [UIBezierPath bezierPathWithRect:view.frame];
    [path addArcWithCenter:center 
                    radius:200 
                startAngle:0 
                  endAngle:2*M_PI 
                 clockwise:YES];
    

I would use second solution. :)

Alex Cio
  • 6,014
  • 5
  • 44
  • 74
Cemal Eker
  • 1,266
  • 1
  • 9
  • 24
  • 2
    The second solution is the better, except the arc needed to be added anticlockwise, so the area it delimits is excluded by [the nonzero winding number rule](https://developer.apple.com/library/ios/documentation/graphicsimaging/conceptual/drawingwithquartz2d/dq_paths/dq_paths.html#//apple_ref/doc/uid/TP30001066-CH211-TPXREF106) (the rectangle is added in a clockwise direction). – Nicolas Bachschmidt Jun 25 '12 at 14:51
  • this isn't working for me i am getting quite a very weird result..i have added it in the question..kindly see..thanks – Shubhank Jun 25 '12 at 15:21
  • 1
    The endAngle needs to be radians (not degrees). You want a 360° circle, so that's 2pi or 6.283. You can write this as `2*M_PI`. – Andreas Ley Jun 28 '12 at 12:36