如何在Swift中使用Codable和URLSession.shared.uploadTask(多部分/表单-数据)上传图像文件?

6l7fqoea  于 2023-01-04  发布在  Swift
关注(0)|答案(3)|浏览(227)

我想上传一个图像文件到后端服务器,使用特定的URL端点。我可以很容易地使用Alamofire的上传请求作为multipartFormData。但是,我想摆脱Alamofire,以最大限度地减少对第三方框架的依赖。以下是Alamofire代码,它的工作:

func uploadRequestAlamofire(parameters: [String: Any], imageData: Data?, completion: @escaping(CustomError?) -> Void ) {

let url = imageUploadEndpoint!

let headers: HTTPHeaders = ["X-User-Agent": "ios",
                            "Accept-Language": "en",
                            "Accept": "application/json",
                            "Content-type": "multipart/form-data",
                            "ApiKey": KeychainService.getString(by: KeychainKey.apiKey) ?? ""]

Alamofire.upload(multipartFormData: { (multipartFormData) in
    for (key, value) in parameters {
        multipartFormData.append("\(value)".data(using: String.Encoding.utf8)!, withName: key as String)
    }

    if let data = imageData {
        multipartFormData.append(data, withName: "file", fileName: "image.png", mimeType: "image/jpg")
    }

}, usingThreshold: UInt64.init(), to: url, method: .post, headers: headers) { (result) in
    switch result {
    case .success(let upload, _, _):
        upload.responseJSON { response in

            completion(CustomError(errorCode: response.response!.statusCode))

            print("Succesfully uploaded")
        }
    case .failure(let error):
        print("Error in upload: \(error.localizedDescription)")

    }
}
}

以下是URLSession上载任务,该任务不起作用:

func requestNativeImageUpload(imageData: Data, orderExtId: String) {

var request = URLRequest(url: imageUploadEndpoint!)
request.httpMethod = "POST"
request.timeoutInterval = 10

    request.allHTTPHeaderFields = [
        "X-User-Agent": "ios",
        "Accept-Language": "en",
        "Accept": "application/json",
        "Content-type": "multipart/form-data",
        "ApiKey": KeychainService.getString(by: KeychainKey.apiKey) ?? ""
    ]

let body = OrderUpload(order_ext_id: orderExtId, file: imageData)

do {
    request.httpBody = try encoder.encode(body)
} catch let error {
    print(error.localizedDescription)
}

let session = URLSession.shared


session.uploadTask(with: request, from: imageData)  { data, response, error in
    guard let response = response as? HTTPURLResponse else { return }

    print(response)
    if error != nil {
        print(error!.localizedDescription)
    }

    }.resume()
}

这是我调用Alamofire和URLSession方法的方式:

uploadRequestAlamofire(parameters: ["order_ext_id": order_ext_id, "file": "image.jpg"], imageData: uploadImage) { [weak self] response in } 

requestNativeImageUpload(imageData: uploadImage!, orderExtId: order_ext_id)

以下是后端服务器期望在请求正文中接收的内容:

let order_ext_id: String
let description: String
let file: string($binary)

这是要为请求的httpBody编码的可编码结构。

struct OrderUpload: Codable {
    let order_ext_id: String
    let description: String 
    let file: String
}

虽然在这个演示中我的方法可能不完全合适,并且我不处理响应状态代码,但是Alamofire方法工作得很好。
为什么URLSession不能工作?

mo49yndu

mo49yndu1#

最后我终于找到了解决办法。来源是:URLSession: Multipart Form-Data Requests | Swift 3, Xcode 8。在我的特定情况下,我需要提供orderExtId作为后端服务器接受我的映像的参数。您的情况可能会有所不同,这取决于后端的要求。

func requestNativeImageUpload(image: UIImage, orderExtId: String) {

    guard let url = imageUploadEndpoint else { return }
    let boundary = generateBoundary()
    var request = URLRequest(url: url)

    let parameters = ["order_ext_id": orderExtId]

    guard let mediaImage = Media(withImage: image, forKey: "file") else { return }

    request.httpMethod = "POST"

    request.allHTTPHeaderFields = [
                "X-User-Agent": "ios",
                "Accept-Language": "en",
                "Accept": "application/json",
                "Content-Type": "multipart/form-data; boundary=\(boundary)",
                "ApiKey": KeychainService.getString(by: KeychainKey.apiKey) ?? ""
            ]

    let dataBody = createDataBody(withParameters: parameters, media: [mediaImage], boundary: boundary)
    request.httpBody = dataBody

    let session = URLSession.shared
    session.dataTask(with: request) { (data, response, error) in
        if let response = response {
            print(response)
        }

        if let data = data {
            do {
                let json = try JSONSerialization.jsonObject(with: data, options: [])
                print(json)
            } catch {
                print(error)
            }
        }
        }.resume()
}

func generateBoundary() -> String {
    return "Boundary-\(NSUUID().uuidString)"
}

func createDataBody(withParameters params: [String: String]?, media: [Media]?, boundary: String) -> Data {

    let lineBreak = "\r\n"
    var body = Data()

    if let parameters = params {
        for (key, value) in parameters {
            body.append("--\(boundary + lineBreak)")
            body.append("Content-Disposition: form-data; name=\"\(key)\"\(lineBreak + lineBreak)")
            body.append("\(value + lineBreak)")
        }
    }

    if let media = media {
        for photo in media {
            body.append("--\(boundary + lineBreak)")
            body.append("Content-Disposition: form-data; name=\"\(photo.key)\"; filename=\"\(photo.fileName)\"\(lineBreak)")
            body.append("Content-Type: \(photo.mimeType + lineBreak + lineBreak)")
            body.append(photo.data)
            body.append(lineBreak)
        }
    }

    body.append("--\(boundary)--\(lineBreak)")

    return body
}

extension Data {
    mutating func append(_ string: String) {
        if let data = string.data(using: .utf8) {
            append(data)
        }
    }
}

struct Media {
    let key: String
    let fileName: String
    let data: Data
    let mimeType: String

    init?(withImage image: UIImage, forKey key: String) {
        self.key = key
        self.mimeType = "image/jpg"
        self.fileName = "\(arc4random()).jpeg"

        guard let data = image.jpegData(compressionQuality: 0.5) else { return nil }
        self.data = data
    }
}
pes8fvy9

pes8fvy92#

1.标头中的Content-Type错误。它应如下所示:

var request = URLRequest(url: imageUploadEndpoint!)

let boundary = "Boundary-\(UUID().uuidString)"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

1.您需要基于对象中的每个字段形成主体,就像您在Alamofire示例中向multipartFormData添加值一样(您在那里使用了字典)

let body = NSMutableData()

let boundaryPrefix = "--\(boundary)\r\n"

for (key, value) in parameters {
    body.appendString(boundaryPrefix)
    body.appendString("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n")
    body.appendString("\(value)\r\n")
}

body.appendString(boundaryPrefix)
body.appendString("Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n")
body.appendString("Content-Type: \(mimeType)\r\n\r\n")
body.append(imageData)
body.appendString("\r\n")
body.appendString("--".appending(boundary.appending("--")))

向数据添加字符串的助手:

extension NSMutableData {
    func appendString(_ string: String) {
        let data = string.data(using: .utf8)
        append(data!)
    }
}
bkkx9g8r

bkkx9g8r3#

这是一个多部分的编码器,Custom Swift Encoder/Decoder for the Strings Resource Format,你可以直接转换成多部分的表单数据。把编码后的数据附加到你的主体数据上。

import Foundation

/// An object that encodes instances of a data type
/// as strings following the simple strings file format.
public class MultipartEncoder {
    
    var boundary: String = ""
    /// Returns a strings file-encoded representation of the specified value.
    public func encode<T: Encodable>(_ value: T) throws -> Data {
        let multipartencoding = MultipartEncoding()
        try value.encode(to: multipartencoding)
        return dataFromFormat(from: multipartencoding.data.strings)
    }
    
    private func dataFromFormat(from strings: [String: String]) -> Data {
        let lineBreak = "\r\n"
        //return dotStrings.joined(separator: "\n")
        var fieldData = Data()
        for (key, value) in strings{
            fieldData.append("--\(boundary + lineBreak)")
            fieldData.append("Content-Disposition: form-data; name=\"\(key)\"\(lineBreak + lineBreak)")
            fieldData.append(value)
            fieldData.append(lineBreak)
        }
        
        
        print("multipartdata \(String(data: fieldData, encoding: .ascii) )")
        
        return fieldData as Data
    }
}

fileprivate struct MultipartEncoding: Encoder {
    
    /// Stores the actual strings file data during encoding.
    fileprivate final class dictData {
        private(set) var strings: [String: String] = [:]
        
        func encode(key codingKey: [CodingKey], value: String) {
            let key = codingKey.map { $0.stringValue }.joined(separator: ".")
            strings[key] = value
        }
    }
    
    fileprivate var data: dictData
    
    init(to encodedData: dictData = dictData()) {
        self.data = encodedData
    }

    var codingPath: [CodingKey] = []
    
    let userInfo: [CodingUserInfoKey : Any] = [:]
    
    func container<Key: CodingKey>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> {
        var container = StringsKeyedEncoding<Key>(to: data)
        print("in container keyed")
        container.codingPath = codingPath
        return KeyedEncodingContainer(container)
    }
    
    func unkeyedContainer() -> UnkeyedEncodingContainer {
        var container = StringsUnkeyedEncoding(to: data)
        container.codingPath = codingPath
        return container
   }
    
    func singleValueContainer() -> SingleValueEncodingContainer {
        var container = StringsSingleValueEncoding(to: data)
        container.codingPath = codingPath
        return container
    }
}

fileprivate struct StringsKeyedEncoding<Key: CodingKey>: KeyedEncodingContainerProtocol {

    private let data: MultipartEncoding.dictData
    
    init(to data: MultipartEncoding.dictData) {
        self.data = data
    }
    
    var codingPath: [CodingKey] = []
    
    mutating func encodeNil(forKey key: Key) throws {
        data.encode(key: codingPath + [key], value: "nil")
    }
    
    mutating func encode(_ value: Bool, forKey key: Key) throws {
        data.encode(key: codingPath + [key], value: value.description)
    }
    
    mutating func encode(_ value: String, forKey key: Key) throws {
        data.encode(key: codingPath + [key], value: value)
    }
    mutating func encode(_ value: Date, forKey key: Key) throws {
        var formatter = getDayFormatter()
        print("value is \(formatter.string(from: value))")
        data.encode(key: codingPath + [key], value:  formatter.string(from: value))
    }
    mutating func encode(_ value: Double, forKey key: Key) throws {
        data.encode(key: codingPath + [key], value: value.description)
    }
    
    mutating func encode(_ value: Float, forKey key: Key) throws {
        data.encode(key: codingPath + [key], value: value.description)
    }
    
    mutating func encode(_ value: Int, forKey key: Key) throws {
        data.encode(key: codingPath + [key], value: value.description)
    }
    
    mutating func encode(_ value: Int8, forKey key: Key) throws {
        data.encode(key: codingPath + [key], value: value.description)
    }
    
    mutating func encode(_ value: Int16, forKey key: Key) throws {
        data.encode(key: codingPath + [key], value: value.description)
    }
    
    mutating func encode(_ value: Int32, forKey key: Key) throws {
        data.encode(key: codingPath + [key], value: value.description)
    }
    
    mutating func encode(_ value: Int64, forKey key: Key) throws {
        data.encode(key: codingPath + [key], value: value.description)
    }
    
    mutating func encode(_ value: UInt, forKey key: Key) throws {
        data.encode(key: codingPath + [key], value: value.description)
    }
    
    mutating func encode(_ value: UInt8, forKey key: Key) throws {
        data.encode(key: codingPath + [key], value: value.description)
    }
    
    mutating func encode(_ value: UInt16, forKey key: Key) throws {
        data.encode(key: codingPath + [key], value: value.description)
    }
    
    mutating func encode(_ value: UInt32, forKey key: Key) throws {
        data.encode(key: codingPath + [key], value: value.description)
    }
    
    mutating func encode(_ value: UInt64, forKey key: Key) throws {
        data.encode(key: codingPath + [key], value: value.description)
    }
    
    mutating func encode<T: Encodable>(_ value: T, forKey key: Key) throws {
        if T.self == Date.self{
            var formatter = getDayFormatter()
            print("value is \(formatter.string(from: value as! Date))")
            data.encode(key: codingPath + [key], value:  formatter.string(from: value as! Date))
        }else{
            var stringsEncoding = MultipartEncoding(to: data)
            stringsEncoding.codingPath.append(key)
            try value.encode(to: stringsEncoding)
        }
    }
    
    mutating func nestedContainer<NestedKey: CodingKey>(
        keyedBy keyType: NestedKey.Type,
        forKey key: Key) -> KeyedEncodingContainer<NestedKey> {
        var container = StringsKeyedEncoding<NestedKey>(to: data)
        container.codingPath = codingPath + [key]
        return KeyedEncodingContainer(container)
    }
    
    mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
        var container = StringsUnkeyedEncoding(to: data)
        container.codingPath = codingPath + [key]
        return container
    }
    
    mutating func superEncoder() -> Encoder {
        let superKey = Key(stringValue: "super")!
        return superEncoder(forKey: superKey)
    }
    
    mutating func superEncoder(forKey key: Key) -> Encoder {
        var stringsEncoding = MultipartEncoding(to: data)
        stringsEncoding.codingPath = codingPath + [key]
        return stringsEncoding
    }
}

fileprivate struct StringsUnkeyedEncoding: UnkeyedEncodingContainer {

    private let data: MultipartEncoding.dictData
    
    init(to data: MultipartEncoding.dictData) {
        self.data = data
    }
    
    var codingPath: [CodingKey] = []

    private(set) var count: Int = 0
    
    private mutating func nextIndexedKey() -> CodingKey {
        let nextCodingKey = IndexedCodingKey(intValue: count)!
        count += 1
        return nextCodingKey
    }
    
    private struct IndexedCodingKey: CodingKey {
        let intValue: Int?
        let stringValue: String

        init?(intValue: Int) {
            self.intValue = intValue
            self.stringValue = intValue.description
        }

        init?(stringValue: String) {
            return nil
        }
    }

    mutating func encodeNil() throws {
        data.encode(key: codingPath + [nextIndexedKey()], value: "nil")
    }
    
    mutating func encode(_ value: Bool) throws {
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    }
    mutating func encode(_ value: Date) throws {
        let formatter = getDayFormatter()
        print("value2 is \(formatter.string(from: value))")
        data.encode(key: codingPath + [nextIndexedKey()], value: formatter.string(from: value))
    }
    mutating func encode(_ value: String) throws {
        data.encode(key: codingPath + [nextIndexedKey()], value: value)
    }
    
    mutating func encode(_ value: Double) throws {
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    }
    
    mutating func encode(_ value: Float) throws {
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    }
    
    mutating func encode(_ value: Int) throws {
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    }
    
    mutating func encode(_ value: Int8) throws {
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    }
    
    mutating func encode(_ value: Int16) throws {
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    }
    
    mutating func encode(_ value: Int32) throws {
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    }
    
    mutating func encode(_ value: Int64) throws {
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    }
    
    mutating func encode(_ value: UInt) throws {
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    }
    
    mutating func encode(_ value: UInt8) throws {
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    }
    
    mutating func encode(_ value: UInt16) throws {
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    }
    
    mutating func encode(_ value: UInt32) throws {
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    }
    
    mutating func encode(_ value: UInt64) throws {
        data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
    }
    
    mutating func encode<T: Encodable>(_ value: T) throws {
        var stringsEncoding = MultipartEncoding(to: data)
        stringsEncoding.codingPath = codingPath + [nextIndexedKey()]
        try value.encode(to: stringsEncoding)
    }
    
    mutating func nestedContainer<NestedKey: CodingKey>(
        keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> {
        var container = StringsKeyedEncoding<NestedKey>(to: data)
        container.codingPath = codingPath + [nextIndexedKey()]
        return KeyedEncodingContainer(container)
    }
    
    mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
        var container = StringsUnkeyedEncoding(to: data)
        container.codingPath = codingPath + [nextIndexedKey()]
        return container
    }
    
    mutating func superEncoder() -> Encoder {
        var stringsEncoding = MultipartEncoding(to: data)
        stringsEncoding.codingPath.append(nextIndexedKey())
        return stringsEncoding
    }
}
fileprivate struct StringsSingleValueEncoding: SingleValueEncodingContainer {
    
    private let data: MultipartEncoding.dictData
    
    init(to data: MultipartEncoding.dictData) {
        self.data = data
    }

    var codingPath: [CodingKey] = []
    
    mutating func encodeNil() throws {
        data.encode(key: codingPath, value: "nil")
    }
    
    mutating func encode(_ value: Bool) throws {
        data.encode(key: codingPath, value: value.description)
    }
    
    mutating func encode(_ value: String) throws {
        data.encode(key: codingPath, value: value)
    }
    
    mutating func encode(_ value: Double) throws {
        data.encode(key: codingPath, value: value.description)
    }
    mutating func encode(_ value: Date) throws {
        let formatter = getDayFormatter()
        print("value3 is \(formatter.string(from: value))")
        data.encode(key: codingPath, value: formatter.string(from: value))
    }
    mutating func encode(_ value: Float) throws {
        data.encode(key: codingPath, value: value.description)
    }
    
    mutating func encode(_ value: Int) throws {
        data.encode(key: codingPath, value: value.description)
    }
    
    mutating func encode(_ value: Int8) throws {
        data.encode(key: codingPath, value: value.description)
    }
    
    mutating func encode(_ value: Int16) throws {
        data.encode(key: codingPath, value: value.description)
    }
    
    mutating func encode(_ value: Int32) throws {
        data.encode(key: codingPath, value: value.description)
    }
    
    mutating func encode(_ value: Int64) throws {
        data.encode(key: codingPath, value: value.description)
    }
    
    mutating func encode(_ value: UInt) throws {
        data.encode(key: codingPath, value: value.description)
    }
    
    mutating func encode(_ value: UInt8) throws {
        data.encode(key: codingPath, value: value.description)
    }
    
    mutating func encode(_ value: UInt16) throws {
        data.encode(key: codingPath, value: value.description)
    }
    
    mutating func encode(_ value: UInt32) throws {
        data.encode(key: codingPath, value: value.description)
    }
    
    mutating func encode(_ value: UInt64) throws {
        data.encode(key: codingPath, value: value.description)
    }
    
    mutating func encode<T: Encodable>(_ value: T) throws {
        var stringsEncoding = MultipartEncoding(to: data)
        stringsEncoding.codingPath = codingPath
        try value.encode(to: stringsEncoding)
    }
}

用法:

func imageFormField(named name: String,
                               data: Data,
                               mimeType: String) -> Data {
        var fieldData = Data()

        fieldData.append("--\(boundary)\r\n")
        fieldData.append("Content-Disposition: form-data; name=\"\(name)\";filename=\"photo.jpg\"\r\n")
        fieldData.append("Content-Type: \(mimeType)\r\n")
        fieldData.append("\r\n")
        fieldData.append(data)
        fieldData.append("\r\n")

        return fieldData as Data
    }
let encoder = MultipartEncoder()
            encoder.boundary = self.boundary
            //encoder.dateEncodingStrategy = .formatted(getDayFormatter())
            let jsondata = try encoder.encode(user)
            
            
            bodydata.append(imageFormField(named: "profileUrl", data: image, mimeType: "image/jpeg"))
            bodydata.append(jsondata)
            bodydata.append("--\(boundary)\r\n")

相关问题