対象: ファイルアップロード(Swift)アプリからサーバに写真等のファイルをアップロードする。ファイルのアップロードは基本的にはHTTPのPOSTになる。 まずは送信対象のファイルをData(以前はNSMutableData)にまとめていく。と言っても、たった今撮影した画像をアップロードする場合と、既存の画像ファイルをアップロードする場合とではDataの作成の仕方が若干異なる。最初に、撮影直後の画像をJPEGに変換してアップロードすることを考える。 もし、UIImagePickerControllerを使っているなら、UIImageからDataを作成することになるだろう。その場合はUIImageJPEGRepresentation関数を利用してDataに変換する。 // UIImageからJPEGに変換してアップロード let imageData = UIImageJPEGRepresentation(UIImage(named: fileName)!, 1.0) AVFoundationを使っているなら、AVCaptureStillImageOutput.jpegStillImageNSDataRepresentationメソッドかAVCapturePhotoのfileDataRepresentationメソッド等でDataに変換する。この辺りが何を言っているか分からなければ、以下もご参照いただければと思う。 一方、既存のJPEGファイルをアップロードする場合は、Data(contentsOf:)イニシャライザでDataに変換できるだろう。この例では、予めプロジェクトに追加しておいた"DSCF0085.JPG"ファイルをDataに変換している。 let fileName = "DSCF0085.JPG" let fileNameWithoutExt = (fileName as NSString).deletingPathExtension let ext = (fileName as NSString).pathExtension // 読み込んだJPEGファイルをそのままアップロード let imageData = try! Data(contentsOf: Bundle.main.url(forResource: fileNameWithoutExt, withExtension: ext)!) 送信したいJPEGファイルのDataが準備できたら、アップロードの情報を編集していく。送信対象のデータをバウンダリではさみ、ファイル名やファイル形式の情報を付加していく。尚、ファイルの境界を表すバウンダリはユニークな文字列であれば何でも良いが、ここではWebKit系のそれを模してある。Content-Dispositionヘッダはアップロードするファイル名等のヘッダとなる。Content-Typeヘッダは今回で言えば"image/jpeg"となる。 let boundary = "----WebKitFormBoundaryZLdHZy8HNaBmUX0d" func httpBody(_ fileAsData: Data, fileName: String) -> Data { var data = "--\(boundary)\r\n".data(using: .utf8)! // サーバ側が想定しているinput(type=file)タグのname属性値とファイル名をContent-Dispositionヘッダで設定 data += "Content-Disposition: form-data; name=\"file\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)! data += "Content-Type: image/jpeg\r\n".data(using: .utf8)! data += "\r\n".data(using: .utf8)! data += fileAsData data += "\r\n".data(using: .utf8)! data += "--\(boundary)--\r\n".data(using: .utf8)! return data } この後、ファイルアップロードに必要なものは、HTTP POSTするのと同様にURLRequestとURLSessionの2つである。まずはURLを指定してURLRequestを生成する。URLSessionはFoundation.URLSessionから新たに生成する。尚、マルチパートでアップロードする事になるので、ファイルの他にもJSON等のデータを載せる事ができるが、それについては割愛する。 取得したセッションのuploadTaskメソッドにURLRequestを渡して呼び出し、生成されたタスクのresumeメソッドを呼び出せばファイルをアップロードできる。今回はアップロード部分をメソッドにし、アップロード完了時にクロージャが呼ばれるようにしてみた。 // リクエストを生成してアップロード func fileUpload(_ url: URL, data: Data, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) { var request = URLRequest(url: url) request.httpMethod = "POST" // マルチパートでファイルアップロード let headers = ["Content-Type": "multipart/form-data; boundary=\(boundary)"] let urlConfig = URLSessionConfiguration.default urlConfig.httpAdditionalHeaders = headers let session = Foundation.URLSession(configuration: urlConfig) let task = session.uploadTask(with: request, from: data, completionHandler: completionHandler) task.resume() } uploadTaskメソッドに渡すcompletionHandlerにはHTTPリクエスト完了時、Data、URLResponse、及びErrorの3つの引数が渡される。DataはHTTPリクエストが成功した場合にサーバから取得したデータ(Body)、URLResponseはHTTPヘッダやHTTPステータスコード、Errorはエラーがなければnilである。 completionHandler(クロージャ)の中ではHTTPレスポンスのうち、Body部分がData(バイト列)として渡される。URLResponseはHTTPURLResponseにキャストすることによってstatusCodeを得ることができる。 コード全体を以下に示す。 import UIKit class ViewController: UIViewController { let boundary = "----WebKitFormBoundaryZLdHZy8HNaBmUX0d" override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } @IBAction func upload(_ sender: Any) { let fileName = "DSCF0085.JPG" let fileNameWithoutExt = (fileName as NSString).deletingPathExtension let ext = (fileName as NSString).pathExtension // UIImageからJPEGに変換してアップロード //let imageData = UIImageJPEGRepresentation(UIImage(named: fileName)!, 1.0)! // 読み込んだJPEGファイルをそのままアップロード let imageData = try! Data(contentsOf: Bundle.main.url(forResource: fileNameWithoutExt, withExtension: ext)!) let body = httpBody(imageData, fileName: fileName) let url = URL(string: "http://192.168.0.xx:8080/upload")! fileUpload(url, data: body) {(data, response, error) in if let response = response as? HTTPURLResponse, let _: Data = data , error == nil { if response.statusCode == 200 { print("Upload done") } else { print(response.statusCode) } } } } func httpBody(_ fileAsData: Data, fileName: String) -> Data { var data = "--\(boundary)\r\n".data(using: .utf8)! // サーバ側が想定しているinput(type=file)タグのname属性値とファイル名をContent-Dispositionヘッダで設定 data += "Content-Disposition: form-data; name=\"file\"; filename=\"\(fileName)\"\r\n".data(using: .utf8)! data += "Content-Type: image/jpeg\r\n".data(using: .utf8)! data += "\r\n".data(using: .utf8)! data += fileAsData data += "\r\n".data(using: .utf8)! data += "--\(boundary)--\r\n".data(using: .utf8)! return data } // リクエストを生成してアップロード func fileUpload(_ url: URL, data: Data, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) { var request = URLRequest(url: url) request.httpMethod = "POST" // マルチパートでファイルアップロード let headers = ["Content-Type": "multipart/form-data; boundary=\(boundary)"] let urlConfig = URLSessionConfiguration.default urlConfig.httpAdditionalHeaders = headers let session = Foundation.URLSession(configuration: urlConfig) let task = session.uploadTask(with: request, from: data, completionHandler: completionHandler) task.resume() } } 確認に使用したRubyのスクリプト(uploadserver.rb)も載せておく。これをruby uploadserver.rb -o 0.0.0.0 -p 8080で起動した。 require 'sinatra' post '/upload' do filename = params[:file][:filename] file = params[:file][:tempfile] File.open("./#{filename}", 'wb') do |f| f.write(file.read) end "Upload" end (2018/07/09)
Copyright© 2004-2019 モバイル開発系(K) All rights reserved.
[Home]
|