Migrating an Objective-C class to Swift: a piecemeal approach

I’m currently working on a fairly large iOS app with a long history. We try to write new code in Swift, but about 75–80 % of the code base is still Objective-C. We don’t rewrite existing code in Swift just for the sake of it, but only if a subsystem is due for a major overhaul anyway. The exception to the rule: when a particular subsystem reaches a tipping point — say, 60 % of it is written in Swift — it can make sense to transition the rest as well in order to minimize pains caused by mixing the two languages.

The interoperability between Objective-C and Swift is generally quite good1 — in fact, I’m amazed how well Apple was able to make the two languages work together given how different they are. That said, mixing two languages always causes some amount of friction.

No stored properties in extensions

I’d like to talk about one such cause of friction in particular that isn’t even directly related to interoperability, and that’s the fact that extensions can’t contain stored properties; in both Swift and Objective-C, all stored properties/ivars must be part of the main type definition. Even though this is a general limitation that doesn’t have anything to do with interoperability, it affects me most when I work on mixed Swift/Objective-C code.

Piecemeal conversion of an Objective-C class to Swift

Imagine the following scenario:

  • You have a fairly large and complex Objective-C class.
  • You want to add a new feature to the existing class, or you want to make substantial changes to part of the class. You’d prefer to write the new/changed code in Swift.
  • Rewriting the entire class in Swift is not the (immediate) goal. A piecemeal migration, perhaps over a span of one or two years as part of regular maintenance, would be nice though.

What if the Swift extension needs stored properties?

My usual strategy for this is to write a Swift extension for the Objective-C class. The new code goes into the extension. Where necessary, @objc annotations expose the extension’s code to Objective-C. This works great until the new code requires me to add a stored property to the class. It can’t go in the extension, I have to add it to the Objective-C class definition.

This in turn means the property must have an Objective-C-compatible type, even if it’s only to be used internally by the Swift code.** This is a fairly big limitation that I regularly run into: it means no structs, no enums with associated values, no generics, and more.

Workaround

Here’s the workaround I use: in Swift, I define an Objective-C-compatible class that acts as a wrapper for all stored properties I want to use in my Swift extension. In Objective-C, I add a property for an instance of that class to the main class definition. Once that’s done, everything else happens in the Swift code: the properties can use Swift-only features (assuming you don’t need to access them from Objective-C) — only the class itself must be visible to Objective-C.

Example

Let’s go through an example. Suppose the Objective-C class I want to extend is named NetworkService. I need to store a reference to a closure that isn’t compatible to Objective-C because it uses a generic Result enum. I define a NetworkServiceProperties class that has the Swift-only onDownloadComplete property next to my Swift extension:

@objc class NetworkServiceProperties: NSObject {
    var onDownloadComplete:
        ((Result<Data, NetworkError>) -> Void)? = nil
}

This class must be compatible with Objective-C, even if its properties aren’t. (I also tried nesting the class definition in the extension as that would nicely colocate it with the code it belongs to. Sadly, that didn’t work; the Objective-C property referencing the nested class would not be visible in the Swift code.)

Next, I add a property named props to the main class definition in Objective-C. This requires a forward declaration for NetworkServiceProperties because the generated Swift header can’t be imported into Objective-C header files.

@import Foundation;
@class NetworkServiceProperties;

@interface NetworkService : NSObject

@property (nonatomic, strong, readonly, nonnull)
    NetworkServiceProperties *props;

@end

Finally, in the NetworkService initializer, I initialize the new property:

#import "NetworkService.h"
#import "MyApp-Swift.h"

@implementation NetworkService

- (instancetype)init {
    self = [super init];
    if (self) {
        _props = [NetworkServiceProperties new];
    }
    return self;
}

// ...

Whenever possible, I try to give the Properties class an Objective-C-compatible initializer that I can call from the main class’s init. If that doesn’t work because some properties need special initialization, I define a swift_init (or similar) function in the Swift extension that initializes the Swift-only properties. I then call swift_init from the class’s regular (Objective-C) initializer. This works because the class initialization rules are not as strictly enforced by the compiler in Objective-C.

And that’s it. I can now access the properties everywhere in my extension through props:

extension NetworkService {
    func doSomething() {
        // ...
        // let result = Result(...)
        props.onDownloadComplete?(result)
    }
}

Final words

I really like this solution. It’s very straightforward; heck, you might say it’s totally obvious, and I’d agree. Still, it took me months of working on this project to think of it. Maybe it helps you too.

And when it’s time to complete the migration and remove the Objective-C class definition entirely, all I have to do is move the property definitions from NetworkServiceProperties into the class (now written in Swift) and delete .props everywhere it is used.

Check out part 2 of this series where I approach the same problem from a different angle.

  1. I’d love to link to a document titled Using Swift with Cocoa and Objective-C here, which I thought was an excellent resource, one of the best pieces of Swift documentation Apple has ever published.

    Sadly, Apple took it down recently, and the documentation that replaced it seems way less comprehensive. As far as I can tell, the new docs only talk about using C/Objective-C from Swift, and not at all about calling Swift code from Objective-C. ↩︎