ObjC default initializers appear in a xcframework

Hello!
I struggling with Objective-C default initializers popped up in xcframework while we don’t have such initializers in the framework code. It looks like this in the xcframework interface file

Code Block
@objc override dynamic public init()

So if I use this framework not as xcframework but as Swift Package with source code then Xcode doesn’t allow to me to use this init() inherited from NSObject. But in a xcframework this init() appears and allows to me to use it and then crashes because there is no such init in this object.

So for example I have some MyStackView in a framework which is a subclass of UIStackView. And I have my initializer like init(withParameter: … ). But I can use default initializers of UIStackView like init(frame:) after conversion to xcframework. And it crashes.

For example, I have such code in the Framework

Code Block
public class Logger: NSObject {
    private let string: String
    public init(withString string: String) {
        self.string = string
    }
    public func printSomething() {
        print(string)
    }
}
public class MyStackView: UIStackView {
    public init(withName name: String) {
        super.init(frame: .zero)
        /*custom logic here*/
    }
    required public init(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}


now in the swiftinterface file we can see this
Code Block public var TestFrameworkVersionNumber: Double
@objc public class Logger : NSObject {
    public init(withString string: String)
    public func printSomething()
    @objc override dynamic public init() <---- ???
}
@objc public class MyStackView : UIStackView {
    public init(withName name: String)
    @objc required dynamic public init(coder: NSCoder)
    @objc override dynamic public init(frame: CGRect) <---- ???
}


If I try to do something like that in the client, it will crash, but compiler says that all is OK.

Code Block        
/* crash here!!! */
        let logger2 = Logger()
        logger2.printSomething()
/* crash here!!! */
        let stackView = MyStackView(frame: .zero)

How can I cope with it in the right way?

Replies

The rules of initializer inheritance are very subtle, but they're at the core of why everything seems to be misbehaving here. Let's break down the problem into its component parts:


Logger Does Not Override All The Designated Initializers of its Superclass


NSObject defines a single designated initializer: an argument-less init(). That initializer is automatically inherited by subclasses with the expectation that they will either override it to fully initialize their stored properties, or mark it as unavailable so clients no longer have to override it. The latter option is the one you want here, because Logger defines init(withString:). Here is one possible implementation of such a Logger

Code Block
public class Logger: NSObject {
  private let string: String
  @available(*, unavailable)
  public override init() {
    fatalError("Do not call this initializer!")
  }
  public init(withString string: String) {
    self.string = string
    super.init()
  }
}


Swift is caught between a rock and a hard place without this override. If we were to allow Logger to elide init(), then clients that call Logger.init() will fail to initialize Logger.string, and will just read garbage. So the compiler *must* synthesize something. We cannot synthesize a memberwise initializer because Logger.string has no default argument, so instead we fall back to a stub constructor that - as you've noticed here - crashes.

MyStackView Does Not Override All The Designated Initializers of its Superclass


Here, UIView is actually the one that defines a designated initializer init(frame:). You're in a similar boat here in that you need to either override this designated initializer yourself so it has a reasonable behavior when used by clients, or mark it unavailable. In this case, it seems you want to override it. I would perform all of the initialization logic in init(frame:) and mark MyStackView.init(withName:) as a convenience initializer that delegates to it. Like so:

Code Block
public class MyStackView: UIStackView {
  public override init(frame: CGRect) {
    /* initialize stored properties here */
    super.init(frame: frame)
    /* setup here */
  }
  public convenience init(withName name: String) {
    self.init(frame: .zero)
  }
  required public init(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}


Hello!

This looks like an issue with the Swift compiler when building for distribution. The @objc override dynamic public init() is not something that should actually be callable, but it being emitted into the .swiftinterface file. The reason you are seeing this with XCFrameworks and not your package is that the package is actually using the .swiftmodule file during build time, and the XCFramework has removed the .swiftmodule file during its creation process. You would see the same issue if you were to use the framework as well, as long it was missing the .swiftmodule file.

As a workaround, you'll have to ensure that you do not call one of those inherited methods and only those you expose via your API.

Could you please file Feedback on this issue? I want to make sure this gets routed to the appropriate place.

Thank you!
Thank you for your thorough reply. I have a related issue: In the PromiseKit framework, AnyPromise's default argument-less init initializer is marked as unavailable in the Objective-C header (see https://github.com/mxcl/PromiseKit/blob/master/Sources/AnyPromise.h#L295).
When trying to call it from Objective-C with [[AnyPromise alloc] init] the compiler correctly complains. However, there's no warning and no compiler error when illegally constructing it from Swift using AnyPromise(). Is there a way to work around this? I've tried a few, but to no avail. Shouldn't the Swift compiler pick up the Objective-C unavailable declaration in AnyPromise.h?
I have filed FB9016252 with a simple sample project clearly demonstrating the issue.

This issue effectively makes "Build for Distribution" a useless and dangerous feature since it allows compilation of code that will crash at runtime.