Search code examples
iosobjective-cuiviewcalayercashapelayer

Inserting subview into view where I've added subviews and sublayers


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.


Solution

  • 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:

    enter image description here

    Looks like this (with the layers separated out):

    enter image description here

    and we want to insert cyanView (and its triangle sublayer) between the rectangle and oval layers of yellowView:

    enter image description here

    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:

    enter image description here

    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:

    enter image description here

    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:

    enter image description here

    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