Objective-C からの見え方を意識した Swift の Error

TL; DR

Objective-C からも利用できる Swift コードを書く場合、

  • throws キーワード付きのメソッドは戻り値の型に制限がある
  • 引数や戻り値にエラーを指定する際はカスタムエラー型ではなく Swift.Error 型を使う

の 2 点に気をつけたほうがよいです。

Objective-C → Swift の連携で困った

Swift でライブラリを書いていて、 Objective-C から呼ぶときのエラーの扱われ方にハマってしまいました。

この記事によると、 Swift のエラーを Objective-C でもハンドリングできるようにするには 列挙型の raw value に Int を指定したカスタムエラーを定義すると良いようです。 qiita.com

しかし、それだけではうまくいかないケースがあったので、もう少し実験してみました。

準備

参考記事と同じように、 raw value に Int を指定したカスタムエラーを定義しておきます。

@objc public enum CustomError: Int, Swift.Error {
    case error1 = 1
    case error2 = 2
}

この Swift コードをビルドすると、以下のような Objective-C コードが生成されます。

typedef SWIFT_ENUM(NSInteger, CustomError) {
  CustomErrorError1 = 1,
  CustomErrorError2 = 2,
};
static NSString * _Nonnull const CustomErrorDomain = @"MySwiftFramework.CustomError";

ケース 1: throws キーワード

Swift ライブラリに、以下のように throws キーワードの付いたメソッドを作成します。

@objc public func throwError() throws {
    throw CustomError.error1
}

Objective-C アプリからはこのように見えます。

- (BOOL)throwErrorAndReturnError:(NSError * _Nullable * _Nullable)error;

引数に out パラメータの NSError が追加され、戻り値が BOOL 型になりました。 メソッド名も ~AndReturnError と変化しています。

このとき、ライブラリ側の処理が成功すると引数 error は nil@YES が返り、 エラーが発生すると引数 error にエラーオブジェクトが渡され @NO が返ります。

ケース 2: 戻り値ありの throws キーワード

戻り値があり throws キーワードが付いたメソッドの場合、戻り値の型に制限がつきます。

@objc public func throwsWithReturnValue() throws -> Data {
    throw CustomError.error1
}

Objective-C アプリからの見え方はこうなります。

- (NSData * _Nullable)throwsWithReturnValueAndReturnError:(NSError * _Nullable * _Nullable)error;

Swift 側の戻り値が NonNull Data 型なのに対し、 Objective-C では Nullable NSData 型に変化しています。 このとき、ライブラリ側の処理が成功すると NSData オブジェクトが返り、エラーが発生すると nil が返ります。

nil を返すことで処理の成否を表現するため、 Swift 側の戻り値を Optional 型やプリミティブ型にすることはできません。

f:id:takasfz:20180702200132p:plain

nilObjective-C で失敗を表すので戻り値に optional 型は指定できないよ

f:id:takasfz:20180702200150p:plain

戻り値は Void か Objective-C のクラスにブリッジされる型しか指定できないよ

ケース 3: 戻り値に Error

throws キーワードではなく、戻り値の型を Error とした場合、 Objective-C アプリからの見え方は Swift と変わりません。

@objc public func returnError() -> Error {
    return CustomError.error1
}
- (NSError * _Nonnull)returnError;

Swift 側で CustomError を返すと、 Objective-C 側では適切な domain / code が設定された NSError オブジェクトが返ります。

ケース 4: 戻り値に CustomError

戻り値を CustomError 型とした場合、 Objective-C では戻り値が NSError ではなく列挙型の CustomError になりました。

@objc public func returnCustomError() -> CustomError {
    return CustomError.error1
}
- (enum CustomError)returnCustomError;

CustomError はただの NSInteger な列挙型なので、 NSError オブジェクトのように扱うことはできません。

ケース 5: 引数に Error

クロージャ等で引数を Error 型とする場合、 Objective-C アプリからの見え方は Swift と変わりません。

@objc public func passError(to closure: (Error) -> Void) {
    closure(CustomError.error1)
}
- (void)passErrorTo:(SWIFT_NOESCAPE void (^ _Nonnull)(NSError * _Nonnull))closure;

Swift 側で引数に CustomError を渡すと、ケース 3 と同じく Objective-C 側では適切な domain / code が設定された NSError オブジェクトが返ります。

ケース 6: 引数に CustomError

引数を CustomError 型とした場合、ケース 4 と同じく Objective-C では引数が NSError ではなく列挙型の CustomError になりました。

@objc public func passCustomError(to closure: (CustomError) -> Void) {
    closure(CustomError.error1)
}
- (void)passCustomErrorTo:(SWIFT_NOESCAPE void (^ _Nonnull)(enum CustomError))closure;

この場合、 Objective-C 側では CustomError を NSError オブジェクトのように扱うことはできません。

まとめ

Objective-C からも利用できる Swift コードを書く場合、 throws キーワード付きのメソッドは戻り値の型に制限があることに注意する必要があります。 また引数や戻り値にエラーを指定する際は、特定のカスタムエラー型しか返さない場合でも Swift.Error 型を指定するほうがよいでしょう。


出典: - Swift3時代のErrorとNSErrorに関するいくつかの実験