Swift の Runtime Exception はキャッチできるのかと試したこと

# はじめに

Swift の Runtime Exception はキャッチできるのかと試した話

## 追記

- 2019/01/27 ライブラリ公開

# やりたいこと

Swiftの例外キャッチは ```throw``` を実装したメソッドに対して，行える．

```Swift
do{
	try hogehoge()
}catch{
	...
}
```

しかし，ランタイム例外は ```do-catch``` で囲ってもキャッチできない．

```Swift
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](https://github.com/mitsuharu/demo_swift_catches_exceptions) で公開してます．


# 処理をブロック化して例外をキャッチする

Objective-Cで，処理をブロックで渡して，例外が起こったときのコールバックを行うクラス ```ExcBlock``` を用意した．

```Objective-C:ExcBlock.h
typedef void (^Block)(void);
typedef void (^Completion)( NSException *exception );

@interface ExcBlock : NSObject

+ (void)executeBlock:(Block)block completion:(Completion)completion;

@end
```

```Objective-C:ExcBlock.m
@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 で呼び出して，故意に例外を起こして確認した．しかしながら，ブロック内の例外がキャッチされることはなく，そのままクラッシュした．

```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``` を用意した．

```Objective-C:ExcTableView.h
typedef void (^ExcCompletion)( NSException *exception );

@interface ExcTableView : UITableView

- (void)exc_reloadRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths
                  withRowAnimation:(UITableViewRowAnimation)animation
                     excCompletion:(ExcCompletion)excCompletion;

@end
```

```Objective-C:ExcTableView.m
@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では故意に例外が起こるようにようにしたところ，アプリはクラッシュせずに例外を取得できた．

```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あたりの例外をキャッチする [ライブラリ](https://github.com/mitsuharu/MEOException) を公開しました．上記のようなUITableViewを継承したクラスではなく，Extensionで生やして使いやすいようにしてます．例外周りなので使用時は注意してもらえれば．

```Swift:ライブラリの使用例
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なのでは？とやってて思った．