Swift: use principalClass of bundle - is this a BUG?

Hello,


I spent a lot of time to find a way of using a Swift-bundle in a pure Swift Application. Finally I succeeded to avoid compile-time-linking, which caused the problems. The rules, I have to obay:


1. The principalClass in the bundle may not be declared as "final"! //Why that? Is it a bug?

2. No typecast for bundle.principalClass is allowed! //When casting, runtime-linking follows.

3. You only can use a singleton instance in the bundle - no bundle.principlaClass.init() is possible. //Is this a bug?


At the end, I could use a bundle like this:


public class XYZClass: NSObject {

public let xyzShared = XYZClass()


public func xyzRockTheBundle() {}

}


The application calls the bundle by:

import BundleName

guard let bundle = NSBundle(URL: url) else {continue} //url aus builtInPlugInsURL ermitteln

guard let bundleClass = bundle.principalClass else {return}

bundleClass.xyzShared.xyzRockTheBundle()


Is that really the right way? I did not find this in any documentation? Or is it a bug?


I'm interested in your comments.


Kind regards


Wolfgang

Firstly, I’m assuming that your desired end state is the ability to instantiate objects from the bundle’s principal class, as folks have done since the dawn of time in Cocoa.

Second, I’ve assumed that you’re looking for a pure Swift solution.

Given those assumptions, I played around with this and got something working. However, I’m reticent to suggest it as an option because, well, it’s kinda scary. Honestly, I think you might be better off not using a pure Swift solution here, but instead use an Objective-C class to provide some infrastructure.

Anyway, here’s how I did this using pure Swift:

  1. I created a project with three targets: a framework, a bundle and an app.

    IMPORTANT I used a framework to guarantee that there’d only be one instance of my base class (see the next point) published to the runtime.

  2. In the framework I added a base class,

    XYZ
    . The key feature of this class is that it implements the
    +makeXYZ
    method, as shown below.
  3. In my bundle I linked to the framework, subclassed

    XYZ
    and set that class as the bundle’s principal class.
  4. In my app I also linked to the framework and then instantiated the principal class as shown below.

Framework base class code:

public class XYZ : NSObject {
    class func makeXYZ() -> XYZ {
        let s = "new"
        return self.performSelector(Selector(s)).takeRetainedValue() as! XYZ
    }
    public func foo() {
        NSLog("foo")
    }
}

Host app code:

let bundleURL = NSBundle.mainBundle().builtInPlugInsURL!.URLByAppendingPathComponent("xxc.bundle")
let bundle = NSBundle(URL: bundleURL)!
let success = bundle.load()
assert(success)
let bundleClass: AnyClass = bundle.principalClass!
let instance = bundleClass.makeXYZ()
instance.foo()

There’s two bits of magic here:

  • calling class methods of the principal class — I can do this because

    bundleClass
    is of type
    AnyClass
    , and that lets you call any class method that’s known to Objective-C, which includes my
    +makeXYZ
    method.
  • instantiating an unknown class in Swift — I did this by using

    -performSelector:
    to send the class a
    +new
    method. Wow, that’s ugly, and it’s the main reason why I think that an Objective-C base class would be better. If you implemented
    XYZ
    in Objective-C,
    +makeXYZ
    would just call alloc/init, which is absolutely 100% standard practice.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks for your comments! I understand:


  1. There is infact (today) no other way to do it completely in Swift! But I believe in Swift. 🙂As I started only a couple of months ago with my first MacOS-project, I do'nt have Objective-C-Code. The only limitation I see: I can not use init on principalClass in my application.
  2. As for now, it's better to do it in Objective-C. Yes, I tried it with an "Objective-C-Bridge" between the Application (Swift) and the Bundle (Swift). This works perfect - besides of the limitations you get, when declaring methods and properties in the bundle as "@objc"! Mainly with parameters of callback-functions I had those problems.


My conclusion: I do it with pure Swift. It's much easier to do and to read the code. I hope, the limitations are only for today and will be gone in future Swift-versions. That's why I'm interested to know, if I should send a BUG-report?

I haven't tried this in this context, but could you get around the ugliness of the performSelector("new") by declaring in XYZ class a required initializer init() and then calling it from the host app like:


let bundleClass = bundle.principalClass as! XYZ.Type
let instance = bundleClass.init()


I've done something similar in a previous project, but not using bundle.principalClass, so I'm not sure how that would work, though I too am curious about this, as a current project will need a similar bundle-loading mechanism for plugins.

… could you get around the ugliness of the performSelector("new") by declaring in XYZ class a required initializer init() and then calling it from the host app like …

Alas, that does not work. Your line 2 won’t compile.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

...it will compile but not link!

Ironically, I just ran in to this same issue with a project of mine converting to Swift3... I managed to get everything compiling using a small bundle class loader written in ObjC, but I can't seem to get the principalClass to configure correctly... Seems the principalClass is always nil for me.


Did you ever get this working with a pure-Swift principal class? If so, how did you reference it in the info.plist for the bundle?

Updated to Xcode 8 beta 4 today... solved the problem... all good now.

FYI. I stumbled across a blog post by Jarek Pendowski that works beatifully. He also has a sample Git project. Another Git user 'Prince2k3' contributed a fix to make it work with pure 100% swift. I had to tweak it a bit further by applying the fixes recommended by Xcode. But it is now working with 100% Swift and no bridging header.


http://blog.pendowski.com/plugin-architecture-in-swift-ish/


https://github.com/pendowski/SwiftPluginsExample


https://github.com/pendowski/SwiftPluginsExample/issues/1

Swift: use principalClass of bundle - is this a BUG?
 
 
Q