Manage legacy Objective-C in modern applications

Legacy or old projets might have Objective-C files, often mixed with Swift ones. How to continue implementing new behavior and fixes in such a stack?

Context

In projects with both Swift and Objective-C, you have to expose one to the other. In order to do so, you have the Objective-C bridging header file Project-Bridging-Header.h. This allows you to expose the Objective-C files you declare in this file to Swift. For the other way around, you have the Swift bridging header file Project-Swift.h, that you must import in your Objective-C files. This bridging file is automatically generated with the @objc or @objcMembers annotations in Swift.

Objective-C to Swift golden rule: never write more than one line of Objective-C

As languages evolve, pratices do as well. One main component of it is that people are now writing Swift code everyday, Objective-C one not so often. To be able to make some projet evolve the right way we have to be strict: never write more than one line of Objective-C code. To do that we have several options that you can see down below.

The key component is to write minimal Objective-C code to be able to bridge it with Swift files.

Given a regular Objective-C class like the following, let’s see what we can do.

@interface ViewController: UIViewController
@end

First try to put your development in a Swift extension

extension ViewController {}

This will be enough for most cases, the main limitation being that you can’t add new properties in this extension.

Example:

extension ViewController {

    @objc func functionInSwiftForObjc() {
        // Put your implementation here
    }
}
@implementation ViewController: UIViewController

- (void)functionInObjc {
    [self functionInSwiftForObjc];
}
@end

Then use heritage to add properties

@interface ViewControllerObjc: UIViewController
@end

@implementation ViewControllerObjc: UIViewController

- (void)functionInObjc {
    // some behavior
    [(ViewController *)self functionInSwiftForObjc];
    // some behavior
}
@end
class ViewController: ViewControllerObjc {

    var newVariable: Class
    
    @objc func functionInSwiftForObjc() {
        // Put your implementation with newVariable here
    }
}

Tip: Rename the Objective-C file in NameObjc so the implementation outside this file will not change, and make a convention to not call NameObjc files directly in your project, they will only be used from their Swift child file.

Use Swift components in Objective-C: be smart

Create a class for enum that are not visible in Objective-C

If you use SwiftGen to generate your wording, you can manually create an enum visible in Objective-C.

For instance, this code is auto-generated by SwiftGen:

internal enum L10n {
  internal static let alertMessage = L10n.tr("alert_message")
}

You can manually create a ObjcL10n class for variables which need to be Objective-C available

@objcMembers class ObjcL10n: NSObject {
    static let alertMessage = L10n.alertMessage
}
@implementation ViewController: UIViewController

- (void)functionInObjc {
    // some behavior
    self.label.text = ObjcL10n.alertMessage
    // some behavior
}
@end

Create a class for struct that are not visible in Objective-C

Struct are not visible in Objective-C. In order to use them you can transform them into classes.

⚠️ Keep those classes simple as structs cannot be inherited and be aware that structs are value types while classes are reference ones.

struct ModelMapper {
    
    let parameter: Class
    func map() -> Model {}
}

becomes:

class ModelMapper {

    private let parameter: Class

    init(parameter: Class) {
        self.parameter = parameter
    }

    func map() -> Model {}
}

Add Objc-c bridges for Swift components: for instance Swift.Result

class Repository {

    func fetch(completion: @escaping (Result<Class, Error>) -> Void) { 
        // implementation
    }
}

becomes for instance:

class Repository {

    func fetch(completion: @escaping  (Class?, Error?) -> Void) { 
        // same implementation
        switch result {
            case .success(let object):
                completion(object, nil)
            case .error(let error):
                completion(nil, error)
        }
    }
}

Conclusion

Objective-C has still a long run before its end-of-life support, so you can take your time and follow these easy steps to migrate your code, one class at a time. Keep in mind that you should not write more than one line of Objective-C and use some Swift bridges.