TL;DR: it’s possible to handle Java-originating exceptions in Swift for j2objc-based projects. Scroll to the end for example code.
It’s getting more common to call j2objc-generated Objective-C code from Swift as iOS development shifts to this modern language. At a high level, we can imagine this means calling Java code from Swift. But Objective-C is an important link in this chain and it shapes the way Swift interacts with the code that started its life as Java.
j2objc does a great job of supporting Swift’s features when called with the --swift-friendly flag. This feature is particularly useful when the Java code is annotated with @Nonnull, @Nullable, and @ParametersAreNonnullByDefault to enforce Swift’s optionality at compile time and as you type in Xcode.
But there’s one important Java feature that gets lost in translation on the way to Swift: exceptions.
You might expect to catch Java exceptions from Swift like:
do {
// original Java code throws IllegalArgumentException
try javaObject.frobnicate(withFoo: bar)
} catch is JavaLangIllegalArgumentException {
// handle exception
}
But when you try this, you’ll notice that Xcode complains that frobnicate() doesn’t throw such an exception. And if you move frobnicate() out of the do block, you’ll notice Xcode gives no warning that it can throw an exception. But your app will happily crash when that exception is thrown!
The reason for this mismatch is largely conventional. Objective-C exceptions are typically thrown for unrecoverable errors and, thus, by design, Swift cannot catch these exceptions. All exceptions are expected to be handled within Objective-C:
there’s no safe way to recover from Objective-C exceptions in Swift. To handle Objective-C exceptions, write Objective-C code that catches exceptions before they reach any Swift code.
So, although j2objc exposes Java exceptions in the generated Objective-C code, they aren’t further exposed to Swift.
However, Objective-C does have a class and a convention that is used similarly to Java exceptions though the syntax is a bit different. In short, functions that return a BOOL and accept an NSError** as their final argument return NO and set the NSError** can be handled similarly to Java methods that throw an exception.
With a little utility code, we can automatically translate JavaLangThrowable to NSError** and thus expose our Java Throwables (including Exceptions and Errors) to Swift.
I copied the technique from this Stack Overflow post and simply adjusted it so it copes with JavaLangThrowable instead of NSException. To save any confusion, I’m inlining the resulting code below.
First, create the following files in your project:
ObjC.h
@interface ObjC : NSObject
+ (BOOL)catchThrowable:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error;
@end
@end
ObjC.m
#import "ObjC.h"
@implementation ObjC
+ (BOOL)catchThrowable:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error {
@try {
tryBlock();
return YES;
}
@catch (JavaLangThrowable *throwable) {
*error = [[NSError alloc] initWithDomain:throwable.name code:0 userInfo:throwable.userInfo];
return NO;
}
}
@end
And include the header file in your *-Bridging-Header.h:
@implementation ObjC
+ (BOOL)catchThrowable:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error {
@try {
tryBlock();
return YES;
}
@catch (JavaLangThrowable *throwable) {
*error = [[NSError alloc] initWithDomain:throwable.name code:0 userInfo:throwable.userInfo];
return NO;
}
}
@end
And include the header file in your *-Bridging-Header.h:
#import "ObjC.h"
Now, you can handle exceptions in your Swift code like this:
Now, you can handle exceptions in your Swift code like this:
do {
try ObjC.catchThrowable {
RecipeController().thrower()
}
} catch {
let desc = error.localizedDescription
if desc.contains("IllegalArgumentException") {
print("handle IllegalArgumentException here")
} else if desc.contains("ParseException") {
print("handle ParseException here")
} else {
print("handle all other exceptions here")
}
}
Comments