You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何重写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

火山引擎 最新活动