Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Porting GPUImage filter subclasses to Swift

I'm trying to port an app from Objective-C to Swift, but I'm having problems with a subclass of GPUImageFilter.

In Obj-C, subclassing GPUImageFilter and using a different fragment shader was as easy as

- (id)init;
{
    NSString *fragmentShaderPathname = [[NSBundle mainBundle] pathForResource:@"TestShader" ofType:@"fsh"];
    NSString *fragmentShaderString = [NSString stringWithContentsOfFile:fragmentShaderPathname encoding:NSUTF8StringEncoding error:nil];

    if (!(self = [super initWithFragmentShaderFromString:fragmentShaderString]))
    {
        return nil;
    }

    return self;
}

I can't figure out how to do this in Swift. This is my new initializer:

override init() {
    let fragmentShaderPathname = NSBundle.mainBundle().pathForResource("TestShader", ofType: "fsh")!
    let fragmentShaderString = NSString(contentsOfFile: fragmentShaderPathname, encoding: NSUTF8StringEncoding, error: nil)

    super.init(fragmentShaderFromString: fragmentShaderString)
}

As soon as this gets called, the app crashes with this error message: /Users/peterwunder/Documents/XCode/welp/welp/GPUImageTestFilter.swift: 12: 7: fatal error: use of unimplemented initializer 'init(vertexShaderFromString:fragmentShaderFromString:)' for class 'welp.GPUImageTestFilter'

What am I doing wrong?

like image 206
Peter W. Avatar asked Oct 22 '14 14:10

Peter W.


1 Answers

Interesting problem! You're getting tripped up due to the differences in initialization between Swift and Objective-C. If the GPUImageFilter class were written in Swift, the compiler wouldn't let you have this setup, but since you're mixing & matching it doesn't complain until runtime. Here's what's happening when you try to instantiate your subclass:

  1. You call let myFilter = CustomGPUImageFilter()
  2. Your init() loads the shader string and calls super.init(fragmentShaderString: "foo")
  3. GPUImageFilter.init(fragmentShaderString:) calls [self initWithVertexShaderFromString:@"bar" fragmentShaderFromString:@"foo"].

What type is self in that last line? If you're calling from your subclass's initializer, the type of self is actually that of your subclass: CustomGPUImageFilter. So the initializer that gets called is actually on your subclass. Why is this a problem?

Unfortunately, Swift classes don't automatically inherit the initializers from their superclass if they define their own designated initializers, which is what your init() is. So the runtime tries to call CustomGPUImageFilter.init(vertexShaderFromString:fragmentShaderFromString:), doesn't find it, and crashes with that error message.

Happily, the fix is quite simple. If you mark your initializer as convenience init, you'll no longer be defining a designated initializer, which means your subclass will inherit the initializers from GPUImageFilter. Then in step 3 above, the runtime will find the inherited initializer on your subclass and execute without a problem. Note that you'll need to call self.init... instead of super.init...:

convenience override init() {
    let fragmentShaderPathname = NSBundle.mainBundle().pathForResource("TestShader", ofType: "fsh")!
    let fragmentShaderString = NSString(contentsOfFile: fragmentShaderPathname, encoding: NSUTF8StringEncoding, error: nil)

    self.init(fragmentShaderFromString: fragmentShaderString)
}

For more about this process, read the section on class inheritance and initialization in the Swift documentation. The process is significantly different from Objective-C.

like image 170
Nate Cook Avatar answered Oct 18 '22 09:10

Nate Cook