【Swift】multipart/form-dataを実装してみた


備忘録です。

ライブラリを使えば大体対応されているものですが、 自前で書いたことがなかったのでやってみました。

力技な感じも否めませんが、問題なくできたのでとりあえず大丈夫かと。

来月当たりに自分がみたときにもっと改良できることを祈っています。

## ソースコード

とりあえずソースコード載せます。

`NSURLRequest`を作成する部分だけに絞りますので、リクエストの送り方などは他でお願いします。

```swift:RequestData.swift
/*
送りたいバイナリデータを持つクラス
MIME-TYPEとfile nameとNSDataを保持します
*/

class RequestData {

	enum MimeType: String {
		case JPEG = "image/jpeg"
		case MP4 = "video/mp4"
	}
	
	var data: NSData
	var mimeType: MimeType
	var filename: String
	
	init(data: NSData, mimeType: MimeType, filename: String) {
		self.data = data
		self.mimeType = mimeType
		self.filename = filename
	}
}

```

```swift:API.swift
/*
NSURLRequestを作成するクラス
ここにAPIにRequestを送る処理も書いていました
*/
class API {

	enum Method: String {
		case GET = "GET"
		case POST = "POST"
	}
	
	class func baseURL() -> NSURL {
		return NSURL(string: "リクエストを送るURL")
	}
	
	class func URLRequest(#method: Method, path: String, parameters: [String:AnyObject]) -> NSURLRequest? {
		if let components = NSURLComponents(URL: baseURL(), resolvingAgainstBaseURL: true) {
            let request = NSMutableURLRequest()
            request.HTTPMethod = method.rawValue
            
            if method == .GET {
                components.query = API.Helper.stringFromObject(parameters, encoding: NSUTF8StringEncoding)
            } else if method == .POST {
                var contentType: String?
                var paramsData: NSData?
                if API.Helper.isMultiParams(parameters) {
                    let boundary = "POST-boundary-\(arc4random())-\(arc4random())"
                    paramsData = API.Helper.multiDataFromObject(parameters, boundary: boundary)
                    contentType = "multipart/form-data; boundary=\(boundary)"
                } else {
                    paramsData = API.Helper.dataFromObject(parameters, encoding: NSUTF8StringEncoding)
                    contentType = "application/x-www-form-urlencoded"
                }
                
                if let params = paramsData {
                    request.setValue(contentType, forHTTPHeaderField: "Content-Type")
                    request.setValue("\(params.length)", forHTTPHeaderField: "Content-Length")
                    request.HTTPBody = params
                }
            }
            
            components.path = (components.path ?? "").stringByAppendingPathComponent(path).stringByAppendingString("/")
            request.URL = components.URL
            // responseがjsonの場合
            request.setValue("application/json", forHTTPHeaderField: "Accept")
            return request
        }
        
        return nil
	}
	
	class Helper {
	
		class func isMultiParams(params: [String:AnyObject]) -> Bool {
            
            var isMultiParams = false
            
            for (_, value) in params {
                if value is RequestData {
                    isMultiParams = true
                    break
                }
            }
            
            return isMultiParams
        }
        
        class func dataFromObject(object: AnyObject, encoding: NSStringEncoding) -> NSData? {
            let string = stringFromObject(object, encoding: encoding)
            return string.dataUsingEncoding(encoding, allowLossyConversion: false)
        }
        
        class func stringFromObject(object: AnyObject, encoding: NSStringEncoding) -> String {
            var pairs = [String]()
            
            if let dictionary = object as? [String: AnyObject] {
                for (key, value) in dictionary {
                    let string = (value as? String) ?? "\(value)"
                    let pair = "\(key)=\(string.escape())"
                    pairs.append(pair)
                }
            }
            
            return join("&", pairs)
        }
        
        class func multiDataFromObject(object: [String:AnyObject], boundary: String) -> NSData? {
            var data = NSMutableData()
            
            let prefixString = "--\(boundary)\r\n"
            let prefixData = prefixString.dataUsingEncoding(NSUTF8StringEncoding)!
            
            let seperatorString = "\r\n"
            let seperatorData = seperatorString.dataUsingEncoding(NSUTF8StringEncoding)!
            
            for (key, value) in object {
                    
                var valueData: NSData?
                var valueType: String?
                var filenameClause = ""
                    
                if value is RequestData {
                    let requestData = value as! RequestData
                    if let data = requestData.getData() {
                        valueData = data
                        valueType = requestData.mimeType.rawValue
                        filenameClause = " filename=\"\(requestData.filename)\""
                    }
                } else {
                    let stringValue = "\(value)"
                    valueData = stringValue.dataUsingEncoding(NSUTF8StringEncoding)!
                }
                  
                if valueData == nil {
                    continue
                }
                data.appendData(prefixData)
                let contentDispositionString = "Content-Disposition: form-data; name=\"\(key)\";\(filenameClause)\r\n"
                let contentDispositionData = contentDispositionString.dataUsingEncoding(NSUTF8StringEncoding)
                data.appendData(contentDispositionData!)
                if let type = valueType {
                    let contentTypeString = "Content-Type: \(type)\r\n"
                    let contentTypeData = contentTypeString.dataUsingEncoding(NSUTF8StringEncoding)
                    data.appendData(contentTypeData!)
                }
                data.appendData(seperatorData)
                data.appendData(valueData!)
                data.appendData(seperatorData)
            }
            
            let endingString = "--\(boundary)--\r\n"
            let endingData = endingString.dataUsingEncoding(NSUTF8StringEncoding)!
            data.appendData(endingData)
            
            return data
        }
	}
}
```

とまあこんな感じでできました。

## 使い方

```swift:Playgoround.swift
// UIImageをjpegにしてリクエストする
let image = UIImage(named: "hoge")
let jpegData = UIImageJPEGRepresentation(image, 1)
let requestData = RequestData(data: jpegData, mimeType: JPEG, "hoge.jpeg")
let parameters = ["title":"hogehoge", "text": "hoge", "post_image": requestData]
if let request = API.URLRequest(method: .POST, path: "/hogehoge", parameters: parameters) {
	// requestを送る処理
}
```

みたいになります。

## やっていること

至極簡単です。

**multipart/form-data**のことを調べてもらえればわかるかと思います。

`for-in`で`key`と`value`に分けて、クラス判別をして作業を分けています。

いろいろなライブラリのソースコードを参考にしましたが大体同じことをしていました。

## 最後に

クラス分け、`enum`の使い所など、参考になる点が多々あるのでライブラリを読むのって大事ですね。

以上です。