如何重写Swift方法以Multipart形式上传UIImage为PNG至服务器
解决iOS Swift中Multipart表单上传图片与订单数据的问题
问题分析
你当前的实现核心问题在于把图片数据直接嵌入到JSON字符串里,这完全不符合multipart/form-data的规范。Multipart请求需要用**唯一边界字符串(boundary)**来分隔不同的表单字段,每个字段(不管是文本还是文件)都要有自己的头部描述,再跟上实际内容,不能像普通JSON那样混在一起。
重写后的完整实现
下面是调整后的代码,会把图片转为PNG格式,并且严格按照Multipart表单的要求构建请求:
func placeOrder(withOrder: Order) { // 先做基础数据校验,避免后续崩溃 guard let returnedJobId = UserDefaults.standard.string(forKey: "jobId"), let returnedOrderPrice = UserDefaults.standard.string(forKey: "orderPrice"), let selectedImage = imagePlaceHolder.image, let imageData = selectedImage.pngData() else { print("缺少必填数据或图片转PNG失败") return } let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" let currentDateTime = formatter.string(from: Date()) DispatchQueue.main.async { // 再校验界面输入的字段 guard let date = self.chosenTimeDateTextFieldDisplay.text, !date.isEmpty, let address = self.addressField.text, !address.isEmpty, let phone = self.phoneField.text, !phone.isEmpty, let comments = self.commentsEntryView.text, let user = CoreDataFetcher().returnUser(), let provider = user.provider_id, let userID = user.id, let userType = user.user_type else { print("有必填字段为空,请检查") return } // 1. 生成唯一的boundary字符串,用来分隔表单里的不同字段 let boundary = "OrderFormBoundary-\(UUID().uuidString)" let headers = [ "Content-Type": "multipart/form-data; boundary=\(boundary)", "Cache-Control": "no-cache" ] // 2. 开始构建Multipart请求体 var requestBody = Data() // 先添加所有文本类型的表单字段 let textFields: [String: String] = [ "user_type": userType, "job_id": returnedJobId, "user_id": userID, "provider_id": provider, "order_placing_time": currentDateTime, "order_start_time": date, "order_address": address, "order_phone": phone, "order_comments": comments, "order_price": returnedOrderPrice ] for (fieldName, fieldValue) in textFields { requestBody.append("--\(boundary)\r\n".data(using: .utf8)!) requestBody.append("Content-Disposition: form-data; name=\"\(fieldName)\"\r\n\r\n".data(using: .utf8)!) requestBody.append("\(fieldValue)\r\n".data(using: .utf8)!) } // 添加图片字段,注意指定文件名和Content-Type requestBody.append("--\(boundary)\r\n".data(using: .utf8)!) requestBody.append("Content-Disposition: form-data; name=\"order_image\"; filename=\"order_attachment.png\"\r\n".data(using: .utf8)!) requestBody.append("Content-Type: image/png\r\n\r\n".data(using: .utf8)!) requestBody.append(imageData) requestBody.append("\r\n".data(using: .utf8)!) // 加上结束标记,完成请求体构建 requestBody.append("--\(boundary)--\r\n".data(using: .utf8)!) // 3. 创建并配置请求 guard let apiUrl = URL(string: "http://Api/v2/placeOrder") else { print("API地址无效") return } var request = URLRequest(url: apiUrl) request.httpMethod = "POST" request.allHTTPHeaderFields = headers request.httpBody = requestBody request.timeoutInterval = 10.0 // 4. 发送请求并处理响应 let session = URLSession.shared let dataTask = session.dataTask(with: request) { data, response, error in if let error = error { print("请求出错:\(error.localizedDescription)") return } guard let responseData = data else { print("未收到服务器返回数据") return } // 打印原始响应方便调试 if let responseStr = String(data: responseData, encoding: .utf8) { print("服务器响应:\(responseStr)") } DispatchQueue.main.async { do { let json = try JSON(data: responseData, options: .allowFragments) let status = json["status"].stringValue let answer = json["answer"] if status == "ok", let orderID = answer.int { print("订单提交成功!") let fetcher = CoreDataFetcher() fetcher.addOrderID(orderId: orderID, toOrder: withOrder) print("订单ID已保存到本地") } else { print("订单提交失败,状态:\(status)") } } catch { print("解析响应或保存订单ID失败:\(error.localizedDescription)") } } } dataTask.resume() } }
核心要点说明
- Boundary字符串:用
UUID生成确保唯一性,避免和请求内容冲突,是Multipart请求的核心分隔标识。 - 字段结构:每个字段都要以
--boundary开头,然后是Content-Disposition描述字段类型和名称,文本字段直接加内容,文件字段还要指定文件名和Content-Type。 - 空值校验:新增了多层
guard let校验,提前拦截无效数据,避免运行时崩溃。 - 冗余头部移除:删掉了
postman-token,这是Postman测试专用的,实际业务请求不需要。
内容的提问来源于stack exchange,提问作者Jessica Kimble




