如何在 Swift 中使用 Codable 和 URLSession.shared.uploadTask (multip



我想使用某个 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 方法运行良好。

为什么网址不应该工作?

终于我找到了解决方案。 来源是:网址:多部分表单数据请求 |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 = "rn"
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
}
}
标头
  1. 中的内容类型是错误的。它应如下所示:
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)rn"
for (key, value) in parameters {
body.appendString(boundaryPrefix)
body.appendString("Content-Disposition: form-data; name="(key)"rnrn")
body.appendString("(value)rn")
}
body.appendString(boundaryPrefix)
body.appendString("Content-Disposition: form-data; name="file"; filename="(filename)"rn")
body.appendString("Content-Type: (mimeType)rnrn")
body.append(imageData)
body.appendString("rn")
body.appendString("--".appending(boundary.appending("--")))

向数据添加字符串的帮助程序:

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

这是我根据这个答案制作的多部分编码器, 字符串资源格式的自定义 Swift 编码器/解码器 您可以直接转换为多部分表单数据。 将编码数据附加到您的身体数据中。

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 = "rn"
//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)rn")
fieldData.append("Content-Disposition: form-data; name="(name)";filename="photo.jpg"rn")
fieldData.append("Content-Type: (mimeType)rn")
fieldData.append("rn")
fieldData.append(data)
fieldData.append("rn")
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)rn")

最新更新