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]
whereinitWithXX
is aninit
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?
EDIT
Here is the stack trace from my test project:
#0 0x000000010b0c9f78 in Test.SwiftView.init (Test.SwiftView.Type)(frame : C.CGRect) -> Test.SwiftView at /Users/xxx/Documents/Test/Test/SwiftView.swift:12
#1 0x000000010b0c9fa0 in @objc Test.SwiftView.init (Test.SwiftView.Type)(frame : C.CGRect) -> Test.SwiftView ()
#2 0x000000010b0c37bb in -[View initWithIdentifier:] at /Users/xxx/Documents/Test/Test/View.m:14
#3 0x000000010b0c97a6 in Test.SwiftView.init (Test.SwiftView.Type)(name : Swift.String) -> Test.SwiftView at /Users/xxx/Documents/Test/Test/SwiftView.swift:18
#4 0x000000010b0c9914 in Test.SwiftView.__allocating_init (Test.SwiftView.Type)(name : Swift.String) -> Test.SwiftView ()
#5 0x000000010b0c3b0e in Test.ViewController.viewDidLoad (Test.ViewController)() -> () at /Users/xxx/Documents/Test/Test/ViewController.swift:18
#6 0x000000010b0c3be2 in @objc Test.ViewController.viewDidLoad (Test.ViewController)() -> () ()
#7 0x000000010bbcb1d0 in -[UIViewController loadViewIfRequired] ()
#8 0x000000010bbcb3ce in -[UIViewController view] ()
#9 0x000000010bae6289 in -[UIWindow addRootViewControllerViewIfPossible] ()
#10 0x000000010bae664f in -[UIWindow _setHidden:forced:] ()
#11 0x000000010baf2de1 in -[UIWindow makeKeyAndVisible] ()
#12 0x000000010ba96417 in -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] ()
#13 0x000000010ba9919e in -[UIApplication _runWithMainScene:transitionContext:completion:] ()
#14 0x000000010ba98095 in -[UIApplication workspaceDidEndTransaction:] ()
#15 0x000000010f84c5e5 in __31-[FBSSerialQueue performAsync:]_block_invoke_2 ()
#16 0x000000010d7df41c in __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ ()
#17 0x000000010d7d5165 in __CFRunLoopDoBlocks ()
#18 0x000000010d7d4f25 in __CFRunLoopRun ()
#19 0x000000010d7d4366 in CFRunLoopRunSpecific ()
#20 0x000000010ba97b02 in -[UIApplication _run] ()
#21 0x000000010ba9a8c0 in UIApplicationMain ()
#22 0x000000010b0c90a7 in main at /Users/xxx/Documents/Test/Test/AppDelegate.swift:12
#23 0x000000010e54f145 in start ()
#24 0x000000010e54f145 in start ()
As obvious context, I have a simple single view iOS project, and I attempt to instantiate my SwiftView
object in my view controller's viewDidLoad
method.
EDIT
As bonus problems, another result of this is that if, in an attempt to make my subclass instantiate properly, I were to change my Swift class variable to an implicitly unwrapped optional:
let name: String!
and implement the missing initialiser by setting my immutable name variable to nil
(so that I can use the designated initialiser that I created but if someone uses the designated initialiser that I had to implement but never use then the resulting crash is their fault), I get the completely unexpected result that following my instantiating of the SwiftView
object with name
parameter in
let view: SwiftView = SwiftView(name: "test")
the value of view.name
is nil
(presumably because the init(frame:)
initialiser is called after the init(name:)
method initially sets the name
).
EDIT
Radar posted to Apple
来源:https://stackoverflow.com/questions/31161143/non-designated-initialiser-inheritance-from-objective-c-classes