Slowly migrating from Objective-C to Swift
28 May 2024I 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 extension
s 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.