![](/img/trans.png)
[英]Upload Image and Parameters to API with URLSession.shared.uploadTask
[英]How to upload image file using Codable and URLSession.shared.uploadTask (multipart/form-data) in Swift?
我想使用某个 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 编码的 Codable 结构。
struct OrderUpload: Codable {
let order_ext_id: String
let description: String
let file: String
}
虽然在这个演示中我的方法可能并不完全合适并且我不处理响应状态代码,但 Alamofire 方法效果很好。
为什么 URLSession 不能工作?
var request = URLRequest(url: imageUploadEndpoint!)
let boundary = "Boundary-\(UUID().uuidString)"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
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!)
}
}
最后我能够找到解决方案。 来源是: 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
}
}
这是我根据这个答案制作的多部分编码器, Custom Swift 字符串资源格式的编码器/解码器,您可以直接将其转换为多部分表单数据。 Append 编码数据到你的正文数据。
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")
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.