[Swift] HTTP通信OSS Alamofire 応用編

2014.11.26

今回の記事の前哨戦とも言える導入編では、Alamofireの既存プロジェクトへの追加、request、response、download、upload、validateなどの関数やメソッド、デバッガでの表示などを解説しました。

本記事では応用編と題して、Manager、Request クラスの込み入った使い方、URLRequestConvertible プロトコルの使い方、レスポンスのカスタムシリアライズ機能を紹介していきます。

Manager クラス

Manager クラスは HTTP リクエストを管理します。内部的には NSURLSession を用いてリクエストやダウンロード、アップロード等を行います。

クラスには通信を行うための request, download, upload 等のメソッドが用意されています。導入編では request, download, upload 等のトップレベル関数について言及しましたが、これらの関数の機能は内部的には Manager クラスのシングルトンインスタンスのメソッドを呼び出す事で実現されています。導入編で紹介した関数群と同様に、これらのメソッド群も HTTP 通信を行う役割と同時に、Request クラスのインスタンス生成の役割を担っています。

Manager クラスのイニシャライズ

Manager クラスのシングルトンインスタンスは sharedInstance タイププロパティで得られます。これは補助的なシングルトンインスタンスで、独自の通信設定等を行いたい場合は、別途 Manager クラスを NSURLSessionConfigulation とともに次のイニシャライザを用いて生成します。

public init(configuration: NSURLSessionConfiguration? = nil)

NSURLSessionConfigulation クラスは NSURLSession を用いたデータ送受信の設定を行います。クッキーを使うかどうかの設定もこのクラスで行います。

例1 -Manager インスタンスの生成-

let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
let manager = Alamofire.Manager(configuration: configuration)

ここではデフォルトの SessionConfigulation を用いて Manager インスタンスを生成しています。

sharedInstance タイププロパティで取得できるインスタンスについては、このデフォルトの SessionConfigulation を設定した後に、Accept-Encoding ヘッダについては gzip;q=1.0,compress;q=0.5 を指定し、デバイスの言語設定やOSのバージョンに応じて、Accept-Language ヘッダと User-Agent ヘッダが指定されています。

API毎にデータ送受信を詳細に設定したい場面では、内部で sharedInstance を用いている request, download, upload 関数を使うのではなく、API毎に個別の設定を行った Manager インスタンスを生成して、request メソッドを呼ぶようにします。

startRequestsImmediately プロパティ

Manager クラスに定義された startRequestsImmediately プロパティは Manager クラスの request, download, upload メソッドを呼んだ際に HTTP通信を直ちに行うかどうかを決定できます。Manager クラスに定義された request, donload, upload メソッドを呼ぶ場合、デフォルトでは HTTP 通信がすぐに行われますが、これらのメソッドを呼んで Request クラスのインスタンスを生成するだけにしておきたい場合には、メソッドを呼ぶ前に Manager クラスの startRequestsImmediately プロパティに false を代入します。

例2 -HTTP通信イベントを発火しない Request インスタンスの生成-

let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
let manager = Alamofire.Manager(configuration: configuration)
manager.startRequestsImmediately = false
let request = 
manager.request(.GET, 
                "http://localhost.jp/user", 
                parameters: ["id" : "1"])

ここではユーザ取得APIに対する Request クラスのインスタンスを生成しています。startRequestsImmediately プロパティになにも設定しない場合はメソッドコール時点で HTTP通信が走りますが、この例では request メソッド呼び出しの前に false を設定しているため、通信が走らず、通信を走らせるためには別途 request に対して resume() メソッド(Request クラスで解説します)をコールします。

session プロパティ

session プロパティは Manager クラスが HTTP 通信を走らせる際に用いる NSURLSession インスタンスです。let で定数プロパティとして宣言されているため、プロパティ自体の置き換えはできませんが、参照型のクラスであるために、session プロパティに紐付くプロパティは変更できます。

Request クラス

Request クラスは HTTP 通信のリクエストを扱うクラスです。

task, session プロパティ

task, session はそれぞれ Request クラスの内部で用いられる NSURLSessionTask, NSURLSession を表しています。

task プロパティは取得のみが可能な変数プロパティとして定義されています。

また session プロパティは Manager クラスの session プロパティと同様に定数として宣言されています。

request, response プロパティ

request プロパティはサーバーに送られる、もしくは送られる予定の NSURLRequest インスタンスです。

response プロパティは NSHTTPURLResponse? 型の取得のみが可能なプロパティで、Resuest クラスに対するレスポンスが返って来ているかどうかに応じてオプショナルの中身の値が入っているかどうかが変わります。

authenticate メソッド

リクエストに認証情報を予め仕込む際には authenticate メソッドを用います。

public func authenticate(#user: String, password: String) -> Self // ベーシック認証用
public func authenticate(usingCredential credential: NSURLCredential) -> Self // NSURLCredentialを用いた認証用

始めのメソッドはベーシック認証に必要な ID, Pass をリクエストに仕込みます。 2つめのメソッドは NSURLCredential を用いて認証を実施します。このクラスの役割に関する説明はURLローディングシステムプログラミングガイドに詳しいです。

例3 -authenticate メソッドを用いた Basic 認証-

let identifier = "yourname"
let password = "yourpassword"

Alamofire.request(.GET, "https://localhost.jp/basic-auth/")
         .authenticate(user: identifier, password: password)
         .response {(request, response, _, error) in
             println(response)
         }

ここではユーザID yourname とパスワード yourpassword を指定してベーシック認証を指定しつつリクエストをかけています。 authenticate メソッドの呼び出しだけでリクエストに認証情報が入ります。

suspend, resume, cancel メソッド

suspend メソッドは Request クラスに対してデータローディング処理の一時停止をするように要請します。メソッドの呼ばれた Request クラスは task プロパティに対して suspend メソッドを呼び、処理は一時停止されます。

停止されたローディング処理を再開したい場合は resume メソッドを Request クラスに対して呼び出すことで実現できます。

また、cancel メソッドはローディング処理自体をエラーとして扱い、以降の処理を行わないようにします。ローディング処理自体はエラーとして扱われるため、response メソッドのクロージャハンドラにはエラーが入ったタプルが返って来ます。

例4 -Request インスタンスのローディング処理を制御する-

class ViewController: UIViewController {
    
    var request: Alamofire.Request?

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
        let manager = Alamofire.Manager(configuration: configuration)
        manager.startRequestsImmediately = false
        request = manager.request(.GET, "http://localhost.jp/image", parameters: nil)
        request?.response { request, response, _, error in
            println(response)
        }
        
    }
    
    @IBAction func restartLoadButtonDidTouchUpInside(sender: UIButton) { // リスタートボタンハンドラ
        request?.resume()
    }
    
    @IBAction func stopLoadButtonDidTouchUpInside(sender: UIButton) { // ストップボタンハンドラ
        request?.suspend()
    }
    
    @IBAction func cancelButtonDidTouchUpInside(sender: UIButton) { // キャンセルボタンハンドラ
        request?.cancel()
    }
}

ここでは UIButton のタッチイベントに応じて request プロパティの挙動を制御しています。

IBAction に紐ついたリスタートボタンが押されると request プロパティはローディング処理を再開、ストップボタンが押されると request プロパティはローディング処理を一時停止、キャンセルボタンが押されると request プロパティはローディング処理を停止し、エラー扱いとします。

キャンセルボタンが押された場合にはエラー扱いとなり、response メソッドクロージャハンドラの error にはエラーオブジェクトが入ります。

URLRequestConvertible プロトコル

request, download, upload などの関数、メソッドは URLRequestConvertible プロトコルに準拠した型の引数をとるようになっています。URLRequestConvertible プロトコルは URLRequest プロパティを介して型を NSURLRequest に変換した後に Alamofire 内部で様々なリクエストを構成するプロトコルです。

public protocol URLRequestConvertible {
    var URLRequest: NSURLRequest { get }
}

デフォルトでは NSURLRequest がこのプロトコルに準拠しています。これを用いれば、簡素な形でAPIに対するリクエストを構成できます。

例5 -APIに対するCRUDリクエストをURLRequestConvertibleを用いて構成する-

public protocol EntityType: class { // 識別子を持つ特徴を示すプロトコルです。
    typealias IdentityType: Hashable
    var identifier: IdentityType { get }
}

public protocol Parameterizable: class { // パラメータ化できる特徴を示すプロトコルです。
    var parameterize: [String: AnyObject] { get }
}

public protocol RouterType: EntityType, Parameterizable { // ルータで扱える特徴を示すプロトコルです
    typealias IdentityType: Hashable, StringInterpolationConvertible
    class var routerName: String { get }
}

// 犬オブジェクトです。ルータと駆動するために必要なプロトコルに準拠しています。
final class Dog: RouterType {
    
    typealias IdentityType = String
    
    class var routerName: String {
        return "/dogs"
    }
    
    let name: String
    let identifier: IdentityType
    
    init(name: String, identifier: String) {
        self.name = name
        self.identifier = identifier
    }
    
    var parameterize: [String: AnyObject] {
        return ["name" : name, "id" : identifier]
    }
}

// APIリクエストを構成するルータ列挙型です。
enum CRUDRouter<T: RouterType>: URLRequestConvertible {
    static var baseURLString: String {
        return "http://localhost.jp"
    }
    
    case Create(T)
    case Read(T.IdentityType)
    case Update(T)
    case Destroy(T.IdentityType)
    
    var method: Alamofire.Method {
        switch self {
        case .Create:
            return .POST
        case .Read:
            return .GET
        case .Update:
            return .PUT
        case .Destroy:
            return .DELETE
        }
    }
    
    var path: String {
        switch self {
        case .Create:
            return T.routerName
        case .Read(let id):
            return T.routerName + "/\(id)"
        case .Update(let object):
            return T.routerName + "/\(object.identifier)"
        case .Destroy(let id):
            return T.routerName + "/\(id)"
        }
    }
    
    // MARK: URLRequestConvertible
    
    var URLRequest: NSURLRequest {
        let URL = NSURL(string: CRUDRouter.baseURLString)!
        let mutableURLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(path))
        mutableURLRequest.HTTPMethod = method.rawValue
        
        switch self {
        case .Create(let object):
            return Alamofire.ParameterEncoding.JSON.encode(mutableURLRequest, parameters: object.parameterize).0
        case .Update(let object):
            return Alamofire.ParameterEncoding.JSON.encode(mutableURLRequest, parameters: object.parameterize).0
        default:
            return mutableURLRequest
        }
    }
}

let dogCreateRequest = Alamofire.request(CRUDRouter.Create(Dog(name: "pochi", identifier: "d0001")))
println(dogCreateRequest.debugDescription)

// $ curl -i \
//  -X POST \
//  -H "Content-Type: application/json" \
//  -H "User-Agent: HTTPStudy/yad.HTTPStudy (1; OS Version 8.1 (Build 12B411))" \
//  -H "Accept-Encoding: gzip;q=1.0,compress;q=0.5" \
//  -H "Accept-Language: en;q=1.0" \
//  -d "{\"id\":\"d0001\",\"name\":\"pochi\"}" \
//  "http://localhost.jp/dogs"

この例では REST API と連携して、必要なオブジェクトの生成(Create)、読込(Read)、更新(Update)、削除(Destroy)を一挙に担う列挙型 CRUDRouter を作成しています。

この CRUDRouter はジェネリクスを用いて RouterType プロトコルに準拠した型引数をとり、クラスの使用者は RouterType に既存のエンティティモデルを準拠させることで REST API を叩くためのリクエストを簡単に構成できます。

レスポンスのカスタムシリアライズ

導入編で述べたように、Alamofire では HTTP 通信でのレスポンス内容を文字列や JSON オブジェクトとして扱うためのメソッド responseString や responseJSON などが予め提供されています。これらに類するレスポンスハンドラの実装をエクステンションを用いて自ら行うことも可能です。

次に紹介する例の中では、レスポンスに応じたイニシャライザをプロトコルでまとめることで、プロトコルに準拠した型に対するシリアライズができます。

例6 -レスポンスをジェネリクス、プロトコルと併用する-

public protocol JSONObjectSerializable: class {
    init?(JSON: AnyObject)
}

extension Alamofire.Request {
    public func responseObject<T: JSONObjectSerializable>
        (completionHandler: (NSURLRequest, NSHTTPURLResponse?, T?, NSError?) -> Void) -> Self {
        let serializer: Serializer = { request, response, data in
            let JSONSerializer = Request.JSONResponseSerializer(options: .AllowFragments) // --(1)
            let (JSON: AnyObject?, serializationError) = JSONSerializer(request, response, data)
            if response != nil && JSON != nil {
                return (T(JSON: JSON!), nil)
            } else {
                return (nil, serializationError)
            }
        }
        
        return response(serializer: serializer) { request, response, object, error in
            completionHandler(request, response, object as T?, error)
        }
    }
}

final class Cat: JSONObjectSerializable {
    let name: String
    let identifier: String
    
    required init?(JSON: AnyObject) {
        let name = (JSON as? NSDictionary)?["name"] as? String
        let identifier = (JSON as? NSDictionary)?["name"] as? String
        self.name = name ?? ""
        self.identifier = identifier ?? ""
        if name == nil || identifier == nil {
            return nil
        }
    }
}

Alamofire.request(.GET, "http://localhost.jp/cat")
         .responseObject { (_, _, cat: Cat?, _) in
             println(cat)
    }

この例では JSON を用いてシリアライズできるクラス全体を JSONObjectSerializable プロトコルとして表し、そのプロトコルに適合した型 T に対するレスポンスメソッド responseObject を Request クラスに追加しています。

responseObject メソッドのハンドラ第三引数に JSONObjectSerializable プロトコルに準拠した型を指定するだけでこのメソッドの使用者は JSON オブジェクトから生成されたモデルクラスをそのまま利用できます。

コメントで --(1) とマークが付けられた部分は Request クラスの JSONResponseSerializer メソッドを用いています

public class func JSONResponseSerializer(options: NSJSONReadingOptions = .AllowFragments) -> Serializer

public typealias Serializer = (NSURLRequest, NSHTTPURLResponse?, NSData?) -> (AnyObject?, NSError?)

Catクラスの初期化で JSON からの値を取り出す際、キャストのコードが冗長ですが、この部分は SwiftyJSON のOSSをもちいれば以下のように簡略化できます。

required init?(JSON: AnyObject) {
    let json = SwiftyJSON.JSON(JSON)
    let name = json["name"].string
    let identifier = json["identifier"].string
    self.name = name ?? ""
    self.identifier = identifier ?? ""
    if name == nil || identifier == nil {
        return nil
    }
}

参考サイト