Articles index

Porting Objective-C to Swift

June 19, 2017

By Jeff Johnson, Developer of Underpass

Objective-C and Swift are said to be interoperable. You can add Swift files to Objective-C projects and vice versa. If you want to port a large Objective-C project to Swift, the sensible way would be piecemeal, file by file. It is well known that rewriting an app from scratch is fraught with peril, and incremental refactoring almost always produces better results. This topic has been discussed extensively elsewhere, so I won't elaborate on it here. I'm just going to talk about the technicalities of porting Objective-C files to Swift.

1. Null

Before you start to add any Swift to an Objective-C project, it's a good idea to go through your Objective-C API and annotate it for nullability. I discussed some issues with that in an earlier blog post.

2. Bridging Header

When you have a mixed Objective-C/Swift project, the principal issue is how to call the API from one language in the other language. To import your Objective-C API into Swift, you need a bridging header, as specified with the build setting SWIFT_OBJC_BRIDGING_HEADER. The bridging header is similar to a prefix header, except it applies only to Swift code. In the bridging header, you #import the Objective-C header files you want to expose to Swift. Unfortunately, if you also use a prefix header in your project, the prefix header is not automatically imported along with the other headers. I consider this a bug, because Objective-C headers have always depended on the prefix header and might not compile without it. So you need to explicitly import your prefix header in the bridging header.

Unlike Objective-C, Swift does not use separate header and implementation files. The API and the implementation are both in the same file. To import your Swift API into Objective-C, the compiler automatically generates a header file named after your project, for example, Underpass-Swift.h. You import that header in your Objective-C files. A natural place to import it would be in your prefix header, but then you can run into an issue with dependencies. The Swift header is not generated until after the Swift code is compiled, but if your bridging header imports your prefix file, and your prefix file imports the Swift header, your build will fail. In other words, you can't import the automatically generated Swift header in your bridging header. Thus, if you still want to import the Swift header in your prefix header, you need to add a compiler directive. For example, you can protect your bridging header like this:

#ifndef UNDERPASS_BRIDGING_HEADER
#define UNDERPASS_BRIDGING_HEADER
#import "PrefixHeader.pch"
#endif

And then you conditionally import the Swift header in your prefix file:

#ifndef UNDERPASS_BRIDGING_HEADER
#import "Underpass-Swift.h"
#endif

This will import the Swift header in your Objective-C files, but it won't import the Swift header in the bridging header.

3. Generated Interface

When you import Objective-C into Swift, the compiler automatically rewrites the API to be more "Swifty". You can see how this works if you select an Objective-C header file in Xcode and run "Generated Interface". (This is in the same menu as Preprocess, Assembly, and Disassembly.) The API is transformed according to rules that only the compiler knows fully, and the rules can change from one Swift version to the next. Fun, eh? All in the name of progress.

As with any form of automated translation, you sometimes get absurd results. For example, I have some NSView category methods that I use to facilitate programmatic autolayout. One such method in Objective-C:

-(NSLayoutConstraint *)JJ_constraintBelowView:(NSView *)view
{
	return [[self topAnchor] constraintEqualToAnchor:[view bottomAnchor] constant:10.0];
}

Note that the method is prefixed to avoid any possible naming collision. And here's the Generated Interface in Swift:

open func jj_constraint(below view: NSView) -> NSLayoutConstraint

The new function name is way too generic. Also, if you didn't manually run "Generated Interface" in Xcode, how could you possibly guess that this would be how the compiler translates the API when you import the Objective-C into Swift? This is a problem because when you're writing Swift code with imported Objective-C API, you have to use the transformed symbol names rather than the original.

Setting aside the issue of unwelcome translations, you might think about using the Generated Interface as a basis for porting your Objective-C code to Swift. There's a dilemma here, though. The advantage of the Generated Interface is that if you already have other Swift files that were using the Objective-C API, those Swift files won't need to be refactored, because they were already using the automatically transformed Objective-C API. The disadvantage of the Generated Interface is that it breaks all of the existing Objective-C calls to that API. The automatic compiler API transformation does not work in reverse: when you import Swift into Objective-C, the Swift API stays the same, it doesn't automatically become more "Objective". So once you let the compiler do its dirty work on the API, you then have to refactor any leftover Objective-C files that use it. When you're just beginning to port an entire Objective-C project to Swift, this can be a major pain.

One approach would be to add @obj() annotations to the Swift API to customize how it's imported into Objective-C. Then you could preserve the existing calls in Objective-C files, as well as the existing calls in Swift files, and when everything has been ported to Swift, you can ditch the @obj(). This seems like a lot of busywork for a "temporary" solution, though, and it has the disadvantage that you are forced to use two different API for the same implementation.

Personally, I prefer to have a single API for both Objective-C and Swift, of course granting some differences to accommodate the syntax of each language. I've never been a fan of the compiler auto-transforming Objective-C API when imported into Swift. The result may be more "Swifty", but there's still an enormous amount of extant Objective-C code that's not disappearing any time soon, so I feel the tradeoff between "Swiftiness" and interoperability was not an ideal one at this point in the development of the Swift language. If all you do is write Swift, it may be fine, but it's problematic if you need to continually jump back and forth between the two languages.

4. Global Functions

Since Objective-C is a superset of C, it's trivial to call global C functions in Objective-C code. Swift also has global functions, so when you port Objective-C to Swift, it's natural to convert global C functions to global Swift functions, and indeed that's what the "Generated Interface" does. However, Swift does not export global functions to Objective-C, so you can't call global Swift functions in Objective-C code. In order to export a Swift function to Objective-C, you have to wrap it with an Objective-C object. My Objective-C project had the function NSTextField *JJLabelView(NSString *label), and I ported the function to Swift with an Objective-C class:

class JJDummyClass : NSObject {
	class func labelView(_ label: String) -> NSTextField {
		return JJLabelView(label)
	}
}

func JJLabelView(_ label: String) -> NSTextField

The ported Swift code can continue to call JJLabelView(). The trick is to preserve the existing function call sites in Objective-C without having to refactor everything. So I added this to my prefix header:

#define JJLabelView(label) [JJDummyClass labelView:label]

Then I can use the same API JJLabelView() in both Objective-C and Swift.

5. To Be Continued…

That's all for now. I'm sure I'll have more to say later, so stay tuned. Same bat time. Same bat channel.

Articles index