Upload image with parameters in Swift

90,033

Solution 1

In your comment below, you inform us that you are using the $_FILES syntax to retrieve the files. That means that you want to create a multipart/form-data request. The process is basically:

  1. Specify a boundary for your multipart/form-data request.

  2. Specify a Content-Type of the request that specifies that it multipart/form-data and what the boundary is.

  3. Create body of request, separating the individual components (each of the posted values as well as between each upload).

For more detail, see RFC 7578. Anyway, in Swift 3 and later, this might look like:

/// Create request
///
/// - parameter userid:   The userid to be passed to web service
/// - parameter password: The password to be passed to web service
/// - parameter email:    The email address to be passed to web service
///
/// - returns:            The `URLRequest` that was created

func createRequest(userid: String, password: String, email: String) throws -> URLRequest {
    let parameters = [
        "user_id"  : userid,
        "email"    : email,
        "password" : password]  // build your dictionary however appropriate
    
    let boundary = generateBoundaryString()
    
    let url = URL(string: "https://example.com/imageupload.php")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
    
    let fileURL = Bundle.main.url(forResource: "image1", withExtension: "png")!
    request.httpBody = try createBody(with: parameters, filePathKey: "file", urls: [fileURL], boundary: boundary)
    
    return request
}

/// Create body of the `multipart/form-data` request
///
/// - parameter parameters:   The optional dictionary containing keys and values to be passed to web service.
/// - parameter filePathKey:  The optional field name to be used when uploading files. If you supply paths, you must supply filePathKey, too.
/// - parameter urls:         The optional array of file URLs of the files to be uploaded.
/// - parameter boundary:     The `multipart/form-data` boundary.
///
/// - returns:                The `Data` of the body of the request.

private func createBody(with parameters: [String: String]? = nil, filePathKey: String, urls: [URL], boundary: String) throws -> Data {
    var body = Data()
    
    parameters?.forEach { (key, value) in
        body.append("--\(boundary)\r\n")
        body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n")
        body.append("\(value)\r\n")
    }
    
    for url in urls {
        let filename = url.lastPathComponent
        let data = try Data(contentsOf: url)
        
        body.append("--\(boundary)\r\n")
        body.append("Content-Disposition: form-data; name=\"\(filePathKey)\"; filename=\"\(filename)\"\r\n")
        body.append("Content-Type: \(url.mimeType)\r\n\r\n")
        body.append(data)
        body.append("\r\n")
    }
    
    body.append("--\(boundary)--\r\n")
    return body
}

/// Create boundary string for multipart/form-data request
///
/// - returns:            The boundary string that consists of "Boundary-" followed by a UUID string.

private func generateBoundaryString() -> String {
    return "Boundary-\(UUID().uuidString)"
}

With:

extension URL {
    /// Mime type for the URL
    ///
    /// Requires `import UniformTypeIdentifiers` for iOS 14 solution.
    /// Requires `import MobileCoreServices` for pre-iOS 14 solution

    var mimeType: String {
        if #available(iOS 14.0, *) {
            return UTType(filenameExtension: pathExtension)?.preferredMIMEType ?? "application/octet-stream"
        } else {
            guard
                let identifier = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(),
                let mimeType = UTTypeCopyPreferredTagWithClass(identifier, kUTTagClassMIMEType)?.takeRetainedValue() as String?
            else {
                return "application/octet-stream"
            }

            return mimeType
        }
    }
}

extension Data {
    
    /// Append string to Data
    ///
    /// Rather than littering my code with calls to `data(using: .utf8)` to convert `String` values to `Data`, this wraps it in a nice convenient little extension to Data. This defaults to converting using UTF-8.
    ///
    /// - parameter string:       The string to be added to the `Data`.
    
    mutating func append(_ string: String, using encoding: String.Encoding = .utf8) {
        if let data = string.data(using: encoding) {
            append(data)
        }
    }
}

Having all of this, you now need to submit this request. I would advise this is done asynchronously. For example, using URLSession, you would do something like:

let request: URLRequest

do {
    request = try createRequest(userid: userid, password: password, email: email)
} catch {
    print(error)
    return
}

let task = URLSession.shared.dataTask(with: request) { data, response, error in
    guard let data = data, error == nil else {
        // handle error here
        print(error ?? "Unknown error")
        return
    }
    
    // parse `data` here, then parse it
    
    // note, if you want to update the UI, make sure to dispatch that to the main queue, e.g.:
    //
    // DispatchQueue.main.async {
    //     // update your UI and model objects here
    // }
}
task.resume()

If you are uploading large assets (e.g. videos or the like), you might want to use a file-based permutation of the above. See https://stackoverflow.com/a/70552269/1271826.


For Swift 2 renditions, see previous revision of this answer.

Solution 2

AlamoFire now supports Multipart:

https://github.com/Alamofire/Alamofire#uploading-multipartformdata

Here's a blog post with sample project that touches on using Multipart with AlamoFire.

http://www.thorntech.com/2015/07/4-essential-swift-networking-tools-for-working-with-rest-apis/

The relevant code might look something like this (assuming you're using AlamoFire and SwiftyJSON):

func createMultipart(image: UIImage, callback: Bool -> Void){
    // use SwiftyJSON to convert a dictionary to JSON
    var parameterJSON = JSON([
        "id_user": "test"
    ])
    // JSON stringify
    let parameterString = parameterJSON.rawString(encoding: NSUTF8StringEncoding, options: nil)
    let jsonParameterData = parameterString!.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: true)
    // convert image to binary
    let imageData = UIImageJPEGRepresentation(image, 0.7)
    // upload is part of AlamoFire
    upload(
        .POST,
        URLString: "http://httpbin.org/post",
        multipartFormData: { multipartFormData in
            // fileData: puts it in "files"
            multipartFormData.appendBodyPart(fileData: jsonParameterData!, name: "goesIntoFile", fileName: "json.txt", mimeType: "application/json")
            multipartFormData.appendBodyPart(fileData: imageData, name: "file", fileName: "iosFile.jpg", mimeType: "image/jpg")
            // data: puts it in "form"
            multipartFormData.appendBodyPart(data: jsonParameterData!, name: "goesIntoForm")
        },
        encodingCompletion: { encodingResult in
            switch encodingResult {
            case .Success(let upload, _, _):
                upload.responseJSON { request, response, data, error in
                    let json = JSON(data!)
                    println("json:: \(json)")
                    callback(true)
                }
            case .Failure(let encodingError):
                callback(false)
            }
        }
    )
}

let fotoImage = UIImage(named: "foto")
    createMultipart(fotoImage!, callback: { success in
    if success { }
})

Solution 3

Thank you @Rob, your code is working fine, but in my case, I am retriving image from gallary and taking name of the image by using code:

let filename = url.lastPathComponent

But this code, displaying image extension as .JPG (in capital letter), but server not accepting extensions in captital letter, so i changed my code as:

 let filename =  (path.lastPathComponent as NSString).lowercaseString

and now my code is working fine.

Thank you :)

Share:
90,033
Pedro Manfredi
Author by

Pedro Manfredi

Updated on July 05, 2022

Comments

  • Pedro Manfredi
    Pedro Manfredi almost 2 years

    I'm trying to upload an image with parameters in Swift. When I try this code, I can get the parameters but not the image

    uploadFileToUrl(fotiño:UIImage){
        var foto =  UIImage(data: UIImageJPEGRepresentation(fotiño, 0.2))
    
    
        var request = NSMutableURLRequest(URL:NSURL(string: "URL"))
        request.HTTPMethod = "POST"
    
        var bodyData = "id_user="PARAMETERS&ETC""
    
    
        request.HTTPBody = bodyData.dataUsingEncoding(NSUTF8StringEncoding);
        request.HTTPBody = NSData.dataWithData(UIImagePNGRepresentation(foto))
        println("miraqui \(request.debugDescription)")
        var response: AutoreleasingUnsafeMutablePointer<NSURLResponse?>=nil
        var HTTPError: NSError? = nil
        var JSONError: NSError? = nil
    
        var dataVal: NSData? =  NSURLConnection.sendSynchronousRequest(request, returningResponse: response, error: &HTTPError)
    
        if ((dataVal != nil) && (HTTPError == nil)) {
            var jsonResult = NSJSONSerialization.JSONObjectWithData(dataVal!, options: NSJSONReadingOptions.MutableContainers, error: &JSONError)
    
            if (JSONError != nil) {
                println("Bad JSON")
            } else {
                println("Synchronous\(jsonResult)")
            }
        } else if (HTTPError != nil) {
            println("Request failed")
        } else {
            println("No Data returned")
        }
    }
    

    edit 2:

    I think that I have some problems with the path of the saved UIImage, because php tells me that the file already exist, which I think is because I send it in blank

    func createRequest (#userid: String, disco: String, id_disco: String, pub: String, foto: UIImage) -> NSURLRequest {
        let param = [
            "id_user"  : userid,
            "name_discoteca"    : disco,
            "id_discoteca" : id_disco,
            "ispublic" : pub] // build your dictionary however appropriate
    
        let boundary = generateBoundaryString()
    
        let url = NSURL(string: "http....")
        let request = NSMutableURLRequest(URL: url)
        request.HTTPMethod = "POST"
        request.timeoutInterval = 60
        request.HTTPShouldHandleCookies = false
        request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
        var imagesaver = ImageSaver()
    
        var image = foto  // However you create/get a UIImage
        let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
        let destinationPath = documentsPath.stringByAppendingPathComponent("VipKing.jpg")
        UIImageJPEGRepresentation(image,1.0).writeToFile(destinationPath, atomically: true)
    
    
        self.saveImage(foto, withFileName: "asdasd22.jpg")
    
    
        var path = self.documentsPathForFileName("asdasd22.jpg")
    
    
        self.ViewImage.image = self.loadImageWithFileName("asdasd22.jpg")
    
    
    
      //  let path1 = NSBundle.mainBundle().pathForResource("asdasd22", ofType: "jpg", inDirectory: path) as String! 
    
        **//path1 always crash**
    
    
        println(param.debugDescription)
        println(path.debugDescription)
        println(boundary.debugDescription)
    
    
    
    
        request.HTTPBody = createBodyWithParameters(param, filePathKey: "asdasd22.jpg", paths: [path], boundary: boundary)
    
        println(request.debugDescription)
    
    
        return request
    }
    
  • Pedro Manfredi
    Pedro Manfredi over 9 years
    Thanks! very usefull!
  • Pedro Manfredi
    Pedro Manfredi over 9 years
    I try a lot of things saving in local the image... let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String i could save it, but the php tell me than already exist when i try to upload it, i think than i send the image in blank or smth goes wrong... i think i'm confuse between paths and filePathKey i edit my answer!
  • Rob
    Rob over 9 years
    @user3426109 By the way, the choice of NSSearchPathForDirectories or NSBundle is how you specify which local file you're uploading. But, if you're getting your "already exist" message from the server, then this has nothing to do with that. The "already exist" indicates that the server has a copy of a file of that name (and apparently the script is written such that it won't just overwrite it).
  • Pedro Manfredi
    Pedro Manfredi over 9 years
    Done!!! Thank you so much!!!!!!! it was on filePathKey: "file" where i have the problem
  • bworby
    bworby over 9 years
    Very nice! I wanted to switch to a swift version of this and thought I would look for a quick solution. Found it here! Im doing this to post to server thats also taking in form submissions and requires multiple files... so file[] instead of file for the filePathKey was needed. Thanks again.
  • Tyler
    Tyler about 9 years
    how is this done when you are passing a file in a post parameter? I can't seem to figure it out stackoverflow.com/questions/30006290/…
  • Nicolas Miari
    Nicolas Miari over 8 years
    It would be great to get the code updated to Swift 2 (the mimeTypeForPath part isn't quite straightforward to me...)
  • Rob
    Rob over 8 years
    @NicolasMiari - This is Swift 2. As the comment for mimeTypeForPath says, you need MobileCoreServices. So, up at the top of the file, do import MobileCoreServices.
  • Nicolas Miari
    Nicolas Miari over 8 years
    Oh, missed that; sorry.
  • Tapas Pal
    Tapas Pal over 8 years
    I am not able to find appendString method under NSMutableData?
  • Rob
    Rob over 8 years
    @TapasPal See the extension for NSMutableData which I included in my code samples above.
  • Tapas Pal
    Tapas Pal over 8 years
    Ohh yes... I missed that.. :)
  • G.Abhisek
    G.Abhisek about 8 years
    How to upload multiple images in an array ? Any idea at all.
  • Admin
    Admin over 7 years
    Hi @Rob quick question, would this work for what I am trying to do here stackoverflow.com/questions/40527140/… Hope you don't mind me asking!
  • Tiago Pereira
    Tiago Pereira almost 7 years
    Hey @Rob, just a quick question. In your example you are loading the file into memeory let data = try Data(contentsOf: url). If we are handling files with a considerable size, this would not be nice. Do you know if there's a way to do it using an InputStream or something like that?
  • Rob
    Rob almost 7 years
    Yeah, both stream-based and file-based approaches can be used to minimize memory impact. And, as an addition wrinkle, if they’re really big, you might consider using background uploads, which must be file-based. So if I were uploading huge assets, I’d use stream based API to build file with the body of the request (minimizing peak memory usage when building what will be the body of the request) and then do file-based upload task which keeps memory usage during upload to a minimum, and then remove that file when the upload is done.
  • luke
    luke almost 7 years
    Hi @Rob, ATM I'm currently uploading images directly to AWS S3 from the mobile application, retrieving the url of the images, then sending another request and uploading the urls to my backend (MySql/PHP) and storing the urls. Would you recommend against this and use the code you posted to upload the images to the backend and then let the backend to upload the images to s3?
  • Rob
    Rob almost 7 years
    @luke - It’s a matter of opinion, but it feels like the client app is a bit entangled in implementation details of the back end architecture. You’re also likely dealing with two authentication systems and two points of failure. I’d probably lean towards a single end point for the app, and have the web service manage the image storage.
  • Krutika Sonawala
    Krutika Sonawala over 6 years
    I'm getting an error : "JSON text did not start with array or object and option to allow fragments not set."
  • Rob
    Rob over 6 years
    @KrutikaSonawala - Above, I assumed you had a web service that wrote JSON responses. (It's one of the most robust ways to have a web service return a parseable response to a client app.) Perhaps your web service isn't designed to return a JSON response. Or perhaps you had an error in your web service that prevented it from creating a proper JSON response. I can't diagnose that on the basis of your comment. If the aforementioned isn't clear, I'd suggest you post your own question showing us what you did, what you expected in response, and what you actually received in response.
  • Krutika Sonawala
    Krutika Sonawala over 6 years
    @Rob here's the link of my question please. stackoverflow.com/questions/48220802/…
  • Peter Pohlmann
    Peter Pohlmann over 3 years
    Thanks you for that high quality answer, makes dev lives much easier but also teaches how to do things. TipTop!
  • Lakshmi Yadav
    Lakshmi Yadav over 3 years
    How can I send URL as a file name? or can we send fileName and URL as another parameter?
  • Rob
    Rob over 3 years
    I’d personally be inclined to send it as a separate parameter.
  • Shawn Frank
    Shawn Frank over 2 years
    Great and timeless answer that works till today !