I’m quite interested in introducing Swift in Nimble Commander’s codebase and gradually replacing its UI components with code written in this language. Integrating Swift into this codebase is not straightforward, as there is almost no pure Objective-C code. Instead, all UI-level code is compiled as Objective-C++, which gives transparent access to components written in C++ and its standard library. Frankly, this is often much more efficient and pleasant to use than [Core]Foundation. The challenge before was that interoperability between C++ and Swift was essentially non-existent, and the only solution was to manually write bridges in plain C, which was a showstopper for me. Last year, with Xcode 15, some reasonable C++ <-> Swift interoperability finally became available, but it was missing crucial parts to be meaningfully used in an established codebase. However, with Xcode 16, it seems that the interop is now mature enough to be taken seriously. This week, I converted a small Objective-C component to Swift and submitted the change to Nimble Commander’s repository. It was a rather bumpy ride and took quite a few hours to iron out the problems, so I decided to write down my notes to help someone else spare a few brain cells while going through a similar journey.
The start was promising: enable ObjC++ interop (SWIFT_OBJC_INTEROP_MODE=objcxx
), add a Swift file, and Xcode automatically enables Clang modules and creates a dummy bridging header. I had to tweak some C++ code with SWIFT_UNSAFE_REFERENCE
to allow the Swift compiler to import the required type, but after that, the setup worked like a charm – the Objective-C++ side created a view now implemented in Swift, and the Swift side seamlessly accessed the component written in [Objective]C++. All of this was fully supported by Xcode: navigation, auto-completion—it all worked! Well, until it didn’t. Trivial functions, like printing “Hello, World!” worked fine, but the actual UI component re-written in Swift greeted me with a crash:
Nimble Commander`@objc PanelListViewTableHeaderCell.init(textCell:):
0x10128ed74 <+0>: sub sp, sp, #0x50
0x10128ed78 <+4>: stp x20, x19, [sp, #0x30]
0x10128ed7c <+8>: stp x29, x30, [sp, #0x40]
0x10128ed80 <+12>: add x29, sp, #0x40
0x10128ed84 <+16>: str x0, [sp, #0x10]
0x10128ed88 <+20>: str x2, [sp, #0x18]
0x10128ed8c <+24>: mov x0, #0x0 ; =0
0x10128ed90 <+28>: bl 0x10149cf84 ; symbol stub for: type metadata accessor for Swift.MainActor
-> 0x10128ed94 <+32>: mov x20, x0
0x10128ed98 <+36>: str x20, [sp, #0x8]
...
This left me quite puzzled—the Swift runtime was clearly loaded, as I could write a function using its standard library, and it was executed correctly when called from the C++ side. Yet the UI code simply refused to work, with parts of it clearly not being loaded—the pointers to the functions were NULL
. Normally, I’d expect a runtime to either work correctly or fail entirely with a load-time error, but this was something new. As I don’t have much (or frankly, any) reasonable understanding of Swift’s runtime machinery under the hood, I tried searching online for any answers related to these symptoms and found essentially none. It’s not the kind of experience one would expect from a user-friendly language.
While searching for what other projects do, I stumbled upon a suspicious libswift_Concurrency.dylib
located in the Frameworks
directory, which gave me a hint—the actors model is related to concurrency, and the presence of this specific library couldn’t be a coincidence. So, out of desperation and curiosity, I blindly copied this library into Nimble Commander’s own Frameworks
directory, and lo and behold—it finally worked! There is an option to make Xcode copy this library automatically: ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES
. Another piece of the puzzle was that my @rpath
only contained @executable_path/../Frameworks
when it should have also included /usr/lib/swift
. With these changes, Nimble Commander can now run correctly on macOS 10.15 through macOS 15.
With that done and the actual application built, it was time to tackle the tooling around the project. While Xcode’s toolchain is used to compile Nimble Commander, a separate LLVM installation from Homebrew is used for linting. That’s because Xcode doesn’t include eitherclang-format
or clang-tidy
(seriously, weird!). Since Apple ships a modified toolchain, consuming the same source code with an external toolchain can be rather problematic. I had to make the following changes to get clang-tidy
to pass again after integrating the Swift source:
- Disable the
explicit-specialization-storage-class
diagnostic, as the automatically generated bridging code from the Swift compiler seems to contain incorrect declarations. - Disable Clang modules by manually removing the
-fmodules
and-fmodules-cache-path=
flags from the response files. - Remove framework imports (e.g.,
@import AppKit;
) from the automatically generated bridging headers. - Add the paths to the Swift toolchain to the project’s search paths, i.e.,
$(TOOLCHAIN_DIR)/usr/lib/swift
and$(TOOLCHAIN_DIR)/usr/include
. - Explicitly include the Swift interop machinery before including the bridging header, i.e.,
#include <swiftToCxx/_SwiftCxxInteroperability.h>
.
Such a number of hacks is unfortunate, and I hope to gradually make it easier to maintain.
This concludes the bumpy road to making Swift usable in Nimble Commander, though I’m sure more active use of the language will reveal other rough edges—no interop is seamless.