Skip to main content
  1. Posts/

NSExpressionのexpressionValueで発生するNSInvalidArgumentExceptionをSwiftでも捕捉する方法

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で出力したものです。 実装したいロジックを伝えるだけで、十分使用できる実装を生成してくれたので、驚いてしまいました。