Multipart request con URLSession y async/await en Swift

Un Multipart request se usa cuando se quiere mandar un archivo junto con un objeto JSON, el archivo puede ser un audio, una imagen, un video etc.

Hay muchos tutoriales que te muestran como hacer un multipart request usando Alamofire, que es una librería de terceros para gestionar la capa de red de una aplicación. Sin embargo, hay muy poca información usando URLSession.

En este artículo vamos a realizar un multipart request usando URLSession, sin enfocarnos en crear una capa de red de principio a fin.

Paso 1: Crear protocolo HTTPClient:

Primero se crea un protocolo llamado HTTPClient:

protocol HTTPClient {
func sendMultipartRequest<T: Decodable>responseModel: T.Type, data: Data?) async -> Result<T, Error>
}

En donde T es un modelo genérico Decodable y data es lo que se va a mandar, en este caso un audio.

Paso 2: Crear extensión del HTTPClient:

Ahora se crea una extensión privada del HTTPClient, con una función createBody.

private extension HTTPClient {
func createBody(boundary: String, data: Data, mimeType: String, filename: String) -> Data {
let body = NSMutableData()
let boundaryPrefix = "--\(boundary)\r\n"
body.append(boundaryPrefix)
body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n")
body.append("Content-Type: \(mimeType)\r\n\r\n")
body.append(data)
body.append("\r\n")
body.append("--".appending(boundary.appending("--")))
return body as Data
}
}

Los parámetros que recibe esta función son:

  • boundary: Parámetro que usa el servidor para saber donde empieza y donde termina el valor que se envía.
  • data: lo que se envía al servidor, puede ser un audio, imagen, video, etc.
  • mimeType: forma estandarizada de indicar el formato del archivo que se envía. Para ver los distintos tipos, ingrese en esta página. MimeTypes
  • filename: Nombre del archivo que se envía.

Esta función, retorna el body en formato Data.

Se puede ver que lo primero y lo último que se le agrega al body es el boundary, así de esta forma el servidor sabe cuando empieza y cuando termina.

Luego se agrega el Content-Disposition, Content-Type y la data que se quiere enviar.

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

Paso 3: Crear nueva extensión del HTTPClient protocol con la implementación:

func sendMultipartRequest<T: Decodable>(responseModel: T.Type, data: Data?) async -> Result<T, Error> {
var urlComponents = URLComponents()
urlComponents.scheme = "HTTPS"
urlComponents.host = "TU HOST"
urlComponents.path = "TU PATH"
urlComponents.port = TU PUERTO
guard let url = urlComponents.url, let data = data else {
return .failure(.invalidURL)
}
let filename = "name_\(Date().toString()).wav"
let boundary = "Boundary-\(UUID().uuidString)"
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
request.httpBody = createBody(
boundary: boundary,
data: data,
mimeType: "audio/wav",
filename: filename
)
do {
let (data, response) = try await URLSession.shared.data(for: request, delegate: nil)
guard let response = response as? HTTPURLResponse else {
return .failure(.noResponse)
}
guard let decodedResponse = try? JSONDecoder().decode(responseModel, from: data) else {
return .failure(.decode)
}
return .success(decodedResponse)
} catch {
return .failure(.unknown)
}
}

Ahora se explica paso a paso que hace la función:

  1. Se crea el URLComponents:

    var urlComponents = URLComponents()
    urlComponents.scheme = "HTTPS"
    urlComponents.host = "YOUR HOST"
    urlComponents.path = "YOUR PATH"
    urlComponents.port = YOUR PORT
    guard let url = urlComponents.url, let data = data else {
    return .failure(.invalidURL)
    }

    Aquí se pone la información del endpoint a dónde se quiere mandar el archivo, y luego se comprueba que se tiene un URL correcto antes de continuar. El .failure(.invalidURL) es un tipo de error que se creó para el ejemplo, puedes usar el tipo de error que desees.

  2. Luego se crea el URLRequest:

    let filename = "name_\(Date().toString()).wav"
    let boundary = "Boundary-\(UUID().uuidString)"
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
    request.httpBody = createBody(
    boundary: boundary,
    data: data,
    mimeType: "audio/wav",
    filename: filename
    )

    Primero se crea un URLRequest con el url que se creó previamente con el URLComponents. Luego se define el httpMethod que será de tipo POST y luego el Content-Type de la cabecera HTTP será de tipo multipart/form-data. Por último, se asigna el httpBody haciendo uso de la función createBody que se creó previamente.

  3. Obtener la respuesta del servidor:

Se usa la nueva función que nos proporciona Swift para obtener tanto la Data como el Response.

let (data, response) = try await URLSession.shared.data(for: request, delegate: nil)

Con la data se puede decodificar y obtener el modelo esperado:

guard let decodedResponse = try? JSONDecoder().decode(responseModel, from: data) else {
return .failure(.decode)
}
return .success(decodedResponse)

Y con el response se verifica el estado del mismo. Primero se verifica que el response no es nulo:

guard let response = response as? HTTPURLResponse else {
return .failure(.noResponse)
}

También se puede hacer un switch del statusCode del response devolviendo un tipo de error, para identificar lo que nos llega del servidor:

switch response.statusCode {
case 200...299:
guard let decodedResponse = try? JSONDecoder().decode(responseModel, from: data) else {
return .failure(.decode)
}
return .success(decodedResponse)
case 401...403:
return .failure(.unauthorized)
case 400:
return .failure(.badRequest)
case 500:
return .failure(.internalServer)
default:
return .failure(.unexpectedStatusCode)
}

Para finalizar, usamos la función que creamos:

  1. Se crea una estructura llamada DataProvider que implemente nuestro protocolo HTTPClient:

    struct DataProvider: HTTPClient {
    func postAudio(data: Data) async -> Result<Model, Error> {
    await sendMultipartRequest(responseModel: Model.self, data: data)
    }
    }
  2. Se llama a nuestro DataProvider:

    let result = await DataProvider().postAudio(data: data)

Se puede observar como usando el DataProvider a partir del protocol HTTPClient creado previamente, podemos enviar la data deseada.

Comparte este artículo

Subscríbete a nuestro Newsletter

Mantente al día en el mundo de las aplicaciones móviles con nuestro blog especializado.

Artículos semanales

Todas las semanas artículos nuevos sobre el mundo de las aplicaciones móviles.

No spam

No te enviaremos spam, solo contenido de calidad. Puedes darte de baja cuando quieras.

Contenido de calidad

Nada de contenido generado de manera automática usando ChatGPT.

Recomendaciones

Tips indispensables sobre mejores prácticas y metodologías.

© 2024 AsyncLearn