Drawing inside a subview of a UIView subclass - ios

I have more of a generic question. I have a custom class that subclasses UIView. I override '-drawRect' to draw points in a line which I successfully did. Now I declare another UIView object inside my class that I want to animate. It should be a white circle outline which I'd like to move with an animation and this is why I want it to be of UIView type. But how can I draw inside it? What if I have 3 other similar cases for example? So my point is, I want to keep all of the functionality inside the component's class and have custom drawing for one or more UIView subviews. I think I am missing something on conceptional level.
In ActionScript 3 I would do it so:
var first:MovieClip = new MovieClip();
first.graphics...
......
var nTh:MovieClip = new MovieClip();
nTh.graphics...
This is what I want to achieve (as I said I've already made the drawing of the dots inside -drawRect, now I want to make the bigger circle which has to be animated later):

What I did is creating an UIView instance and setting its bounds and position in -initWithFrame:
//circleFrameSideSize will serve as the diameter of the circle
UIView *circle = [[UIView alloc] initWithFrame:CGRectMake(0, circleYPos, circleFrameSideSize, circleFrameSideSize)];
circle.layer.borderColor = [UIColor whiteColor].CGColor;
circle.layer.borderWidth = 1;
//here we do the trick for making the circle with using layer's corner radius
circle.layer.cornerRadius = circleFrameSideSize/2;
[self addSubview:circle];

Related

iOS Custom UIView Drawing - CAShapeLayers or drawRect?

I'm new to iOS UIView drawing and I'm really trying to implement a custom UIView class in a standard way.
The UIView class that I'm working on now is simple: it has mostly static background shapes that form a composite shape. I also want to add animation to this UIView class so that a simple shape can animate its path on top of the static shapes.
How I'm currently implementing this
Right now, I'm implementing the static drawing in the drawRect: method of the UIView subclass. This works fine. I also have not implemented animation of the single shape yet.
What I'm looking to have answered
Question 1:
Is it better to have the static shapes drawn in the drawRect: method as I'm currently doing it, or is there a benefit to refactoring all of the shapes being drawn into class-extension-scoped CAShapeLayer properties and doing something like:
-(instancetype) initWithFrame:(CGRect) frame
{
if (self = [super initWithFrame:frame])
{
[self setupView];
}
return self;
}
-(void) setupView // Allocate, init, and setup one-time layer properties like fill colorse.
{
self.shapeLayer1 = [CAShapeLayer layer];
[self.layer addSubLayer:shapeLayer1];
self.shapeLayer2 = [CAShapeLayer layer];
[self.layer addSubLayer:shapeLayer2];
// ... and so on
}
-(void) layoutSubviews // Set frames and paths of shape layers here.
{
self.shapeLayer1.frame = self.bounds;
self.shapeLayer2.frame = self.bounds;
self.shapeLayer1.path = [UIBezierPath somePath].CGPath;
self.shapeLayer2.path = [UIBezierPath somePath].CGPath;
}
Question 2:
Regardless of whether I implement the static shapes as CAShapeLayers or in drawRect:, is the best way to implement an animatable shape for this UIView a CAShapeLayer property that is implemented as the other CAShapeLayers would be implemented above? Creating a whole separate UIView class just for one animated shape and adding it as a subview to this view seems silly, so I'm thinking that a CAShapeLayer is the way to go to accomplish that.
I would appreciate any help with this very much!
You could do it either way, but using shape layers for everything would probably be cleaner and faster. Shape layers have built-in animation support using Core Animation.
It's typically faster to avoid implementing drawRect and instead let the system draw for you.
Edit:
I would actually word this more strongly than I did in 2014. The underlying drawing engine in iOS is optimized for tiled rendering using layers. You will get better performance and smoother animation by using CALayers and CAAnimation (or higher level UIView or SwiftUI animation which is built on CAAnimation.)

Custom border for UIView using CALayer and autolayout

I have to draw a custom border for a UIView.
I already have code to do that but it was written before we started using autolayout.
The code basically adds a sublayer of width/height=1.0f
-(void)BordeIzquierdo:(UIColor *)pcColor cfloatTamanio:(CGFloat )pcfloatTamanio
{
CALayer* clElemento = [self layer];
CALayer *clDerecho = [CALayer layer];
clDerecho.borderColor = [UIColor darkGrayColor].CGColor;
clDerecho.borderWidth = pcfloatTamanio;
clDerecho.frame = CGRectMake(0, 1, pcfloatTamanio, self.frame.size.height);
[clDerecho setBorderColor:pcColor.CGColor];
[clElemento addSublayer:clDerecho];
}
Now the problem is that with autolayout, this happens before layoutSubViews. So UIView.layer.frame is (0,0;0,0) and so the sublayer added is not shown because it has width/height=0.0f
Now how can I make this code without transferring this custom drawing to another method executed after viewDidLoad such as didLayoutSubviews.
I would like this styling to be correctly applied before viewDidLoad is called.
Is there anything like autolayout constraints that I can use with CALayers?
Any other ideas?
There is no CALayer autoresizing or constraints in iOS. (There is on Mac OS X, but no in iOS.)
Your requirements here are unreasonable. You say that you refuse to move the call to BordeIzquierdo:. But you can't give clDerecho a correct size until you know the size of self (and therefore self.layer). Half the art of Cocoa touch programming is putting your code in the right place. You must find the right place to put the call to BordeIzquierdo: so that the values you need have meaning at the moment it is called.
What I would do is put it in layoutSubviews along with a BOOL flag so that you only do it once.

iOS: How to catch a tap event on a marker on a map that has a CALayer on top of it?

I have a mapView (RouteMe mapView) on which there are annotations (markers).
On the mapView there is a touchesEnded function on which I usually catch all events.
Some markers have an added layer on top of them.
That layer has some animation images and as much as I know this is the only way I can show these animation images on top of the markers.
The problem is that I don't know how I can intercept a touch on a marker that has that layer on top of it. When I test the hit on the touchesEnded I recognize a CALayer class rather than a RMMArker class (obv0iously, because the layer is on top of the marker, therefore is first to intercept the event).
How can I reach the Marker once the top CALayer is tapped?
Thanks
Hackish workaround: Create an RMMapLayer instead of a CALayer. Remember to set the annotation on the sublayer to get things like callouts to work, e.g. in your RMMapLayer subclass:
RMMapLayer *sublayer = [[RMMapLayer alloc] init];
sublayer.annotation = ann;
sublayer.contents = (id)img2.CGImage;
sublayer.contentsScale = img2.scale;
sublayer.position = CGPointMake(CGRectGetWidth(self.bounds)/2, CGRectGetHeight(self.bounds)/2);
sublayer.bounds = CGRectMake(0, 0, img2.size.width, img2.size.height);
[self addSublayer:sublayer];
I don't know how many other things this has potential for breaking though, so you can follow this issue for any updates:
https://github.com/mapbox/mapbox-ios-sdk/issues/190
You can instruct the touches to passthrough. Take a look at this question: How can I click a button behind a transparent UIView?
Since the CALayer was on top of a CALAyer, all it took was to access it ancestor blockingLayer.superlayer.
By checking on the ancestor, I could have all that I needed.
This solved the problem.

UIView that is shaped like a UIBezierPath?

Is it possible on iOS to make a regular UIView in some CGRect and add subviews to it and then tell that container UIView something like this:
containerView.layer.path = someClosedUIBezierPath
?
And will then all of the subviews also be curved according to it's parent container view?
I know that every UIView has it's own CALayer and that would be the starting point for me.
I saw examples with animations but I don't see nothing like above (maybe because it isn't there :))
This is a little late, but maybe it will help someone:
You can clip a view to a bezier path by using a CAShapeLayer and the view's layer's mask property:
CAShapeLayer shapeMask = [[CAShapeLayer alloc] initWithFrame:containerView.bounds];
shapeMask.path = someClosedUIBezierPath.CGPath;
containerView.layer.mask = shapeMask;
[shapeMask release];

How to cut out or disable a section of a UIView?

I have a custom subclass of UIView, LineDrawingView. I have this as a #property in my main view controller and I add it to the view. This works fine - it creates a transparent view on top of the current view and uses UIBezierPath, touchesBegan, touchesMoved etc so you can draw all over the view by dragging your finger around.
I need to make that drawing view "L" shaped, so I can have an area in the bottom left corner where various controls are located. I can think of no way to make the drawing view "L" shaped, except maybe to add two separate rectangular drawing views, but this doesn't work because touches are interrupted when you drag your finger from one rectangle into the other. The other solution I've come up with is to add another view on top of the drawing view. This view should prevent drawing and I could also locate my controls within it so they are still usable while drawing is enabled.
I tried creating a UIView and adding it as a subview of the drawing view. I gave it a tint so I could check it was present and in the right place. I expected this to prevent drawing within the area of the new UIView, but drawing continues all over the area of the LineDrawingView. I also tried inserting the new UIView at index 2, with the LineDrawingView inserted at index 1. It still didn't affect the drawing.
self.drawView = [[LineDrawingView alloc] initWithFrame:CGRectMake(0, 50, 768, 905)];
[self.drawView setBackgroundColor:[UIColor clearColor]];
// not effective in preventing drawing!!
UIView *controlView = [[UIView alloc] initWithFrame:CGRectMake(0, 530, 310, 575)];
[controlView setUserInteractionEnabled:NO];
[controlView setBackgroundColor:[UIColor grayColor]];
[controlView setAlpha:0.3];
[self.view addSubview:drawView];
[self.drawView addSubview:controlView];
I would love to know: how can I either...
Create an "L" shaped drawing view?
OR
Cut out a section of the drawing view so users can interact with what is behind it?
OR
Impose an area on top of the drawing view where I can disable drawing and add my controls?
Here is a tutorial on creating a transparent rounded rectangle UIView, which I think you could modify in a straightforward manner to make it L shaped.
The most important thing to keep in mind that you'll have to implement your own drawRect and I believe also your own hitTest or touches (e.g. event handling like touchedBegan:withEvent:) methods.
I contacted Apple about this. It turns out there is not a way to create an "L" shaped view. The solution was to, in the drawing view, test whether touches are within the forbidden area and, if so, prevent the line from being drawn. My controls have been moved to a separate UIView which is moved to the front when drawing is enabled. This means the controls remain active the whole time and drawing cannot take place within the area of the controls.
if you want to draw an L shape drawing view you can do this by concatenating two lines only .. just like i have created an arrow .. and if you want to stop drawing while you are moving (dragging ) any object .. you just need to apply a check in your touches moved method (like is moved , )..
here is the code to draw an arrow (you can use this as a reference to draw any kind of shape) : How can I draw an arrow using Core Graphics?
code to check if you have hit the path (means your touch point is in the bezier path )
if([[self tapTargetForPath:((UIBezierPath *)[testDict objectForKey:#"path"])] containsPoint:startPoint])// if starting touch is in bezierpath
{
ishitInPath = YES;
isMoving = YES;
}
// to easily detect (or select a bezier path object)
- (UIBezierPath *)tapTargetForPath:(UIBezierPath *)path
{
if (path == nil) {
return nil;
}
CGPathRef tapTargetPath = CGPathCreateCopyByStrokingPath(path.CGPath, NULL, fmaxf(35.0f, path.lineWidth), path.lineCapStyle, path.lineJoinStyle, path.miterLimit);
if (tapTargetPath == NULL) {
return nil;
}
UIBezierPath *tapTarget = [UIBezierPath bezierPathWithCGPath:tapTargetPath];
CGPathRelease(tapTargetPath);
return tapTarget;
}

Resources