Dangerous Objective C to Swift Conversions
March 26, 2023 -I have a pretty high confidence converting Java to Kotlin. One of the reasons is because Android Studio has a tool to show the resulting bytecode (which then can be decompiled back to Java).
But not so much when converting Objective C file to Swift. There were some unexpected behaviors that I try to list here. You can also visit this Github playground that contains this page's snippets. Hopefully I can update this list as I gain new knowledge for the older me.
Null parameters
In Kotlin, when a null value is passed to a method that accepts non-null value, the method will crash at runtime. This may happen when you are dealing with platform type (type with unknown nullability) because Kotlin will put assertion at the start of the method.
// Decompiled Java code
public final void doSomething(@NotNull Foo foo) {
Intrinsics.checkNotNullParameter(foo, "foo");
In Swift, it depends on whether the caller is Swift or Objective C. Let's say we have some legacy Objective C classes. In real world this class is usually so complicated that it contains a lot of properties and mixes a lot of concerns (persistence, networking).
@interface ComplexObject : NSObject
@property (readonly, nonatomic) int number;
@end
@interface SomeObjcClass : NSObject
@property (readonly, nonatomic) NSString *text;
@property (readonly, nonatomic) NSNumber *number;
@property (readonly, nonatomic) ComplexObject *complex;
And then as developers, we created a new class and method that's written in shiny Swift. It's just printing the value of the parameters.
@objc
public final class SomeSwiftClass: NSObject {
@objc
public static func processNonNull(text: String, number: NSNumber, complex: ComplexObject) -> String {
"\(text);\(String(describing: number)):\(number.stringValue);\(String(describing: complex)):\(String(complex.number))"
}
Now what happens if we call it from Objective C and Swift?
+ (NSString *)passNilToSwift {
SomeObjcClass *me = [SomeObjcClass new];
// This returns ";:;:0"
return [SomeSwiftClass processNonNullWithText:me.text number:me.number complex:me.complex];
}
let someObjcClass = SomeObjcClass()
// This crashes.
SomeSwiftClass.processNonNull(
text: someObjcClass.text,
number: someObjcClass.number,
complex: someObjcClass.complex
)
Swift | Objective C |
---|---|
Runtime crash | No crash |
When called from Objective C, the usage won't crash if the argument type can be initialized with an empty initializer.
The actual argument inside SomeSwiftClass.processNonNull
will point to an object with address 0x0
(null object
pattern).
That's why even String(describing:)
will not even print nil
. More explanation can be found
in this StackOverflow thread. It will crash if we add NSURL
property and pass it
to SomeSwiftClass.processNonNull
.
@interface SomeObjcClass : NSObject
…
@property (readonly, nonatomic) NSURL *url;
Number conversion
Let's say we have this Objective C method we want to convert to Swift. This method just converts second to milliseconds.
- (NSNumber*)convertTimeInterval:(NSTimeInterval)timeInterval {
return @((NSUInteger)(timeInterval * 1000));
}
We can convert it to Swift even with our eyes closed, right?
func convertTimeInterval(_ timeInterval: TimeInterval) -> NSNumber {
NSNumber(value: (UInt)(timeInterval * 1000))
}
Umm sorry, nope. Life is not that easy.
let objc = SomeObjcClass()
XCTAssertEqual(NSNumber(value: 1000), objc.convertTimeInterval(TimeInterval(1)))
XCTAssertEqual(NSNumber(value: 0), objc.convertTimeInterval(TimeInterval.nan))
let uint: UInt = 18446744073709551615
XCTAssertEqual(NSNumber(value: uint), objc.convertTimeInterval(TimeInterval.infinity))
let swift = SomeSwiftClass()
XCTAssertEqual(NSNumber(value: 1000), swift.convertTimeInterval(TimeInterval(1)))
// Below crashes.
// XCTAssertEqual(NSNumber(value: 0), swift.convertTimeInterval(TimeInterval.nan))
// XCTAssertEqual(NSNumber(value: uint), swift.convertTimeInterval(TimeInterval.infinity))
Summary
Converting to Swift will cause some bugs to come out. Be prepared!