炊きたてのご飯が食べたい

定時に帰れるっていいね。自宅勤務できるっていいね。子どもと炊きたてのご飯が食べられる。アクトインディでは積極的にエンジニアを募集中です。

Swift2 - Alamofire のリクエストでネットワークエラーを検知する


swift2.2, Xcode7

API 通信するアプリを開発していると、ネットワークエラーを検知して、アラートを出すといった処理は共通して書きたいものです。

Apple がサンプルとして公開している Reachability を Swift に変換して使うのもアリですが、ネットワーク通信の ON / OFF だけをサクッと検知したいのであれば Alamofire の NSError を確認することで、簡単に判別することができます。

  1. ネットワークエラーを Alamofire の error( NSError 型 ) から補足する
  2. アラートにそのまま表示できるように NSError を enum( NSErrorType ) で生成する
  3. Alamofire の error を NSErrorType に変換する
  4. NSError をアラート表示する

1. ネットワークエラーを Alamofire の error( NSError 型 ) から補足する

インターネット接続が OFF の状態で Alamofire のリクエストを投げると error.code が NSURLErrorNotConnectedToInternet となるので

if error.code == NSURLErrorNotConnectedToInternet {
    // インターネット接続 OFF
}

で、検知できます。

2. アラートにそのまま表示できるように NSError を enum で生成する

インタネット接続 OFF の時に画面にアラートを表示する為、 NSError を作成する enum を準備します。

enum NSErrorType {
    case System
    case NetworkOff

    var descriptionKey: String {
        switch self {
        case .System:
            return "エラー"
        case .NetworkOff:
            return "ネットワークに接続できません"
        }
    }

    var recoverySuggestionErrorKey: String {
        switch self {
        case .System, .NetworkOff:
            return "接続環境の良いところで再度お試しください"
        }
    }

    var error: NSError {
        let errorInfo = [ NSLocalizedDescriptionKey: self.descriptionKey, NSLocalizedRecoverySuggestionErrorKey: self.recoverySuggestionErrorKey ]
        let error = NSError(domain: "com.example.com", code: self.code, userInfo: errorInfo)
        return error
    }
}

3. Alamofire の error を NSErrorType に変換する

前回の記事で作成した API クライアントを拡張します。API クライアントは以下のようになっています。

import Alamofire

class API {
    class func call<T: RequestProtocol, V where T.ResponseType == V>(request: T, completion: (Result<V, NSError>) -> Void) {
        Alamofire.request(request)
            .validate(contentType: ["application/json"])
            .responseJSON { response in
                switch response.result {
                case .Success(let json):
                    completion(request.fromJson(json))
                case .Failure(let error):
                    completion(.Failure(error))
                }
        }
    }
}

今回はリクエスト失敗時の処理なので .Failure を以下のように書き換えます。

case .Failure(let error):
    completion(.Failure(request.fromNSError(error)))
}

そして、RequestProtocol.swift に fromNSError メソッドを加えます。

protocol RequestProtocol: URLRequestConvertible {
    associatedtype ResponseType

    ... 省略    
    
    func fromJson(json: AnyObject) -> Result<ResponseType, NSError>
    func fromNSError(error: NSError) -> NSError
}

extension RequestProtocol {
    var method: Alamofire.Method {
        return .GET
    }

    ... 省略
    
    func fromJson(json: AnyObject) -> Result<ResponseType, NSError> {
        guard let value = json as? ResponseType else {
            let errorInfo = [NSLocalizedDescriptionKey: "Convert object failed" , NSLocalizedRecoverySuggestionErrorKey: "Good luck!"]
            let error = NSError(domain: "com.example.app", code: 0, userInfo: errorInfo)
            return .Failure(error)
        }
        return .Success(value)
    }

    func fromNSError(error: NSError) -> NSError {
        if error.code == NSURLErrorNotConnectedToInternet {
            return NSErrorType.NetworkOff.error
        } else {
            return NSErrorType.System.error
        }
    }
}

これで、ネットワークオフの時は NSErrorType.NetworkOff.error が、その他のエラーの時は NSErrorType.System.error が返却されるようになりました。

4. NSError をアラート表示する

色んなところで使い回せるように NSError を渡したら Alert を表示する Utility を作っておきます。

struct AlertUtility {
    static func showErrorAlert(error: NSError?, viewController: UIViewController){
        guard let error = error else {
            return
        }
        let title: String = error.localizedDescription
        let message: String = error.localizedRecoverySuggestion ?? "接続環境の良いところで再度お試しください"
        self.showErrorAlert(title, message: message, viewController: viewController)
    }    
}

あとは API.call の .Failure の error をそのまま表示してあげるだけで、ネットワークオフの時のエラーを表示できるようになります。本来は ViewController で API.call するのは良くないですが、今回は説明を簡単にする為に ViewController で呼んでいます。

ViewController.swift

override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated) 

    API.call(Endpoint.User.Find(id: 1)) { response in
        switch response {
        case .Success(let result):
            print("success \(result)")
        case .Failure(let error):
            AlertUtility.showErrorAlert(error, viewController: self)
        }
    }
}