NSExpressionのexpressionValueで発生するNSInvalidArgumentExceptionをSwiftでも捕捉する方法
Table of Contents
NSExpressionのexpressionValue(with:context:)
をSwiftで呼び出す際に、Expressionが不正な内容になっていた場合、NSInvalidArgumentException
が発生します。しかし、このメソッドはthrows
として定義されていないため、Swiftでエラーを補足することができません。エラーが発生した場合はクラッシュし、そのままアプリが終了してしまいます。これを解決するため、expressionValue
をラップするメソッドをObjective-Cで実装し、それをSwift側から呼び出す実装をしました。
なお、このポストでは、OpenAIのChatGPTで生成したコードを、一部手作業で書き換えたものを使用しています。
問題の実装 #
問題が発生した実装は以下のとおりです。
private func calculateExpression(_ expression: String) -> Double? {
let expressionWithoutSpaces = expression.replacingOccurrences(of: " ", with: "")
let expressionToEvaluate = NSExpression(format: expressionWithoutSpaces)
return expressionToEvaluate.expressionValue(with: nil, context: nil) as? Double // NSInvalidArgumentException
}
let expression = "140 - 3..3" // 問題のある表現
calculateExpression(expression) // クラッシュ
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFNumber componentsSeparatedByString:]: unrecognized selector sent to instance 0x60000314ad00'
このようにエラーが発生し、クラッシュしてしまいます。
Appleのドキュメントを確認しても、 expressionValue(with:context:)
はthrowsになっていません。
expressionValue(with:context:) | Apple Developer Documentation
func expressionValue(
with object: Any?,
context: NSMutableDictionary?
) -> Any?
そこで、Objective-Cでラッパーを実装し、エラーを捕捉できるようにしました。
Objective-Cでラッパーを実装する #
実装したObjective-Cは、以下のとおりです。
MyExpression.h
ファイル。(本来ならプレフィックスをつける方が理想)
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface MyExpression : NSObject
- (nullable NSNumber *)evaluate:(NSExpression *)expression error:(NSError * _Nullable *)error;
@end
NS_ASSUME_NONNULL_END
MyExpression.m
ファイル。
#import "MyExpression.h"
@implementation MyExpression
- (nullable NSNumber *)evaluate:(NSExpression *)expression error:(NSError * _Nullable *)error {
@try {
id result = [expression expressionValueWithObject:nil context:nil];
if ([result isKindOfClass:[NSNumber class]]) {
return (NSNumber *)result;
} else {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: @"Invalid result type",
NSLocalizedFailureReasonErrorKey: @"The evaluated result is not a number"
};
*error = [NSError errorWithDomain:@"com.example.MyExpressionErrorDomain" code:1 userInfo:errorInfo];
return nil;
}
} @catch (NSException *exception) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: @"Evaluation error",
NSLocalizedFailureReasonErrorKey: exception.reason
};
*error = [NSError errorWithDomain:@"com.example.MyExpressionErrorDomain" code:2 userInfo:errorInfo];
return nil;
}
}
@end
ここまでできれば、あとはBridging-Headerに #import MyExpression.h
を記載するだけです。
※ アプリのプロジェクトがSwift100%で書いていた場合は、初めてXcodeでObjective-Cファイルを作成するときに、Bridging-Headerファイルを生成するかどうかを問うダイアログが表示されます。
修正されたSwift側の実装 #
Objective-Cで実装したラッパーを使用して、Swift側の問題のコードを修正したものがこちらです。
private func calculateExpression(_ expression: String) -> Double? {
let expressionWithoutSpaces = expression.replacingOccurrences(of: " ", with: "")
let expressionToEvaluate = NSExpression(format: expressionWithoutSpaces)
let wrapper = MyExpression()
do {
let result = try wrapper.evaluate(expressionToEvaluate) as? Double
// tryできるので、NSInvalidArgumentExceptionが発生しても捕捉できる
return result
} catch {
print(error)
return 0
}
}
これで、NSExpressionのexpressionValue(with:context:)
で発生するエラーを捕捉できるようになりました。
アプリもクラッシュしなくなったので、安心です。
この投稿で使用しているコードについて #
この投稿で使用しているコードは、OpenAIのChatGPTで生成したものを、一部手作業で変更したものを使用しています。 変更した部分はObjective-Cのファイル名やクラス名、Swift側の実装のみで、大部分はChatGPTで出力したものです。 実装したいロジックを伝えるだけで、十分使用できる実装を生成してくれたので、驚いてしまいました。