ioscatextlayer

CATextLayer is still in sublayers after calling removeFromSuperlayer


I have a mapView, which has 34 CAShapeLayer to render each province, and another 34 CATextLayer to render the name for each province. I will calculate its center for each province.

Now I add this mapView into a UIScrollView, and when I zoom the mapView, I want to re-draw the CATextLayer to use another font Size. So after zoom, I will manually remove all CATextLayer and redraw them, like below.

However, I found that, after the for loop finished, there is still some CATextLayer in the sublayers, to be exact, everytime I test it, there are 18 CATextLayer not being removed. I have never met this issue, do I miss something? Please help, thanks.

-(void)drawLabelsWithFontSize:(CGFloat)fontSize {
    int i = 0;
    NSUInteger count = [self.mapView.layer.sublayers count];
    for (i = 0; i < count; i++) {
        CALayer *layer = self.mapView.layer.sublayers[i];
        if ([layer isKindOfClass:[CATextLayer class]]) {
            [layer removeFromSuperlayer];
            // NSLog(@"%@, %lu",[layer class],i);
        } else {
            // NSLog(@"%@",[layer class]);
        }
    }
    // at here, some CATextLayer still in self.mapView.layer.sublayers
    __block typeof(self) weakSelf = self;
    NSDictionary *labelNameAndLocation = [self getLabelNameAndLocationInfo];
    NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
    paragraphStyle.alignment = NSTextAlignmentCenter;
    [labelNameAndLocation enumerateKeysAndObjectsWithOptions:NSEnumerationReverse usingBlock:^(id key, id obj, BOOL *stop) {
        if ([(NSString *)key length] > 0) {
            NSDictionary *location = (NSDictionary *)obj;
            CATextLayer *label = [CATextLayer layer];
            CGPoint caculateCenter = CGPointMake([weakSelf longitudeToCoordinate:[location[@"lng"] doubleValue]],[weakSelf latitudeToCoordinate:[location[@"lat"] doubleValue]]);
            NSMutableAttributedString *text = [[NSMutableAttributedString alloc]
                                                     initWithString:key
                                                     attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:fontSize],
                                                                  NSParagraphStyleAttributeName:paragraphStyle,
                                                                  NSForegroundColorAttributeName:[UIColor blackColor]}];
            CGSize size = [text size];
            [label setBounds:CGRectMake(0, 0, size.width, size.height)];
            [label setString:text];
            label.position = caculateCenter;
            label.contentsScale = [[UIScreen mainScreen] scale];
            [weakSelf.mapView.layer addSublayer:label];
        }
    }];
}

Solution

  • You're making the classic mistake of iterating up during removal:

    for (i = 0; i < count; i++) {
        CALayer *layer = self.mapView.layer.sublayers[i];
        if ([layer isKindOfClass:[CATextLayer class]]) {
            [layer removeFromSuperlayer];
            // NSLog(@"%@, %lu",[layer class],i);
        } else {
            // NSLog(@"%@",[layer class]);
        }
    }
    

    Change that for line to iterate down:

    for (i = count-1; i >= 0; i--) {
    

    The reason is that if you start by removing, say, sublayer 0, all the other sublayers move down one index. Thus, if you remove sublayer 0 and sublayer 1 is also a text layer, that text layer is now sublayer 0 and will never be removed because you proceed on to sublayer 1. Thus, as you quite rightly demonstrate, you end up missing exactly half of them.

    I'm a little surprised, in fact, that you haven't crashed because of this, but I guess there are so many sublayers that you just never fell off the end of the array; a crash is what usually happens.