Search code examples
iosswiftcalayercagradientlayer

Swift not calling class init method on CAGradientLayer


I have an Objective-C subclass of 'CAGradientLayer' that overrides the '+ (CAGradientLayer *)layer' initializer. It is correctly included in my Bridging-Header

#import "ONCGradientLayer.h"

@implementation ONCGradientLayer

+ (ONCGradientLayer *)gradientLayer {
    return [ONCGradientLayer layer];
}

+ (ONCGradientLayer *)layer {
    ONCGradientLayer * layer = [super layer];
    layer.colors = @[(id)[[UIColor colorWithWhite:0.0 alpha:1] CGColor], (id)[[UIColor colorWithWhite:0.1 alpha:1] CGColor]];
    layer.startPoint = CGPointMake(0, 0);
    layer.endPoint = CGPointMake(0, 1);
    return layer;
}

This all works as expected from Objective-C, by calling either [ONCGradientLayer layer] or [ONCGradientLayer gradientLayer]

When I try to call it from swift, I'm given a compiler warning that the method has been renamed init, so I must call ONCGradientLayer() This, however, does not work, and the overridden initializer is never called.

I can work around this by adding a differently named initializer :

+ (ONCGradientLayer *)swiftInit {
    return [ONCGradientLayer layer];
}

I can successfully called that in swift using ONCGradientLayer.swiftInit()

What is going on that prevents me from calling ONCGradientLayer() and getting the overridden initializer?

UPDATE : I can make the ONCGradientLayer() work by refactoring it to use the init method, though I'm still curious why the other approach does not work.

- (instancetype)init {
    if (self = [super init]) {
        self.colors = @[(id)[DEFAULT_BACKGROUND_COLOR CGColor], (id)[[UIColor colorWithWhite:.1 alpha:1] CGColor]];
        self.startPoint = CGPointMake(0, 0);
        self.endPoint = CGPointMake(0, 1);
    }
    return self;
}

Solution

  • It's all about a Swift component called the Clang Importer. It is responsible for translating Objective-C method names into Swift method names (and vice versa).

    The Clang Importer assumes that if an Objective C class has a class method with the same name as the class except for the prefix and the lowercasing, possibly extended by with, it is a class factory method.

    It also assumes that a class factory method is the same effectively as the initializer whose name is constructed the same way, and hides the corresponding factory method completely, because with ARC there is no meaningful difference between the two.

    EXAMPLE: NSString has factory method stringWithFormat: and initializer initWithFormat:.

    Well, string is the same as NSString stripped of its prefix and lowercased; so the Clang Importer assumes that stringWithFormat: is a factory method. Moreover, it is constructed just like initWithFormat:, so the Clang Importer assumes they are effectively the same, and hides the former.

    Therefore you have to say String(format:), meaning String.init(format:), in Swift. [The Clang Importer also strips the with, but that's another story.] There is no String.withFormat(_:) (or similar) in Swift, matching Objective-C stringWithFormat:.

    That is exactly the situation with your ONCGradientLayer and gradientLayer. Your class method follows the class factory method naming convention. Therefore, thanks to the Clang Importer, if there is an init — and there is — then the class factory method is hidden. Therefore you have to call init instead, saying ONCGradientLayer() in Swift.

    But you didn’t define init to return a modified ONCGradientLayer, so the result is a plain layer without your modifications.

    If you don’t like that, you have two choices. And you already discovered what they are!

    • Change the class factory name, to e.g. makeGradientLayer. (That is exactly what you did by supplying the name swiftInit.)

    • Or, define init to return a ONCGradientLayer modified in the same way as you did in layer. (That is what you did too.)