対象:
Swift4

ファイルアップロード(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)

新着情報
【オープンソースソフトウェア環境構築】Apple silicon Macで開発環境を構築
【Rust Tips】Actix webでJSONをPOSTする
【Rust Tips】コマンドライン引数を取得する

Copyright© 2004-2019 モバイル開発系(K) All rights reserved.
[Home]