Don't display subviews outside of UIBezierPath - objective-c

I have one UIView which is drawn with:
- (void)drawRect:(CGRect)rect
{
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, self.widthOfLine, self.heightOfLine)];
[bezierPath fill];
}
and whose frame is centered within the iOS window. I want to place a bunch of smaller (5px by 5px) views within this view randomly, which I am doing fine with arc4random() function and drawing the views. However, I can't seem to figure out how to cut off all views outside the bezierPathWithOvalInRect, it will just display them everywhere in the first UIView's frame. Anyone know what to do for this?
edit:
For clarity, I have defined another view as:
- (void)drawRect:(CGRect)rect
{
UIBezierPath *drawPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(self.xPosition, self.yPosition, self.width, self.width)];
[[UIColor whiteColor] setFill];
[drawPath fill];
}
and I am adding a large number of these to the first view as subviews, but I want them to not be displayed outside the oval bezier path. Is there a way to a) only add them within the oval bezier path as opposed to within the entire frame, or b) a way to clip all that are outside the oval from the view?

In order to clip all subviews to a particular UIBezierPath, you'll want to set the mask property of the view's layer property. If you set the layer.mask to a CAShapeLayer of that path, then all subviews will be cropped to that path.
I use the following code to do just that in my app:
// create your view that'll hold all the subviews that
// need to be clipped to the path
CGRect anyRect = ...;
UIView* clippedView = [[UIView alloc] initWithFrame:anyRect];
clippedView.clipsToBounds = YES;
// now create the shape layer that we'll use to clip
CAShapeLayer* maskingLayer = [CAShapeLayer layer];
[maskingLayer setPath:bezierPath.CGPath];
maskingLayer.frame = backingImageHolder.bounds;
// set the mask property of the layer, and this will
// make sure that all subviews are only visible inside
// this path
clippedView.layer.mask = maskingLayer;
// now any subviews you add won't show outside of that path
UIView* anySubview = ...;
[clippedView addSubview:anySubview];

Related

Draw a common outline for the intersecting UIViews (ObjectiveC)?

I have a UIView. Inside this UIView, I have two intersecting subviews. I want to draw a common outline between these two sub-UIViews. How do I get about this?
To make it clear:
[mainView addSubview:subView1];
[mainView addSubview:subView2];
I need to draw an outline for both these intersecting UIViews.
You can combine shapes of both rectangles and create new sublayer from the final shape. This layer will act as your outline.
See my code, its fully commented:
// Create shape of merged rectangles
UIBezierPath *outlinePath = [UIBezierPath bezierPathWithRect:subView1.frame];
[outlinePath appendPath:[UIBezierPath bezierPathWithRect:subView2.frame]];
// Configure the outline
UIColor *outlineColor = [UIColor redColor];
CGFloat outlineWidth = 1.0f * [[UIScreen mainScreen] scale];
// Create shape layer representing the outline shape
CAShapeLayer *outlineLayer = [CAShapeLayer layer];
outlineLayer.frame = mainView.bounds;
outlineLayer.fillColor = outlineColor.CGColor;
outlineLayer.strokeColor = outlineColor.CGColor;
outlineLayer.lineWidth = outlineWidth;
// Set the path to the layer
outlineLayer.path = outlinePath.CGPath;
// Add sublayer at index 0 - below everything already in the view
// so it looks like the border
// "outlineLayer" is in fact the shape of the combined rectangles
// outset by layer border
// Moving it at the bottom of layer hierarchy imitates the border
[mainView.layer insertSublayer:outlineLayer atIndex:0];
Output looks like this:
Edit: I misunderstood the question at first. Below is my original answer which outlines rects intersection.
You can just create another view which will be representing the intersection and add it above the two subviews. It's frame would be intersection of the subView1 and subView2 frames. Just give it clear background color and border using its layer property
Your code could look like this:
// Calculate intersection of 2 views
CGRect intersectionRect = CGRectIntersection(subView1.frame, subView2.frame);
// Create intersection view
UIView *intersectionView = [[UIView alloc] initWithFrame:intersectionRect];
[mainView addSubview:intersectionView];
// Configure intersection view (no background color, only border)
intersectionView.backgroundColor = [UIColor clearColor];
intersectionView.layer.borderColor = [UIColor redColor].CGColor;
intersectionView.layer.borderWidth = 1.0f;

Set size of UIView based on content size

Is it possible to draw something on a UIView, then, after the drawing is completed, resize the view to be the same size as the thing that was previously drawn?
For example, if I draw a circle on a UIView, I would like to crop the UIView to the dimensions of the circle that I just drew on the view.
UPDATE
I am looking into using a CAShapeLayer as a possible solution. Does anyone know how to convert a UIBezierPath to a CAShapeLayer then set the position of the CAShapeLayer?
I have tried:
shapeLayer.path = bezierPath.CGPath;
shapeLayer.position = CGPointMake(0, 0);
but this does not work.
Yes you can do that. Have a look at this example that will answer both your questions.
First of all you need to add a UIView called myView and attach it to an IBOutlet ivar.
Define this global values for demonstration purposes:
#define POSITION CGPointMake(50.0,50.0)
#define SIZE CGSizeMake(100.0,100.0)
Declare two methods, one that will draw a shape in myView and another one that will resize the view to adapt it to the drawn shape:
-(void) circle
{
CAShapeLayer *layerData = [CAShapeLayer layer];
layerData.fillColor = [UIColor greenColor].CGColor;
UIBezierPath * dataPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0.0, 0.0, SIZE.width, SIZE.height)];
layerData.path = dataPath.CGPath;
layerData.position = CGPointMake(POSITION.x, POSITION.y);
[myView.layer addSublayer:layerData];
}
-(void) resize
{
((CAShapeLayer *)[myView.layer.sublayers objectAtIndex:0]).position = CGPointMake(0.0, 0.0);
myView.frame = CGRectMake(POSITION.x + myView.frame.origin.x , POSITION.y + myView.frame.origin.y, SIZE.width, SIZE.height);
}
Finally, in viewWillAppear: call both methods:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self circle];
[self resize];
}
You can run the same code calling only circle and calling both methods. In both cases the drawn circle will be at the same exact position but in the second case myView has been resized to have the same size as the drawn shape.
Hope it helps.

move uiimage inside a bezier curve

I have drawn a circle using bezier curve, I am using this circle as a mask to a uiimage view. Now, how can i move the image inside the circle without moving the circle using touches.
here is my code.
CAShapeLayer *maskLayer = [CAShapeLayer layer];
aPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(220, 220)
radius:170
startAngle:0
endAngle:DEGREES_TO_RADIANS(360)
clockwise:YES];
maskLayer.backgroundColor = [[UIColor clearColor] CGColor];
maskLayer.path = [aPath CGPath];
maskLayer.masksToBounds=YES;
imageView1.layer.mask = maskLayer;
[self.view addSubview:imageView1];
The coordinates of the mask is the same as the coordinates of your views layer = it moves along with the view.
You can add the image inside another view and mask that view instead. Then you can move the image inside the other view and the mask will stay the same.
Alternatively (but really the same solution) you could keep the mask on that layer and add a sublayer with the image and move that instead.

NSView drawRect interfering with subviews?

I have an nsview and i use draw rect to draw an image for background. It also has 3 subviews nsbuttons. The problem is, whenever the mouse is down on a button, the other buttons disappear. But when I remove the draw rect method, this doesn't happen. So I am guessing this has to do with the draw rect method for drawing images.
How can I avoid this?
Thanks.
EDIT:
Ok, i figured out where the problem is. Basically, I have an NSMenuItem, and I am putting a view inside it with 3 buttons. But in NSMenu, at the top, there's a padding of 4 pixels. So, basically, to remove that padding I used the solution provided here:
Gap above NSMenuItem custom view
From the solution there's a line in the drawRect method:
[[NSBezierPath bezierPathWithRect:fullBounds] setClip];
The moment, i remove this line, and the button behave properly. But then, the padding on top doesn't go away.
Here's my drawRect:
- (void) drawRect:(NSRect)dirtyRect {
[[NSGraphicsContext currentContext] saveGraphicsState];
NSRect fullBounds = [self bounds];
fullBounds.size.height += 4;
[[NSBezierPath bezierPathWithRect:fullBounds] setClip];
NSImage *background = [NSImage imageNamed:#"bg.png"];
[background drawInRect:fullBounds fromRect:NSZeroRect operation:NSCompositeCopy fraction:100.0];
[[NSGraphicsContext currentContext] restoreGraphicsState];
}
The solution to the linked question doesn't include saving and restoring the graphics state, which is a good idea when you are modifying one that you didn't create. Give this a try:
- (void)drawRect:(NSRect)dirtyRect {
// Save the current clip rect that has been set up for you
[NSGraphicsContext saveGraphicsState];
// Calculate your fullBounds rect
// ...
// Set the clip rect
// ...
// Do your drawing
// ...
// Restore the correct clip rect
[NSGraphicsContext restoreGraphicsState]
Are you sure that those buttons are actually subviews, and not just placed over the view you're drawing?

Subclassing NSScrollView drawRect: Method

I'm customizing the UI for one of my apps, and the idea is that a text area is initially bordered gray when out of focus, and when it comes into focus, the border becomes bright white. My app uses a dark theme, and for a single-lined NSTextField, this works great.
I'm running into problems with a subclassed NSTextView, however. In order to alter the border properly, I ended up having to actually subclass the parent NSScrollView, but am still seeing strange behavior. (See screenshot below.) I want the red box to fill the entire scroll view, as this would allow me to stroke (instead of filling, which is just for testing) the path, producing a nice border. Instead, the red box seems to be only filling to the internal child view.
The following code snippet, which is for the NSScrollView subclass:
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
NSRect borderRect = self.bounds;
borderRect.origin.y += 1;
borderRect.size.width -= 1;
borderRect.size.height -= 4;
BOOL inFocus = ([[self window] firstResponder] == self);
if (!inFocus) {
inFocus = [self anySubviewHasFocus:self];
}
if (inFocus) {
[[NSColor colorWithDeviceRed:.8 green:.2 blue:0 alpha:1] set];
} else {
[[NSColor colorWithDeviceRed:.1 green:.8 blue:0 alpha:1] set];
}
[NSGraphicsContext saveGraphicsState];
[[NSGraphicsContext currentContext] setShouldAntialias:NO];
[NSBezierPath fillRect:borderRect];
[NSGraphicsContext restoreGraphicsState];
NSLog(#"My bounds: %#", NSStringFromRect(borderRect));
NSLog(#"Super (%#) bounds: %#", [self superview], NSStringFromRect(borderRect));
}
Produces the screenshot as seen below. Also, see the output in the log, which suggests that the entire view should be filled. This is the only output that is ever shown, regardless of the size of the text inside. Entering carriage returns increases the height of the red box, but does not produce different output. (And I would like the red box to fill the entire bounds.)
2011-04-08 21:30:29.789 MyApp[6515:903] My bounds: {{0, 1}, {196, 87}}
2011-04-08 21:30:29.789 MyApp[6515:903] Super (<EditTaskView: 0x3a0b150>) bounds: {{0, 1}, {196, 87}}
Edit: Thanks to Josh Caswell for his answer. See below for the proper behavior when not focused, and when focused.
As ughoavgfhw noted, NSScrollView doesn't usually do any drawing, and probably has a weird interaction with its child views in that way. I'd suggest putting something like the following in your text view's drawing code to draw this custom focus ring that you want*:
// We're going to be modifying the state for this,
// so allow it to be restored later
[NSGraphicsContext saveGraphicsState];
// Choose the correct color; isFirstResponder is a custom
// ivar set in becomeFirstResponder and resignFirstResponder
if( isFirstResponder && [[self window] isKeyWindow]){
[myFocusedColor set];
}
else {
[myNotFocusedColor set];
}
// Create two rects, one slightly outset from the bounds,
// one slightly inset
NSRect bounds = [self bounds];
NSRect innerRect = NSInsetRect(bounds, 2, 2);
NSRect outerRect = NSMakeRect(bounds.origin.x - 2,
bounds.origin.y - 2,
bounds.size.width + 4,
bounds.size.height + 4);
// Create a bezier path using those two rects; this will
// become the clipping path of the context
NSBezierPath * clipPath = [NSBezierPath bezierPathWithRect:outerRect];
[clipPath appendBezierPath:[NSBezierPath bezierPathWithRect:innerRect]];
// Change the current clipping path of the context to
// the enclosed area of clipPath; "enclosed" defined by
// winding rule. Drawing will be restricted to this area.
// N.B. that the winding rule makes the order that the
// rects were added to the path important.
[clipPath setWindingRule:NSEvenOddWindingRule];
[clipPath setClip];
// Fill the rect; drawing is clipped and the inner rect
// is not drawn in
[[NSBezierPath bezierPathWithRect:outerRect] fill];
[NSGraphicsContext restoreGraphicsState];
This should be a reasonable approximation of what AppKit does when it draws a focus ring. Of course, AppKit is sort of allowed to draw outside a view's bounds -- I can't guarantee that this is completely safe, but you seem to get a margin of 3 px to play with. You could draw the ring entirely inside the bounds if you wanted. A true focus ring extends slightly (2 px) inside the view anyways (as I've done here).
Apple docs on Setting the Clipping Region.
EDIT: After re-reading your comments on the question, I realize I may have long-winded-ly buried the real answer. Try either subclassing NSClipView and switching your scroll view's clip view for that, or using a custom view for the document view.
*: You could also put this in the drawing code of a custom view subclass which is set as the document view of the NSScrollView; then your text view could be a subview of that. Or substitute a custom NSClipView subclass.