はじめに
Swift の Runtime Exception はキャッチできるのかと試した話
追記
- 2019/01/27 ライブラリ公開
やりたいこと
Swiftの例外キャッチは throw
を実装したメソッドに対して,行える.
do{
try hogehoge()
}catch{
...
}
しかし,ランタイム例外は do-catch
で囲ってもキャッチできない.
do{
let temps = [0, 1, 2]
print("\(temps[10])") // クラッシュ
// try temps[10] // Xcodeに怒られる
}catch{
...
}
上記の単純な配列indexぐらいなら事前に確認すれば問題ない.しかしながら,いくつもの処理が複雑で絡み合うシーンに出くわしたとき,サードライブラリなどで手出しができないとき,事前確認が複雑なときにそれらが起こす例外をキャッチしたいときがある.
Objective-Cはこれらの例外が取れていた.それなら,Swift内で例外をキャッチしたい処理をObjective-Cで行えばできるかなとやってみた.
本記事のコードは mitsuharu/demo_swift_catches_exceptions で公開してます.
処理をブロック化して例外をキャッチする
Objective-Cで,処理をブロックで渡して,例外が起こったときのコールバックを行うクラス ExcBlock
を用意した.
typedef void (^Block)(void);
typedef void (^Completion)( NSException *exception );
@interface ExcBlock : NSObject
+ (void)executeBlock:(Block)block completion:(Completion)completion;
@end
@implementation ExcBlock
+ (void)executeBlock:(Block)block completion:(Completion)completion
{
@try {
NSLog(@"execute block");
block();
} @catch (NSException *exception) {
NSLog(@"exception = %@", exception);
completion(exception);
} @finally {
}
}
@end
Swift で呼び出して,故意に例外を起こして確認した.しかしながら,ブロック内の例外がキャッチされることはなく,そのままクラッシュした.
ExcBlock.execute({
let temps = [0, 1, 2]
print("\(temps[10])")
// ブロックの中で起こる例外がキャッチできないまま落ちる
}) { (exception) in
print(exception)
}
この手法を調べると同様なのを見かけるので,以前のバージョンでは動いていたのかもしれない.Swiftの初期バージョン時はあまりにも破壊的アップデートが多かったので,Objective-Cに戻って開発して,Swiftの情報をあまりキャッチアップしてこなかったので,あまり詳しくない.
個別に例外をキャッチする
汎用的にSwiftのコードをブロックで渡したからキャッチできなかったと考えた.処理を丸々Objective-Cに渡してみたらどうだろうか.クラスを上書きして,例外が起こる箇所の専用メソッドを作成する.仮に UITableView の reloadRows の例外をキャッチする ExcTableView
を用意した.
typedef void (^ExcCompletion)( NSException *exception );
@interface ExcTableView : UITableView
- (void)exc_reloadRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths
withRowAnimation:(UITableViewRowAnimation)animation
excCompletion:(ExcCompletion)excCompletion;
@end
@implementation ExcTableView
- (void)exc_reloadRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths
withRowAnimation:(UITableViewRowAnimation)animation
excCompletion:(ExcCompletion)excCompletion{
@try {
[self reloadRowsAtIndexPaths:indexPaths withRowAnimation:animation];
} @catch (NSException *exception) {
excCompletion(exception);
} @finally {
}
}
@end
Swiftでは故意に例外が起こるようにようにしたところ,アプリはクラッシュせずに例外を取得できた.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let ip = IndexPath(row: 100, section: 100)
self.tableView.exc_reloadRows(at: [ip], with: UITableView.RowAnimation.automatic) { (exception) in
print("[exc_reloadRows] exception:\(exception)")
// 無事に "[exc_reloadRows] exception:attempt to delete row 100 from section 100, but there are only 1 sections before the update
" が標準出力に表示された
}
}
ライブラリを公開しました(追記, 2019/01/27)
UITableViewのreloadRowsあたりの例外をキャッチする ライブラリ を公開しました.上記のようなUITableViewを継承したクラスではなく,Extensionで生やして使いやすいようにしてます.例外周りなので使用時は注意してもらえれば.
let indexPath = IndexPath(row: 100, section: 100)
self.tableView.exc.reloadRows(at: [indexPath], with: .automatic) { (exception) in
print("exception:\(exception)")
}
まとめ
例外をキャッチしたい処理をObjective-Cに置き換えれば,Swiftでも例外をキャッチすることができた.しかしながら,個別に準備を用意しなければならないので現実的ではないです.それに,それ実質Objective-Cなのでは?とやってて思った.