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

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

(Swift2) Alamofire の upload 用 API クライアントを作成する


前回の記事で Alamofire の request メソッドに対する API クライアントを作成しました。

Swift2 Alamofire + ObjectMapper で API クライアントを作成する

この API クライアントを利用すると request は以下のように書くことができます。

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

前回作成したのは request メソッドだけなので、今回は、前回作成した API クライアントを元に、画像等をアップロードする際に利用する upload メソッドのクライアントを作成したいと思います。

  1. Alamofire の upload 用のリクエストを定義する protocol を用意する( UploadProtocol.swift )
  2. upload を行う Endpoint を定義する ( Endpoint.swift )
  3. Alamofire の upload リクエストを行う class メソッドを実装する( API.swift )

以下のような形で画像のアップロードを行う API をコールすることができるようになります。

let image = UIImage(named: "test")
let humanQuality: CGFloat = 0.8
let imageData = UIImageJPEGRepresentation(image, humanQuality)

API.call(Endpoint.UploadPhoto(imageData: imageData)) { response in
    switch response {
    case .Success(let result):
        print("success \(result)")
    case .Failure(let error):
        print("failure \(error)")
    }
}

1. Alamofire の upload 用のリクエストを定義する protocol を用意する( UploadProtocol.swift )

前回作成した RequestProtocol.swift を元に upload 用の UploadProtocol.swift を作成します。 upload と request で異なる点は以下の点になります

  • 画像のアップロードでは multipart/form-data リクエストになる為 URLRequestConvertible は採用しない
  • request では parameters 。 upload では multipartFormData を使う

RequestProtocol.swift を元に UploadProtocol.swift を作成すると以下のようになります。

・ UploadProtocol.swift

import Alamofire
import ObjectMapper

protocol UploadProtocol { // URLRequestConvertible は採用しない
    associatedtype ResponseType
    
    var baseURL: String { get }
    var method: Alamofire.Method { get }
    var path: String { get }
    var headers: [String: String]? { get }
    var multipartFormData: (MultipartFormData) -> () { get }
    // var parameters: [String: AnyObject]? { get } // upload では不要
    // var encoding: ParameterEncoding { get } // upload では不要
    
    func fromJson(json: AnyObject) -> Result<ResponseType, NSError>
}

extension UploadProtocol {
    var method: Alamofire.Method {
        return .POST // upload リクエストは基本 POST 処理なので、デフォルトで POST を宣言
    }
    
    var baseURL: String {
        return "https://httpbin.org"
    }
    
    var headers: [String: String]? {
        return nil
    }
    
    var parameters: [String: AnyObject]? {
        return nil
    }
    
    var encoding: ParameterEncoding {
        return .URL
    }
    
    // upload では URLRequest ではなく multipartFormData を利用する
    // var URLRequest: NSMutableURLRequest {
    //     let url = "\(baseURL)\(path)"
    //     let encodedUrl = url.stringByAddingPercentEncodingWithAllowedCharacters(
    //         NSCharacterSet.URLQueryAllowedCharacterSet())
    //     let mutableURLRequest = NSMutableURLRequest(URL: NSURL(string: encodedUrl!)!)
    //     mutableURLRequest.HTTPMethod = method.rawValue
    //     mutableURLRequest.allHTTPHeaderFields = headers
    //     do {
    //         if let parameters = parameters {
    //             mutableURLRequest.HTTPBody = try NSJSONSerialization.dataWithJSONObject(
    //                 parameters, options: NSJSONWritingOptions())
    //         }
    //     } catch {
    //         // No-op
    //     }
    //     mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
    //     return mutableURLRequest
    // }
    
    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)
    }
}

extension UploadProtocol where ResponseType: Mappable {
    func fromJson(json: AnyObject) -> Result<ResponseType, NSError> {
        guard let value = Mapper<ResponseType>().map(json) else {
            let errorInfo = [ NSLocalizedDescriptionKey: "Mapping object failed" , NSLocalizedRecoverySuggestionErrorKey: "Rainy days never stay." ]
            let error = NSError(domain: "com.example.app", code: 0, userInfo: errorInfo)
            return .Failure(error)
        }
        return .Success(value)
    }
}

見てみると、ほとんど RequestProtocol.swift と同じなので RequestProtocol.swift と UploadProtorol.swfit の共通で採用する APIProtocol.swift を作成してリファクタリングします。最終的には以下のようなファイルになりました。

・ APIProtocol.swift

import Alamofire
import ObjectMapper

protocol APIProtocol {
    associatedtype ResponseType
    
    var baseURL: String { get }
    var method: Alamofire.Method { get }
    var path: String { get }
    var headers: [String: String]? { get }
    
    func fromJson(json: AnyObject) -> Result<ResponseType, NSError>
}

extension RequestProtocol {
    var method: Alamofire.Method {
        return .GET
    }
    
    var baseURL: String {
        return "https://httpbin.org"
    }
    
    var headers: [String: String]? {
        return nil
    }
    
    var encoding: ParameterEncoding {
        return .URL
    }
        
    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)
    }
}

extension APIProtocol where ResponseType: Mappable {
    func fromJson(json: AnyObject) -> Result<ResponseType, NSError> {
        guard let value = Mapper<ResponseType>().map(json) else {
            let errorInfo = [ NSLocalizedDescriptionKey: "Mapping object failed" , NSLocalizedRecoverySuggestionErrorKey: "Rainy days never stay." ]
            let error = NSError(domain: "com.example.app", code: 0, userInfo: errorInfo)
            return .Failure(error)
        }
        return .Success(value)
    }
}

・ RequestProtocol.swift

import Alamofire
import ObjectMapper

protocol RequestProtocol: APIProtocol, URLRequestConvertible {
    var parameters: [String: AnyObject]? { get }
    var encoding: ParameterEncoding { get }
}

extension RequestProtocol: APIProtocol {
    var parameters: [String: AnyObject]? {
        return nil
    }
    
    var encoding: ParameterEncoding {
        return .URL
    }
    
    var URLRequest: NSMutableURLRequest {
        let url = "\(baseURL)\(path)"
        let encodedUrl = url.stringByAddingPercentEncodingWithAllowedCharacters(
            NSCharacterSet.URLQueryAllowedCharacterSet())
        let mutableURLRequest = NSMutableURLRequest(URL: NSURL(string: encodedUrl!)!)
        mutableURLRequest.HTTPMethod = method.rawValue
        mutableURLRequest.allHTTPHeaderFields = headers
        do {
            if let parameters = parameters {
                mutableURLRequest.HTTPBody = try NSJSONSerialization.dataWithJSONObject(
                    parameters, options: NSJSONWritingOptions())
            }
        } catch {
            // No-op
        }
        mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
        return mutableURLRequest
    }
}

・ UploadProtocol.swift

import Foundation
import Alamofire

protocol UploadProtocol: APIProtocol {
    var multipartFormData: (MultipartFormData) -> () { get }
}

extension UploadProtocol {
    var method: Alamofire.Method {
        return .POST
    }
}

2. upload を行う Endpoint を定義する ( Endpoint.swift )

Request の時と同じように Endpoint.swift 内に定義します。

class Endpoint {
    class UploadPhoto: UploadProtocol {
        typealias ResponseType = AnyObject
        var imageData: NSData
        
        init(imageData: NSData) {
            self.imageData = imageData
        }
        
        var path: String {
            return "/post"
        }

        // クロージャー内で self を利用する為に lazy を指定する
        // クロージャー内で self を参照すると強参照で循環してしまうので、weak self としてクロージャー内の self の参照を弱参照にする
        lazy var multipartFormData: (MultipartFormData) -> () = { [weak self](data: MultipartFormData) in
            guard let weakSelf = self else {
                return
            }
            data.appendBodyPart(
                data: weakSelf.imageData,
                name: "photo",
                fileName: "image",
                mimeType: "image/jpeg"
            )
        }
    }
}

3. Alamofire の upload リクエストを行う class メソッドを実装する( API.swift )

API.swift に call メソッドを引数の異なる同名メソッドで定義します

・ API.swift

// request 用の call メソッド
class func call<T: RequestProtocol, V where T.ResponseType == V>(request: T, completion: (Result<V, NSError>) -> Void) -> Request? {
    ...
}

class func call<T: UploadProtocol, V where T.ResponseType == V>(request: T, completion: (Result<V, NSError>) -> Void){
    Alamofire.upload(
        request.method,
        request.baseURL + request.path,
        headers: request.headers,
        multipartFormData: request.multipartFormData,
        encodingCompletion: { encodingResult in
            switch encodingResult {
            case .Success(let upload, _, _):
                upload.responseJSON { response in
                    switch response.result {
                    case .Success(let json):
                        completion(request.fromJson(json)
                    case .Failure(let error):
                        completion(.Failure(error))
                    }
                }
            case .Failure(let encodingError):
                print(encodingError)
            }
        }
    )
}

これで以下のように request の時と同じように upload を行うことができるようになります。

let image = UIImage(named: "test")
let humanQuality: CGFloat = 0.8
let imageData = UIImageJPEGRepresentation(image, humanQuality)

API.call(Endpoint.UploadPhoto(imageData: imageData)) { response in
    switch response {
    case .Success(let result):
        print("success \(result)")
    case .Failure(let error):
        print("failure \(error)")
    }
}