Pedro Manfredi
Pedro Manfredi

Reputation: 3768

Upload image with parameters in Swift

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
}

Upvotes: 77

Views: 95580

Answers (3)

Rob
Rob

Reputation: 437882

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 `multipart/form-data` request with specifics values.
///
/// - Parameters:
///     - userid:   The userid to be passed to web service
///     - password: The password to be passed to web service
///     - email:    The email address to be passed to web service
///     - imageUrl: The URL of file resource for image to be uploaded
///
/// - returns:            The `URLRequest` and its associated `Data`

func requestAndData(userid: String, password: String, email: String, urls: [URL]) throws -> (URLRequest, Data) {
    let parameters = [
        "user_id"  : userid,
        "email"    : email,
        "password" : password]  // build your dictionary however appropriate
    
    return try requestAndData(parameters: parameters, fileKeyPath: "file", urls: urls)
}

/// General purpose routine to create `multipart/form-data` request
///
/// - Parameters:
///     - parameters:  Dictionary of parameters to be added to body.
///     - headers:     Dictionary of headers to be added to request.
///     - filePathKey: The optional field name to be used when uploading files.       If you supply paths, you must supply filePathKey, too.
///     - urls:        An array of fileURLs to be added to body.
///
/// - returns:            The `URLRequest` and its associated `Data`

func requestAndData(
    parameters: [String: String]? = nil,
    headers: [String: String]? = nil,
    fileKeyPath: String,
    urls: [URL]
) throws -> (URLRequest, Data) {
    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")
    parameters?.forEach { key, value in
        request.setValue(value, forHTTPHeaderField: key)
    }

    let data = try createBody(with: parameters, filePathKey: fileKeyPath, urls: urls, boundary: boundary)

    return (request, data)
}

/// Create body of the `multipart/form-data` request
///
/// - Parameters:
///     - parameters:   The optional dictionary containing keys and values to be passed to web service.
///     - filePathKey:  The optional field name to be used when uploading files. If you supply paths, you must supply filePathKey, too.
///     - fileKeyPath:  The key that the server is expecting for files in body of request.
///     - urls:         The optional array of file URLs of the files to be uploaded.
///     - 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:

import UniformTypeIdentifiers // for iOS 14+
import MobileCoreServices     // for pre-iOS 14

extension URL {
    /// Mime type for the URL

    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
    ///
    /// Append string using `.utf8` format.
    ///
    /// - parameter string:       The string to be added to the `Data`.

    mutating func append(_ string: String) {
        append(Data(string.utf8))
    }
}

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:

func upload(userId: String, password: String, email: String, imageUrl: URL) async throws {
    let (request, payload) = try requestAndData(userid: userid, password: password, email: email, imageUrl: imageUrl)
    let (data, response) = try await URLSession.shared.upload(for: request, from: payload)
    guard let httpResponse = response as? HTTPURLResponse else {
        // throw error for non-HTTP response
    }
    guard 200..<300 ~= httpResponse.statusCode else {
        // throw error if server reported anything other than success
    }
    // ok, server returned 2xx response, so now parse `data` here
}

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 completion-handler-based rendition (i.e., for codebases that have not adopted Swift concurrency) see previous revision of this answer.

Upvotes: 150

Nirmala Maurya
Nirmala Maurya

Reputation: 91

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 :)

Upvotes: 2

Robert Chen
Robert Chen

Reputation: 5353

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 { }
})

Upvotes: 14

Related Questions