エラーの扱いについて
Swift では Error 型と NSError 型を使うことができる。Error 型は SwiftUI で使われる一般的なエラー型ではあるがエラーコードがなかったりとかゆいところに手が届かなかったりする。
ここでは独自の Error 型を定義し、それを柔軟に使っていくためのチュートリアルを解説する。
独自のエラー型を定義しよう
エラーの定義である Enum は Error を継承することはもちろん、ついでに CaseIterable を継承しておくと良い。
今回はアプリが「不明」「期限切れ」「空」「無効」の四パターンのエラーを返すものを想定した。
enum APPError: Error, CaseIterable {
case unknown
case expired
case empty
case invalid
}
それは 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
}
}
}
エラー詳細
エラー詳細は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"
}
}
}
これで独自に定義したエラーに対してエラーコードとエラー詳細を設定することができた。
エラーの呼び出し
SwiftUI においてはthrow ERROR
とすることでエラーを呼び出すことができる。これは普通の return などと違い、放置すればクラッシュするのでtry?
でエラーをなかったことにするかdo catch
で適切にハンドリングする必要がある。
エラーが呼び出される関数には必ず呼び出される可能性があることを明示しなければならない。
// OK
func throwError() throws -> () {
throw (APPError.allCases.randomElement() ?? APPError.unknown)
}
例えばこれは定義された APPError 型から適当に一つ選んでエラーを発生させるコードである。randomElement()
が nil を返す場合があるのでその場合にはとりあえず不明なエラーを返すようにした。
ここでのthrows
(throw ではない)はエラーが発生したときにエラーハンドリングをせずにこの関数を呼び出した関数に「エラー自体」を伝達することを意味する。
なぜならこの関数はdo catch
でエラーハンドリングをしていないにもかかわらず関数内にthrow
があるためにエラーを発生させる可能性があるためである。エラーを発生させる可能性(throw
)があるが、do catch
がない関数には必ずthrows
でエラーを投げる可能性があることを明示しなければならないのだ。
// NG
func throwError() -> () {
throw (APPError.allCases.randomElement() ?? APPError.unknown)
}
なのでこのようにthrows
がない関数はコンパイルエラーが発生する。
// OK
func throwError() -> () {
do {
throw (APPError.allCases.randomElement() ?? APPError.unknown)
} catch {
print("ERROR")
}
}
このようにdo catch
を使ってエラーハンドリングをし、関数からエラーが投げられないようにすればthrows
を書かなくて済む。ただ、これだとエラーが発生したときに ERROR という文字列が表示されるだけで、これではエラーハンドリングとは言えない。
エラー処理をする
まず、エラーの中身を見たときはこのように書けば良い。多くのプログラミング言語ではcatch
で error が定義されている。Swift の場合もそうなので定義しなくてもerror
という変数でエラーの内容をとってくることができる。
do {
try throwError()
} catch {
print(error)
}
もしも独自の変数名を与えたい場合は次のようにかけば良い。
do {
try throwError()
} catch(let e) {
print(e)
}
throwError()
は APPError 型を返すのだが、実際にどんな値を受け取っているのか見てみるとempty
やinvalid
という値が返っていていた。
つまり、受け取っているのはただの Enum だということだ。
do {
try throwError()
} catch {
print(error.localizedDescription)
}
では肝心の中身を見る話だかこれはerror.errorCode
やerror.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)
}
つまり、上のように 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)))
}
}
これは単にエラーが発生したら表示するだけなので再利用するのは簡単である。ViewModifier の中身を変えれば自由にカスタマイズすることもできる。