NSExceptionをSwiftでtry-catchする


KVOでaddObserverしたものをremoveObserverするときなどに、addした以上にremoveを呼ぶとExceptionが発生します。
しかし、発生したExceptionはSwiftで直接catchすることはできません。

以下のようなObjective-Cのラッパーを使用することで、そのExceptionをcatchすることができるようになります。

ExceptionHandler.m
#import "ExceptionHandler.h"

@implementation ExceptionHandler

+ (BOOL)catchExceptionWithTryBlock:(__attribute__((noescape)) void(^ _Nonnull)())tryBlock error:(NSError * _Nullable __autoreleasing * _Nullable)error {
    @try {
        tryBlock();
        return YES;
    } @catch (NSException *exception) {
        *error = [NSError errorWithDomain:exception.name code:-9999 userInfo:exception.userInfo];
        return NO;
    }
}

@end

上記のように引数がNSError **で返り値がBOOLなメソッドは、Swift側で自動的にthrowableなメソッドとして変換されるようです。

ExceptionCatchable.swift
protocol ExceptionCatchable {}

extension ExceptionCatchable {
    func execute(_ tryBlock: () -> ()) throws {
        try ExceptionHandler.catchException(try: tryBlock)
    }
}

Protocolとして継承したクラスでのみ使いたかったので、デフォルト実装をしてもう一度throwします。

ViewController.swift
class DataSource: NSObject {
    dynamic var value: String = ""
}

class ViewController: UIViewController, ExceptionCatchable {

    let dataSource = DataSource()

    deinit {
        do {
            try execute {
                dataSource.removeObserver(self, forKeyPath: "value")
                dataSource.removeObserver(self, forKeyPath: "value")
            }
        } catch let e {
            print("\n", e, "\n")
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        dataSource.addObserver(self, forKeyPath: "value", options: .new, context: nil)
    }
}

上記のようにすることで、executeメソッドにExceptionが起こるであろう処理のClosureを渡して、try-catchできるようになります。

KVOのremoveの場合だけでなく、握りつぶしたいExceptionの場合にも使用可能です。