エラーの扱いについて

Swift では Error 型と NSError 型を使うことができる。Error 型は SwiftUI で使われる一般的なエラー型ではあるがエラーコードがなかったりとかゆいところに手が届かなかったりする。

ここでは独自の Error 型を定義し、それを柔軟に使っていくためのチュートリアルを解説する。

独自のエラー型を定義しよう

エラーの定義である Enum は Error を継承することはもちろん、ついでに CaseIterable を継承しておくと良い。

今回はアプリが「不明」「期限切れ」「空」「無効」の四パターンのエラーを返すものを想定した。

enum APPError: Error, CaseIterable {
    case unknown
    case expired
    case empty
    case invalid
}
1
2
3
4
5
6

それは Enum を使ってこのように書けるが、これだけだと意味がないので、この APPError に対してエラーの詳細やエラーコードを割り当てていく。

エラーコード

エラーコードは CustomNSError を継承すれば定義することができる。

extension APPError: CustomNSError {
    var errorCode: Int {
        switch self {
        case .unknown:
            return 9999
        case .expired:
            return 10000
        case .empty:
            return 2000
        case .invalid:
            return 3000
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

エラー詳細

エラー詳細はerrorDescriptionというメンバ変数に割り当てる。これはLocalizedErrorを継承すれば String?型で定義することができる。

extension APPError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .unknown:
            return "ERROR_UNKNOWN"
        case .expired:
            return "ERROR_EXPIRED"
        case .empty:
            return "ERROR_EMPTY"
        case .invalid:
            return "ERROR_INVALID"
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

これで独自に定義したエラーに対してエラーコードとエラー詳細を設定することができた。

エラーの呼び出し

SwiftUI においてはthrow ERRORとすることでエラーを呼び出すことができる。これは普通の return などと違い、放置すればクラッシュするのでtry?でエラーをなかったことにするかdo catchで適切にハンドリングする必要がある。

エラーが呼び出される関数には必ず呼び出される可能性があることを明示しなければならない。

// OK
func throwError() throws -> () {
    throw (APPError.allCases.randomElement() ?? APPError.unknown)
}
1
2
3
4

例えばこれは定義された APPError 型から適当に一つ選んでエラーを発生させるコードである。randomElement()が nil を返す場合があるのでその場合にはとりあえず不明なエラーを返すようにした。

ここでのthrows(throw ではない)はエラーが発生したときにエラーハンドリングをせずにこの関数を呼び出した関数に「エラー自体」を伝達することを意味する。

なぜならこの関数はdo catchでエラーハンドリングをしていないにもかかわらず関数内にthrowがあるためにエラーを発生させる可能性があるためである。エラーを発生させる可能性(throw)があるが、do catchがない関数には必ずthrowsでエラーを投げる可能性があることを明示しなければならないのだ。

// NG
func throwError() -> () {
    throw (APPError.allCases.randomElement() ?? APPError.unknown)
}
1
2
3
4

なのでこのようにthrowsがない関数はコンパイルエラーが発生する。

// OK
func throwError() -> () {
    do {
        throw (APPError.allCases.randomElement() ?? APPError.unknown)
    } catch {
        print("ERROR")
    }
}
1
2
3
4
5
6
7
8

このようにdo catchを使ってエラーハンドリングをし、関数からエラーが投げられないようにすればthrowsを書かなくて済む。ただ、これだとエラーが発生したときに ERROR という文字列が表示されるだけで、これではエラーハンドリングとは言えない。

エラー処理をする

まず、エラーの中身を見たときはこのように書けば良い。多くのプログラミング言語ではcatchで error が定義されている。Swift の場合もそうなので定義しなくてもerrorという変数でエラーの内容をとってくることができる。

do {
    try throwError()
} catch {
    print(error)
}
1
2
3
4
5

もしも独自の変数名を与えたい場合は次のようにかけば良い。

do {
    try throwError()
} catch(let e) {
    print(e)
}
1
2
3
4
5

throwError()は APPError 型を返すのだが、実際にどんな値を受け取っているのか見てみるとemptyinvalidという値が返っていていた。

つまり、受け取っているのはただの Enum だということだ。

do {
    try throwError()
} catch {
    print(error.localizedDescription)
}
1
2
3
4
5

では肝心の中身を見る話だかこれはerror.errorCodeerror.errorDescriptionのように受け取ることができない。SwiftUI で受け取ることができるのはあくまでも Error 型であり、Error 型はlocalizedDescripionというメンバ変数しか持たないためだ。

ただ、localizedDescriptionを表示するとerrorDescriptionの値を表示することはできた。問題はerrorCodeをどうやって受け取るかである。

Swift で使えるエラーにはError, NSError, CustomNSErrorなどがあるが、今回のケースではエラーコードを利用するためにCustomNSErrorを継承しているのでこれを利用する。

do {
    try throwError()
} catch {
    let customNSError = error as? CustomNSError
    print(error.errorCode)
}
1
2
3
4
5
6

つまり、上のように CustomNSError にキャストすることでエラーコードを表示することができるようになる。

アラートでエラー発生

エラーが発生したときにそれを検知してアラートを表示したいケースが多いが、そのたびに何度も Alert の定義を書くのはめんどくさいのでエラーが発生しそうなところに使える ViewModifier を定義した。

struct AlertView: ViewModifier {
    @Binding var isPresented: Bool
    let error: CustomNSError
    func body(content: Content) -> some View {
        content
            .alert(isPresented: $isPresented) {
                Alert(title: Text("ERROR"), message: Text(error.localizedDescription), dismissButton: nil)
            }
    }
}
extension View {
    func alert(isPresented: Binding<Bool>, error: CustomNSError?) -> some View {
        guard let error = error else { return AnyView(self) }
        return AnyView(self.modifier(AlertView(isPresented: isPresented, error: error)))
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

これは単にエラーが発生したら表示するだけなので再利用するのは簡単である。ViewModifier の中身を変えれば自由にカスタマイズすることもできる。