I have the following set-up of CAShapeLayers
and UIViews
attached to a UIView
:
[self.layer addSublayer:self.shapeLayer1];
if (someCase)
{
[self.layer addSublayer:self.shapeLayerOptional];
}
[self.layer addSublayer:self.shapeLayer2];
[self addSubview: self.view1];
I want there to be functionality that allows the user to insert another UIView
, let's call it view0
, before shapeLayer2, but I don't know how to accomplish this. The parent UIView
subviews
property only includes view1
, while its layer.sublayers
property includes the layers of the shapelayers and subview1, but I don't want to insert subview0.layer
into layers.sublayers
because subview0
has a UITouchGestureRecognizer
. This seems like a simple design problem on the surface but it's really confusing me for some reason. Anyone have any recommended solutions? I want to try and avoid wrapping the CAShapeLayers
into UIViews
if I can.
Couple things to note...
Layers - whether CALayer
, CAShapeLayer
, CAGradientLayer
, etc - belong to a view. You can manipulate the appearance of the layers via the .zPosition
.
Also, a layer doesn't really exist on its own... it must be added as a sublayer to a view's layer.
A view has its own layer ... so when trying to manipulate layer ordering, that must be taken into account.
So, your goal is to have one view with multiple layers, another view with one or more layers, and insert the second view (and its layers) between layers of the first view.
If we start with two views -- yellow with two CAShapeLayer
sublayers; cyan with one CAShapeLayer
sublayer:
Looks like this (with the layers separated out):
and we want to insert cyanView (and its triangle sublayer) between the rectangle and oval layers of yellowView:
Let's add cyanView as a subview of yellowView, offset by 40,40 and yellowView.clipsToBounds = true
so we can see it's a subview:
That gives us what we expect... but not what we want, because the rect and oval layers belong to yellowView.
To move the oval layer to the top, we'll change the .zPosition
properties:
// we don't want to mess with the .zPosition of yellow view's layer,
// because it's the "base" of the hierarchy
rectShapeLayer.zPosition = 1;
cyanView.layer.zPosition = 2;
triShapeLayer.zPosition = 3;
ovalShapeLayer.zPosition = 4;
and we get this result:
cyanView and its triangle layer are now between yellow view's rectangle and oval layers.
Goal accomplished? Maybe...
Let's look at the Debug View Hierarchy now:
Notice that the layers are not separated from their views.
In the first Hierarchy images above, I used individual views just to show what we're trying to do.
Now, managing the .zPosition
values may work for you... but I would think it would be much easier to implement either only layers, or only views (we can change a view's layer class to eliminate the need to add a sublayer to every view).
Here is some example code to produce the above:
ViewsAndLayersViewController.h
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface ViewsAndLayersViewController : UIViewController
@end
NS_ASSUME_NONNULL_END
ViewsAndLayersViewController.m
#import "ViewsAndLayersViewController.h"
@implementation ViewsAndLayersViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = UIColor.systemBackgroundColor;
info = [UILabel new];
info.numberOfLines = 0;
info.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:info];
UILayoutGuide *g = self.view.safeAreaLayoutGuide;
[NSLayoutConstraint activateConstraints:@[
[info.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:20.0],
[info.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-20.0],
[info.topAnchor constraintEqualToAnchor:g.topAnchor constant:20.0],
]];
[self reset];
}
- (void)reset {
[cyanView removeFromSuperview];
[yellowView removeFromSuperview];
cyanView = nil;
yellowView = nil;
yellowView = [UIView new];
cyanView = [UIView new];
rectShapeLayer = [CAShapeLayer new];
ovalShapeLayer = [CAShapeLayer new];
triShapeLayer = [CAShapeLayer new];
yellowView.clipsToBounds = YES;
[self.view addSubview:yellowView];
[self.view addSubview:cyanView];
CGRect r = CGRectMake(40.0, 160.0, 240.0, 200.0);
yellowView.frame = r;
cyanView.frame = CGRectOffset(r, 0.0, r.size.height + 20.0);
yellowView.backgroundColor = UIColor.yellowColor;
cyanView.backgroundColor = UIColor.cyanColor;
for (CAShapeLayer *s in @[rectShapeLayer, ovalShapeLayer, triShapeLayer]) {
s.fillColor = UIColor.clearColor.CGColor;
s.lineWidth = 16;
s.lineJoin = kCALineJoinRound;
}
rectShapeLayer.strokeColor = UIColor.systemRedColor.CGColor;
ovalShapeLayer.strokeColor = UIColor.systemGreenColor.CGColor;
triShapeLayer.strokeColor = UIColor.systemBlueColor.CGColor;
[yellowView.layer addSublayer:rectShapeLayer];
[yellowView.layer addSublayer:ovalShapeLayer];
[cyanView.layer addSublayer:triShapeLayer];
UIBezierPath *pth;
CGRect pRect = CGRectInset(yellowView.bounds, 20.0, 20.0);
CGPoint p;
// rectangle path
pth = [UIBezierPath bezierPathWithRect:pRect];
rectShapeLayer.path = pth.CGPath;
// oval path
pth = [UIBezierPath bezierPathWithOvalInRect:pRect];
ovalShapeLayer.path = pth.CGPath;
// triangle path
pth = [UIBezierPath new];
p = CGPointMake(CGRectGetMidX(pRect), CGRectGetMinY(pRect));
[pth moveToPoint:p];
p = CGPointMake(CGRectGetMaxX(pRect), CGRectGetMaxY(pRect));
[pth addLineToPoint:p];
p = CGPointMake(CGRectGetMinX(pRect), CGRectGetMaxY(pRect));
[pth addLineToPoint:p];
[pth closePath];
triShapeLayer.path = pth.CGPath;
info.text = @"Tap to add cyanView as a subview of yellowView, offset by 40,40";
iStep = 0;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
++iStep;
switch (iStep) {
case 1:
[self step1];
break;
case 2:
[self step2];
break;
case 3:
[self step3];
break;
default:
break;
}
}
- (void)step1 {
cyanView.frame = CGRectOffset(yellowView.bounds, 40.0, 40.0);
[yellowView addSubview:cyanView];
info.text = @"Tap to arrange layers via .zPosition";
}
- (void)step2 {
// we don't want to mess with the .zPosition of yellow view's layer,
// because it's the "base" of the hierarchy
rectShapeLayer.zPosition = 1;
cyanView.layer.zPosition = 2;
triShapeLayer.zPosition = 3;
ovalShapeLayer.zPosition = 4;
info.text = @"Tap to reset...";
}
- (void)step3 {
[self reset];
}
@end