Non-designated initialiser inheritance from Objective C classes

Having come across problems when sub-classing UIKit classes and adding immutable variables to them, I made a test project to figure out what was going on.


My conclusion is that if

  • we have an Objective C class, which inherits from another class, with its own designated initialiser (implicit or explicitly annotated)
  • in its initialiser, it calls [self initWithXX] where initWithXX is an init method on the superclass
  • we subclass this class in Swift, adding an immutable property (which obviously must be initialised on instantiation)
  • we implement a single designated initialiser for this Swift class which sets the immutable property then calls the parent class designated initialiser

then this will cause a runtime exception because the Swift class, when calling the Objective C superclass's designated initialiser, will attempt to call initWithXX on self and this method has not been inherited from the superclass because we have implemented a designated initialiser.

The code for this test would be:

View.h

#import <UIKit/UIKit.h>

@interface View : UIView

-(instancetype)initWithIdentifier:(NSString *)identifier;

@end


View.m

#import "View.h"

@implementation View

-(instancetype)initWithIdentifier:(NSString *)identifier {
  self = [self initWithFrame:CGRectZero];
  if (self) {
    if ([identifier length] > 0) {
      return self;
    }
  }
  return nil;
}

-(instancetype)initWithFrame:(CGRect)frame {
  self = [super initWithFrame:frame];
  if (self) {
    return self;
  }
  return nil;
}

@end


SwiftView.swift

import Foundation
import UIKit

class SwiftView: View {
  let name: String

  init(name: String) {
    self.name = name

    super.init(identifier: "test")
  }

  required init(coder aDecoder: NSCoder) {
      fatalError("init(coder:) has not been implemented")
  }
}


Exception generating code

let view: SwiftView = SwiftView(name: "test")


Exception

fatal error: use of unimplemented initializer 'init(frame:)' for class 'Test.SwiftView'


My motive behind this was sub-classing standard library classes (UITableViewController, MKAnnotationView...) and adding immutable properties to contain injected dependencies on them, which then needed to be initialised on instantiation of the sub-class by passing them in to a custom designated init method, and finding that this caused the above, unexpected exception.


Having done this experiment to determine the causes, I think that I understand why it is happening (because the parent Objective C class designated initialiser is delegating to another designated initialiser of its own, rather than calling its superclass's initialiser). It would seem obvious that to fix this, the standard libraries should change their initialiser methods to call superclass initialisers instead of self initialisers, but I can appreciate that calling self instead of super may be valid functionality in order to allow subclasses to override default behaviour while preserving particular initialisers. Also, I would imagine that such a change may cause lots of problems because many apps may have built on this functionality, and this would break it. I suppose the basic problem here is a language feature incompatibility between Objective C and Swift.


Can anyone see any way around this problem (either in terms of an Apple fix or code workarounds) other than the (very undesirable) way of making these immutable variables mutable and setting them in the second initialisation phase?

"It would seem obvious that to fix this, the standard libraries should change their initialiser methods to call superclass initialisers instead of self initialisers"

Nope, the correct solution would be to fix whatever bug in Swift is responsible for this problem. Blocking initializers from calling self would seriously break behavior, not only preventing cross-calls but up-calls as well. Imagine you have the following class hierarchy:



BaseClass
requiredInitializer
MainClass : BaseClass
convenienceInitializer [calls self.requiredInitializer]
CustomClass : MainClass
override requiredInitializer [calls super.requiredInitializer]


If

convenienceInitializer
is only allowed to call
super.requiredInitializer
, invoking
CustomClass.convenienceInitializer
would be a total disaster because
CustomClass.requiredInitializer
would never be called; and no prizes for guessing the outcome of that.


OTOH, if you say that clients are not allowed to invoke

CustomClass.convenienceInitializer
then you've just violated Liskov's substitution principle and thus broken OOP itself; even more embarrassing!


Honestly, Swift's initializers are going to go down in history as not one of its better ideas, but in the meantime you need to get over to https://bugreport.apple.com if you've not already done so and file a new one so that this gets fixed before release. If you can copy your ticket over to http://openradar.appspot.com and post the URL here then that'd also be helpful as the rest of us can just dupe it. (I ran into the same problem yesterday and was going to put a ticket in, but your test code provides a far better description of the problem than my messy codebase.)

(Other things unlikely to distinguish themselves in the annals of history: this horrid forum software.)

Not sure that I agree with you on this. Swift seems to me to preserve Liskov's substitution principle pretty well in most things (and indeed better than Objective C which allows transforming immutable properties into mutable ones).


Swift's initialisation process / hierarchy is a special case, and while it does seem cumbersome, it does also have its logic. According to the language rules of Swift I don't think we can see this as a bug - Apple's documentation (https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Initialization.html) explicitly states that initialisers are only inherited when you don't implement a designated initialiser yourself (and convenience initialisers may be inherited if you implement all superclass designated initialisers). Therefore, by implementing my own designated initialiser, I may call a superclass initialiser within my initialisation method, but my class will not inherit implementations of my superclass's other initialisers. If, in my Swift class, I were to attempt to call another initialiser on self, which I have not implemented (even if my superclass does implement it) then this is an error. However, Objective C does allow automatic inheritance of all initialisers, so the call to self succeeds.


In your example,

convenienceInitializer
may call
self.requiredInitializer
so long as
self
implements
requiredInitializer


So perhaps the initialisation methods (and only the initialisation methods) may be considered a violation of Liskov's substitution principle, but I would say that the principle is incompatible with adding non-optional immutable properties to a sub-class (where they cannot have a default instantiation) so I think that I can live with that since the only case where this should be a problem should be when using reflection to instantiate objects, since at all other times (ie whenever you have an already instantiated instance or when you are calling a static method) Liskov's principle is preserved.


So I think that this does come down to a basic incompatibility between the two languages that will not be easy to resolve.


Anyway, sorry for not replying earlier - your original message was marked as hidden for quite a while.


I will definitely file a bug report, just to see what Apple think can be done, and will post the link here when done.

Non-designated initialiser inheritance from Objective C classes
 
 
Q