Search code examples
xcodemacosswiftsystem-preferencesnspreferencepane

Using Swift with an OS X Preference Pane plugin


I'd like to use Swift to build an OS X Preference Pane plugin for the System Preferences app, but I can't get it to work.

enter image description here

After clicking "Next" the Xcode template doesn't offer an option to choose Swift as a language, but automatically creates the project in Objective-C.

enter image description here

Without adding any code or doing anything else, the project builds successfully. If you right-click on the Product and select "Open in External Editor", System Preferences will successfully install and load the preference pane.

enter image description here

enter image description here

enter image description here

It just works!

Well that's great, but now, I want to add a new Cocoa subclass using Swift.

enter image description here

enter image description here

Accepting the default, and allowing it to create the Bridging Header.

enter image description here

Now, quit System Preferences and without adding any code, rebuild the project. As before, right-click the Product and "Open in External Editor".

System Preferences will confirm replacing the preference pane, and it will install it, but then it fails to load.

enter image description here

enter image description here

If you show the built Product in the Finder, in addition to the .prefPane plugin, there's also a .swiftmodule folder.

enter image description here

I'm guessing there's something missing in the Build Phases or Build Settings that's responsible for incorporating the .swiftmodule with the rest of the bundle, but haven't been able to figure it out.

After you add some code that uses the new class, it's necessary to import the Swift project umbrella header ("Prax-Swift.h") to make the project compile, but importing the umbrella header doesn't fix this problem.

//  Prax.h

#import <PreferencePanes/PreferencePanes.h>
#import "Prax-Swift.h"

@interface Prax : NSPreferencePane

@property PraxObject *ourPrax;

- (void)mainViewDidLoad;

@end

I also tried deleting Prax.h and Prax.m and simply implementing the NSPreferencePane subclass in Swift. As before, the project builds and installs, but System Preferences fails to load it.

//  Prax.swift

import PreferencePanes

class Prax: NSPreferencePane {

    override func mainViewDidLoad() {

    }
}

Sorry if I've used too many pictures in this question; it seemed to be the clearest way to explain the problem and make it easy to reproduce. There's probably a simple solution. Any ideas?


Solution

  • First, you need to enable the "Embedded Content Contains Swift" setting so that Xcode will copy the necessary Swift libraries into the bundle.

    Then, you get this error:

    System Preferences[68872]: dlopen_preflight failed with
      dlopen_preflight(/.../preftest.prefPane/Contents/MacOS/preftest):
    
      Library not loaded: @rpath/libswiftAppKit.dylib
        Referenced from: /.../preftest.prefPane/Contents/MacOS/preftest  
        Reason: image not found for /.../preftest.prefPane
    

    This means the app doesn't know where to load the included Swift libraries from.

    To fix this, add @loader_path/../Frameworks to the runpath search paths in the build settings, telling it that the Swift libraries are in the Frameworks directory of your prefpane:

    See the dyld man page for further info about dynamic loading.