Slowly migrating from Objective-C to Swift

I had a case recently where I wanted to migrate an Objective-C class to Swift but as it was a large class. I wanted to go one method at a time to allow easier reviewing and to keep my sanity, whilst having each step still pass all unit tests. I quickly hit issues where it seemed like I would have to bite the bullet and just do it as a single large commit. Helpfully I saw a proposal to allow you to provide Objective-C implementations in Swift, which lead me to finding the _ version of the feature spelt @_objcImplementation that is perfect for my quick migration until the full implementation lands.


Starting point

Let’s say I have the following simple class that I want to migrate one function at a time

MyObject.h

@interface MyObject: NSObject

- (void)doSomething1;
- (void)doSomething2;

@end

MyObject.m

@interface MyObject ()

@property (nonatomic, copy) NSString *title;

@end

@implementation MyObject

- (void)doSomething1 { ... }
- (void)doSomething2 { ... }

@end

The above is a class with two public methods and a “private” property declared in an anonymous category.


One step migration

If I wanted to migrate this in one go I can delete the .m file and create a Swift file like this

MyObject.swift

@_objcImplementation MyObject { }

At this point the compiler will complain about the missing implementations

Extension for main class interface should provide implementation for instance method ‘doSomething1()’; this will become an error before ‘@_objcImplementation’ is stabilized

Extension for main class interface should provide implementation for instance method ‘doSomething2()’; this will become an error before ‘@_objcImplementation’ is stabilized

To make the compiler happy I need to provide all the implementations like so:

MyObject.swift

@_objcImplementation MyObject {
    func doSomething1() { ... }
    func doSomething2() { ... }
}

This might be fine for small classes but my goal was to be able to break the task down into small chunks whilst keeping everything compiling and tests passing.


Create named categories

To do this in a more controlled way the best thing to do is to split the @interface into named categories and then specify the category name in the annotation. For example I called my category SwiftMigration

MyObject.h

@interface MyObject: NSObject

- (void)doSomething2;

@end

@interface MyObject (SwiftMigration)

- (void)doSomething1;

@end

The corresponding Swift file that targets the category now only needs to implement the one method and would look like this:

MyObject.swift

@_objcImplementation(SwiftMigration) extension MyObject {
    func doSomething1() { ... }
}

With this approach I can go one method at a time and it doesn’t feel like such a big risk doing the port.


Properties

In the example above I have a property declared in an anonymous category which essentially makes it private to my class implementation. Normally you cannot declare new storage in extensions but with @_objcImplementation you are allowed to declare storage on the top implementation (the unnamed one), which would look like this:

MyObject.swift

@_objcImplementation extension MyObject {
    private var title: String?
}

Final clean up

Whether migrating in one go or piece by piece it’s then worth asking if the @_objcImplementation is required at all or if you can delete the header file and make it a pure Swift class. There are cases where you might need to continue to use the new capabilities like if you still have code in Objective-C that subclasses your class.


General migration strategies

There are other ways of avoiding rewriting large amounts of code whilst still taking advantage of Swift. I use these techniques to help avoid adding any new Objective-C.

Extensions

Swift extensions are a great way for adding new Swift code to legacy Objective-C code. In the simple case where all call sites will only be Swift based you can create an extension and don’t annotate it as @objc

MyObject.swift

extension MyObject {
    func doSomethingNew() { ... }
}

I will even prefer doing this over writing too much new Objective-C in the a class. For example I might just annotate my doSomethingNew function as @objc and then call it on self. This isn’t perfect for encapsulation but I don’t generally write frameworks and I’m happy to ignore the purity for the added safety.

MyObject.m

@implementation MyObject

- (void)doSomething
{
    [self doSomethingNew];
}

@end

Shims

In cases where I know the interop between Swift and Objective-C should be fairly short lived I’ll often create small shims. The aim is to write the Swift code in the most natural style and then just write ugly bridge code in the shim with the knowledge that in future I can delete the shim and won’t need to reevaluate the interface of the underlying class. For example I might have

class MyObject {
    func doSomething(completion: (Result<String, Error>) -> Error) { ... }
}

With the above I can’t annotate the method with @objc because Objective-C can’t represent the Result type. Instead of making this less Swifty I’d write a shim like this

class MyObject {
    @objc
    func doSomething(completion: (Bool, String?, Error?) -> Error) {
        doSomething { result in
            switch result {
            case let .success(string):
                completion(true, string, nil)
            case let .failure(error):
                completion(false, nil, error)
            }
        }
    }

    func doSomething(completion: (Result<String, Error>) -> Error) { ... }
}

Wrap up

Although it may not always make sense it’s amazing how many bugs you find when you look at porting Objective-C to Swift. There’s the obvious errors that language features help you avoid writing and then there is just being forced to look at old code with fresh eyes and new patterns.

mise en place Xcode

I’ve recently started using mise “a polyglot tool version manager” and have been really impressed with how much simpler it makes configuring projects.

For example if I want to use a tool like swiftlint I have to make sure that all other developers on my team and the CI machine agree on which version to use. If we have different versions we might end up with contrasting linting rules causing various errors/disagreement. In the past I’ve dealt with this by bundling the binary into the repository but this consumes repository space that will never be reclaimed.

A better way

Enter mise a tool which I only investigated because tuist started using it for managing its versions. With mise I can create a file in my repository called .mise.toml and configure the version of swiftlint I want to use like this

.mise.toml

[tools]
swiftlint = "0.55.1"

With this file in place I can call mise install and mise will download the version I specified and make sure it is installed on my $PATH. Now when I call swiftlint from within my projects directory it will ensure that the correct version of swiftlint is used.

What about in Xcode?

mise works by modifying $PATH when you cd into a directory, so that won’t work in Xcode run scripts. Fortunately mise provides a solution; instead of calling swiftlint directly in my run script I can instead tell mise to execute swiftlint like this

$HOME/.local/bin/mise x -- swiftlint

mise will do the same thing of looking inside the .mise.toml file and then making sure it invokes the correct version of swiftlint for me.

What about on CI?

You can use the same trick above on CI or if you are using github actions there is a specific mise action that will take care of calling install and setting up the path for any following actions.


Can it help with fastlane?

Yes. fastlane is a ruby gem and Ruby is not always easy to get configured right especially if you’ve not got much terminal experience. Now I love Ruby but I hate to think how many hours/days I lost over the years helping colleagues get their environment set up. I’ve also personally changed my Ruby version management tooling from rvm to chruby to rbenv over the years in search of a solution that is stable.

Thankfully mise is polyglot and so I can now just use this for making sure that I have a version of Ruby suitable for use with fastlane.

e.g. updating my .mise.toml to

.mise.toml

[tools]
ruby = "3.3.0"
swiftlint = "0.55.0"

Then again running mise install - this time it will take longer as it builds me the version of Ruby I specify.

For best results with building Rubies I’ve found it wise to get your build environment set up as recommended here.


Any more?

I work in different languages on various Kotlin/Java code bases. Nothing is ever simple so of course those different code bases require different JVMs for which I had to use a version manager - I was using jenv. Now that I have mise I don’t need jenv anymore and my entire set up is simplified further.

In fact there is an eye watering list of tooling that mise supports which you can see here and it doesn’t look difficult to add your own plugin if you want to support different tools.


Wrap up

I’m really liking mise. There were a couple of things to figure out like the mise x command but after that and realising I can bin off my various other tools for managing versions I feel I have a much cleaner and more maintainable way to get build environments set up across the team.

assignTo

Not sure if this is a good idea or a terrible one but here we go… It’s not uncommon to find yourself in situations where you need to add little bits of glue code to handle the optionality of instance variables at the point you construct their values.

Here’s an example where I need to assign an optional instance variable and also return the non optional result from the function that is doing the creation.

class Coordinator {
    private var controller: MyViewController?

    func start() -> MyViewController {
        controller = MyViewController()
        return controller!
    }
}

In the above it’s tempting to just use the instance variable but then to fulfil the contract we end up having to force unwrap. We can improve this by creating a local let that is non optional, which can then be assigned to the optional instance variable

class Coordinator {
    private var controller: MyViewController?

    func start() -> MyViewController {
        let controller = MyViewController()
        self.controller = controller
        return controller
    }
}

This works and removes the less than desirable handling of the optionality but it introduces the risk that we might forget to assign the instance variable.

In practice you see many different slight variations of this glue code e.g.

class Coordinator {
    private var controller: MyViewController?

    func start0() -> MyViewController {
        let controller = MyViewController()
        self.controller = controller
        return controller
    }

    // Making the contract weaker by returning the optional
    func start1() -> MyViewController? {
        controller = MyViewController()
        return controller
    }

    // Being more explicit and explaining the unwrapping
    func start2() -> MyViewController {
        controller = MyViewController()

        guard let controller else {
            fatalError("We should never get here")
        }

        return controller
    }
}

Of the above I think start0 is the most desirable as it models the optionality correctly and doesn’t resort to any unwrapping, it’s just unfortunate that it’s 3 lines and you can forget the assignment.

The above are simplified examples and in real code the functions will likely be longer so this juggling of optionality could be spread out or just be less clear. This isn’t just limited to cases where you need to assign and return there are lots of times where you need to create an object, assign it to an instance variable and perform some further configuration; one common example would be

func loadView() {
    view = UIView()
    view?.backgroundColor = .red
    view?.alpha = 0.5
}

Here I’m having to handle the safe unwrapping ? multiple times. I could use if let/guard let but it starts getting wordy again.


Let’s create a helper

Here’s an idea for a helper that can get us back to a single line of code in the simplest case and help avoid the additional unwraps in the loadView case.

By adding these helpers

func assignTo<T>(_ to: inout T?, _ value: T) -> T {
    to = value
    return value
}

@_disfavoredOverload
func assignTo<T>(_ to: inout T, _ value: T) -> T {
    to = value
    return value
}

We can now simplify the original start example to

func start() -> MyViewController {
    assignTo(&controller, .init())
}

and the loadView example loses all the optional handling

func loadView() {
    let view = assignTo(&view, UIView())
    view.backgroundColor = .red
    view.alpha = 0.5
}

@_disfavoredOverload

I could be wrong but my understanding that for my usage I expect the value and return to be non optional. The to parameter could be optional or non optional depending on requirements. Without the annotation Swift will try to make all the Ts align as Optional<Wrapped> which is not what I wanted. With the annotation Swift is now clever enough to know that it should really only have one optional and not try and force all T positions to be Optional.


Wrap up

No idea if this is a good idea or not but I took some inspiration from Combine having assign(to:) and assign(to:on:) methods.


A little more

The types do have to line up which can make things less ideal e.g. in the loadView example if I needed to configure things available to a subclass of UIView then I’m back to needing to do some reassignment and typecasting e.g.

func loadView() {
    guard let view = assignTo(&view, MyView()) as? MyView else { return }
    view.title = "title"
}

You can kind of work around this by composing other helper methods - a common one in Swift projects is with which would look like this

func loadView() {
    let view = assignTo(&view, with(MyView()) { $0.title = "title" })
    registerView(view)
}

In the above it’s only inside the scope of the with trailing closure that we know it’s an instance of MyView and I had to add another function call registerView that takes the view to make it worthwhile needing to use assignTo still.