Bottom Line: This is a quick example of calling Swift from an Objective C app.

I’ve often wished that I could contribute to Quicksilver, probably my all-time favorite app, but unfortunately it’s written in Objective C. I don’t know any Objective C, and honestly I’m not terribly interested in learning it.

Lately, I’ve been picking up a little Swift. So far I don’t care for it as much as Rust, but I’ve read that it can be integrated into existing ObjC projects, which does pique my interest.

Today I decided to see if I could get a Swift “hello world” working in Quicksilver. It ended up not being terribly difficult, mostly thanks to this blog post and the StackOverflow thread it links to. There were a few bumps along the way, especially since I’m not proficient with Xcode or ObjC, so I wanted to document my process here.

Again, note that this is almost exactly as documented here, and I’ve copied the example code from there with minimal changes, just enough to run under a more modern version of Swift (5.4).

Basically my process was:

  1. Clone Quicksilver and open in Xcode
  2. Note that the Product Module Name is Quicksilver in the project settings
  3. Open the Code-App folder in the file navigator
  4. Create a new Swift File (name doesn’t seem to matter)
  5. Select yes regarding automatic generation of the header bridge file
  6. Add some Swift code
    • Classes should inherit from NSObject
    • Functions to be called from ObjC should be marked with @objc
  7. Back in Code-App, add to main.m:
    • #import <Quicksilver-Swift.h> (or replace Quicksilver with the product module name you found above – this has nothing to do with the name of the Swift file)
    • Some hello, world! code to call your Swift functions
  8. Run and see if it works!

Points that contrast with some of the other tutorials I found:

  • I did not have to enable the Defines Module setting
  • I did have to let Xcode do the automatic header bridge generation
    • NB: Some tutorials say you can decline this; it looks like that also works, but in that case I think you have to scatter public all over your Swift code to allow access to your class, init, functions, etc. – with the header file Xcode creates you can leave out the public.
  • I had to change a few Swift function definitions in the example code provided in the blog post I keep linking
  • I had to add @objc to functions that were going to be used directly from ObjC code

I’ll include below copies of the code changes I added / made; the parts of main.m that I modified are marked with comments.

$ swiftc --version
Apple Swift version 5.4 (swiftlang-1205.0.26.9 clang-1205.0.19.55)
Target: arm64-apple-darwin20.4.0
//
//  TrySomeSwift.swift
//  Quicksilver
//
//  Created by Nathan Henrie on 20210430.
//

import Foundation

class SwiftClass : NSObject {

    override init() {
        super.init()
        print("SwiftClass init")
    }

    func helloWorld() -> Void {
        print("hello, world!")
    }

    @objc func sayHello() -> Void {
        print("hello")
        helloWorld()
    }

    @objc func addX(_ x:Int, andY y:Int) -> Int {
        return x+y
    }

    // Make a dictionary
    // No, this code doesn't protect against values.count > keys.count
    @objc func dictionaryWithKeys(_ keys:[String], andValues values:[String]) -> Dictionary<String,String> {

        var dictionary = Dictionary<String,String>()

        for i in 0..<keys.count {
            dictionary[keys[i]] = values[i]
        }

        return dictionary
    }
}
//
//  main.m
//  Quicksilver
//
//  Created by Alcor on Sun Jun 29 2003.
//  Copyright (c) 2003 Blacktree, Inc. All rights reserved.
//

#import <Cocoa/Cocoa.h>
// BEGIN added by NDH 20210430
#import <Quicksilver-Swift.h>
// END added by NDH

int main(int argc, const char *argv[]) {
#ifdef DEBUG
	if(DEBUG_MEMORY) {
		setenv("NSZombieEnabled", "YES", 1);
		setenv("NSDeallocateZombies", "YES", 1); // So leaks don't get mad.
		setenv("MallocStackLogging", "1", 1);
		setenv("MallocStackLoggingNoCompact", "1", 1);
		setenv("NSAutoreleaseFreedObjectCheckEnabled", "YES", 1);
	}
#endif

	// BEGIN added by NDH 20210430
	SwiftClass* mySwiftClass = [[SwiftClass alloc]init];

	[mySwiftClass sayHello];

	int result = [mySwiftClass addX:5 andY:5];

	NSLog(@"5 + 5 is %d", result);

	NSDictionary* dictionary = [mySwiftClass dictionaryWithKeys:@[@"key1",@"key2",@"key3"] andValues:@[@"val1",@"val2",@"val3"]];

	NSLog(@"dictionary = %@", dictionary);

	NSLog(@"I'm here");
	// END added by NDH
	return NSApplicationMain(argc, argv);
}

Output in the Xcode console:

SwiftClass init
hello
hello, world!
2021-04-30 11:55:04.910562-0600 Quicksilver[92200:6185299] 5 + 5 is 10
2021-04-30 11:55:04.911047-0600 Quicksilver[92200:6185299] dictionary = {
    key1 = val1;
    key2 = val2;
    key3 = val3;
}
2021-04-30 11:55:04.911072-0600 Quicksilver[92200:6185299] I'm here

References