Migrating an Objective-C class to Swift using subclassing

Updates:

  1. Aug 10, 2018
    Clarified the restriction that this pattern doesn’t work for migrating a class that has existing Objective-C subclasses.

Other articles in this series:

  1. (1) Using extensions

  2. (2) Using subclassing

Following up on last week’s article on migrating an Objective-C class to Swift using extensions, I’d like to discuss an alternative approach: adding a Swift subclass to the Objective-C class you want to convert, as suggested by Jérôme Alves (thanks!):

[…] But another great solution I adopted successfully for a very large class was to rename ObjC HHFooBar into _HHFooBar and redefine HHFooBar as a Swift subclass of _HHFooBar. […]

@jegnux

Jérôme Alves

July 31, 2018

Prerequisites

I’ll use the same scenario as in part 1: you have a fairly large Objective-C class and you want to add a feature to it in Swift, without converting the entire class to Swift.

We need an additional constraint: the subclassing approach only works if the Objective-C class doesn’t have any Objective-C subclasses and you’re not planning to add any in the foreseeable future.

Strategy

Jérôme’s tweet lays down the strategy:

  1. Rename the Objective-C class, e.g. by adding an underscore: @interface NetworkService becomes @interface _NetworkService. Add the class header to your Swift bridging header.

  2. Create a Swift class that inherits from the Objective-C class and uses the original name:

    class NetworkService: _NetworkService {
        // ...
    }
    

    Since you’re reusing the class name, clients will automatically use the Swift subclass from now on, without you having to change anything. The only thing you may have to do is #import the Xcode-generated Swift header in places that use NetworkService.

  3. Subclasses of the original class will now also have the Swift class as their superclass. This is the reason these classes (if they exist) must be written in Swift because Swift/Clang doesn’t support subclassing a Swift class in Objective-C.

  4. Write your new code in the Swift subclass. Use @objc annotations where necessary to expose the new functionality to Objective-C clients. Unlike the extension pattern, adding stored properties to the subclass is no problem.

Accessing “private” superclass members in the subclass

If your Objective-C class has any “private” properties or methods (i.e. members that aren’t declared in the header file), those won’t be visible to the subclass. You’ll have to make them “public” by moving the declarations to the header file to be able to use them in Swift. Unfortunately, this pollutes the public interface of the class, but I don’t think there’s a way around that. (And it’s worth mentioning that the same issue affects the extension approach.)

The common Objective-C pattern of creating a separate header file for “protected” members doesn’t help much here: you’d have to add the internal header to the Swift bridging header to make it visible to the Swift class, which would make the internal members visible to (a) your entire Swift code and (b) all Objective-C code that imports the Swift header.

Accessing subclass APIs in the superclass

The subclassing approach has one significant drawback when compared to the extension pattern: the Objective-C superclass can’t easily call into the Swift subclass’s code. This makes it harder to add internal code that serves the rest of the class in Swift, or to migrate such internal methods from Objective-C to Swift.

The workaround is ugly: move the method/property declaration up from the subclass into the Objective-C class and let the Swift class override it, taking advantage of dynamic dispatch. The superclass method effectively becomes an abstract method — you should throw an exception in its implementation.

Recap

The subclassing approach is in many ways a cleaner design than the extension approach I discussed in the previous article. The ability to freely define stored properties is a clear win. And once you have finished the migration to Swift, simply delete the (now empty) superclass and you’re done.

The only significant downside seems to be the inability to call into the subclass code from the superclass (unless you work around it). The subclassing pattern may not be possible when the class you want to extend has Objective-C subclasses, however. In that case, you’d need to fully convert the subclasses to Swift first.