Non-designated initialiser inheritance from Objective C classes

守給你的承諾、 提交于 2020-01-02 11:20:12

问题


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?

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!