Codificación Base64 en Swift: una guía completa
Tienes algo que necesita viajar, y el camino es solo de texto: una API JSON que rechaza los bytes en bruto, un canal de correo que recuerda sus orígenes de 7 bits, una URL que se atraganta con lo que no sabe nombrar, un archivo de configuración que solo acepta los caracteres más llanos. Bienvenido al lado del empaquetado de base64, donde Swift convierte tus bytes en un muro de letras amigable con una llamada a un método, una sobrecarga de aproximadamente un carácter extra por cada tres bytes y unas pocas opciones de envoltura que existen porque dos décadas distintas tenían opiniones sobre la longitud de las líneas.
La página de inicio de este sitio ya explica el formato en detalle (64 caracteres imprimibles, cuatro de ellos por cada tres bytes de entrada, hasta dos caracteres = de padding en el grupo final), así que la charla del formato termina antes de empezar. Dos hechos que llevarte a este artículo: base64 es empaquetar, no bloquear, y el empaquetado expande tus datos un 33 por ciento aproximado, lo que importa cada vez que estás cerca de un límite de tamaño. En Swift, todo el trabajo pasa por un único tipo, Data, y un único método total, base64EncodedString(options:). La única habilidad real que se requiere es saber qué pasa en los dos pasos que rodean a ese método, porque el método en sí nunca falla. Fallan los pasos.
Un método, cero excusas
Todo lo relacionado con base64 en Swift vive en Data del framework Foundation, y lleva viviendo allí desde los primeros lanzamientos del lenguaje (Apple lista el método desde iOS 8.0, macOS 10.10, tvOS 9.0, watchOS 2.0 y visionOS 1.0). El pipeline es siempre el mismo de tres pasos: mete tu contenido en un Data, llama al método, y despacha la cadena.
import Foundation
let note = "Pack it, wrap it, ship it."
let packed = Data(note.utf8).base64EncodedString()
print(packed) // UGFjayBpdCwgd3JhcCBpdCwgc2hpcCBpdC4=
Dos detalles de esas tres líneas merecen una mirada más cerca. Primero, Data(note.utf8) es el paso silencioso: la vista utf8 puede representar cada escalar Unicode por definición, así que nunca falla, y por eso es el valor por defecto en la mayoría de los ejemplos. El primo que puede fallar, note.data(using:), puede y de hecho contesta nil para algunas codificaciones, y esa decisión entera de "¿qué bytes?" tiene su propia sección más abajo, porque es el primer lugar donde tus datos pueden desaparecer. Segundo, el método en sí es total: siempre contesta, no tiene caso de error, y la única pregunta que te hace es qué envoltura de líneas quieres. También hay un hermano, base64EncodedData(options:), que devuelve el resultado empaquetado como Data de bytes ASCII en vez de una cadena, para pipelines donde la siguiente parada es una API binaria y no un campo de texto.
Y porque la mitad de ustedes llegó vía "mi app de Swift necesita una dependencia base64": no hay nada que instalar. Base64 es parte de Foundation, Foundation es parte del toolchain, y el toolchain llega de la misma forma en todas las plataformas. En macOS es Xcode o las herramientas de línea de comandos; en Linux y Windows es el instalador de swift.org, donde la línea estable actual a fecha de escribir esto es la 6.3.x y el gestor de versiones Swiftly es la puerta de entrada recomendada; y las imágenes oficiales de Docker cubren a la gente de contenedores. Tu Package.swift se queda vacío, y debería.
La primera decisión real: ¿qué bytes?
Antes de que se produzca un solo carácter base64, ya has tomado la decisión que más importa, porque base64 empaca bytes, y una cadena solo es una cadena hasta que eliges su forma de bytes. UTF-8 es el valor por defecto cuerdo y la respuesta correcta para casi todo, pero en el momento en que tus datos vienen de un sistema legado, un protocolo binario o un rincón de Unicode, la elección deja de ser invisible:
import Foundation
let phrase = "héllo"
print(phrase.data(using: .utf8)?.count ?? -1) // 6
print(phrase.data(using: .ascii) == nil) // true
print(phrase.data(using: .utf16)?.count ?? -1) // 12
print(phrase.data(using: .utf16LittleEndian)?.count ?? -1) // 10
print(phrase.data(using: .utf8)!.base64EncodedString())
// aMOpbGxv
print(phrase.data(using: .utf16LittleEndian)!.base64EncodedString())
// aADpAGwAbABvAA==
| Conversión | Bytes de "héllo" | Lo que lleva el base64 |
|---|---|---|
.utf8 |
6 | aMOpbGxv, la grafía que las API modernas esperan |
.ascii |
falla con nil |
el carácter acentuado está por encima de 0x7F y ASCII se lo niega |
.utf16 |
12 | el doble del tamaño de UTF-8, más una marca de orden de bytes de dos bytes que viaja al frente |
.utf16LittleEndian |
10 | la misma palabra sin la etiqueta BOM: 10 bytes, sigue siendo la más pesada de las opciones sin BOM que figuran aquí |
Tres lecciones se esconden en esa salida. La forma data(using:) puede fallar y .ascii es candidata principal al fallo, así que desenvolverla a la fuerza es la forma en que una frase perfectamente buena se convierte en una app caída. La conversión .utf16 simple antepone una marca de orden de bytes de dos bytes (FF FE en una máquina little-endian), y ese BOM viaja hasta tu salida empaquetada y confunde a cualquier decodificador que no lo esperaba. Y la matemática de tamaño no perdona: una elección descuidada de juego de caracteres te cuesta la sobrecarga base64 sobre el doble de datos, así que la pregunta nunca es "¿esto se codificará?" sino "¿qué espera encontrar el otro extremo cuando desempaque?" La regla de oro: ambos extremos del viaje deben ponerse de acuerdo en la forma de bytes antes de que empiece el base64, porque el decodificador no tiene forma de adivinar lo que elegiste y no va a preguntar.
Envoltura: dos hábitos, un parámetro
Las opciones del método van todas sobre el salto de línea, y todas existen porque dos formatos del siglo XX no pudieron ponerse de acuerdo sobre cuánto debe medir una línea de letras. MIME, el estándar de correo de 1996, envuelve el base64 a los 76 caracteres con fines de línea CRLF. PEM, el linaje de Correo con refuerzo de privacidad de 1987, envuelve a los 64 caracteres, y esa es la forma que encuentras dentro de certificados y claves, los bloques -----BEGIN CERTIFICATE----- que tus servidores guardan en un directorio de configuración.
import Foundation
let certBytes = Data((0..<300).map { UInt8($0 % 256) })
let raw = certBytes.base64EncodedString()
let pemStyle = certBytes.base64EncodedString(options: [.lineLength64Characters, .endLineWithLineFeed])
let mimeStyle = certBytes.base64EncodedString(options: [.lineLength76Characters,
.endLineWithCarriageReturn, .endLineWithLineFeed])
print(raw.count) // 400 caracteres en una línea
print(pemStyle.components(separatedBy: "\n").count) // 7 líneas de como máximo 64
print(mimeStyle.components(separatedBy: "\r\n").count) // 6 líneas de como máximo 76
| Opción | Trabajo | Ojo con |
|---|---|---|
.lineLength64Characters |
corta una línea después de 64 caracteres, el hábito de PEM | el fin de línea es CRLF a menos que digas lo contrario |
.lineLength76Characters |
corta una línea después de 76 caracteres, el hábito de MIME | el mismo valor por defecto CRLF |
.endLineWithCarriageReturn |
incluye un retorno de carro en el fin de línea | por sí solo es solo CR, estilo Mac antiguo, y rara vez es lo que quieres |
.endLineWithLineFeed |
incluye un salto de línea en el fin de línea | pasa las dos opciones cuando quieres decir CRLF |
Ahora el valor por defecto que sorprende: pide cualquier opción .lineLength sin elegir un fin de línea, y el fin de línea que recibes es CRLF, el par completo de retorno de carro más salto de línea. El método tiene un estilo de casa, y su estilo de casa es 1996. ¿Quieres solo LF? Págalos explícitamente con .endLineWithLineFeed y nada más. Una regla de casa más para dejar constancia: la última línea nunca recibe un fin de línea al final. Un resultado envuelto termina con su último carácter de datos o con sus pads de =, sin importar qué opciones elegiste, así que puedes concatenar y pegar sin una línea en blanco huérfana al final. Y sin opciones en absoluto, la salida es una única línea ininterrumpida, que es la forma correcta para cuerpos JSON, URLs y payloads de API: el trabajo que una app de Swift moderna hace en realidad la mayor parte del tiempo.
Base64url: una cadena que puede viajar
El alfabeto estándar es un buen ciudadano del JSON y un terrible ciudadano de una URL. En una cadena de consulta, un + se lee como un espacio por el parseo de formularios, un / es un separador de ruta y un = separa las claves de los valores, y por eso codificar por porcentaje el alfabeto estándar lo hace más largo y más feo en vez de más corto. La sección 5 del RFC 4648 existe para arreglar exactamente eso: el "Alfabeto Seguro para URL y Nombres de Archivo", donde + pasa a ser -, / pasa a ser _ y el padding de = suele descartarse porque un pad en una URL normalmente se convierte en %3D, derrotando el propósito. El RFC añade una advertencia que merece un marco: esta codificación "no debe considerarse la misma que la codificación base64". Los IDs de vídeo de YouTube, los JWT y la mayoría de los identificadores de API modernos lo hablan, así que espera usarlo.
import Foundation
extension Data {
var base64URLEncoded: String {
base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}
let tricky = Data("The + / and = trio goes home.".utf8)
print(tricky.base64EncodedString())
// VGhlICsgLyBhbmQgPSB0cmlvIGdvZXMgaG9tZS4=
print(tricky.base64URLEncoded)
// VGhlICsgLyBhbmQgPSB0cmlvIGdvZXMgaG9tZS4
Mira de cerca esa salida: este payload en concreto resultó no producir un + ni un /, así que las dos grafías solo difieren en el padding descartado. Cambia un byte y divergirán en el alfabeto, que es todo el punto. Dos reglas de enfrentamiento. Elige el dialecto una vez, en el límite donde tus datos se encuentran con el mundo exterior, y nunca mezcles alfabetos dentro del mismo documento: un decodificador estándar que recibe base64url (o al revés) o bien rechaza la entrada o, en modos permisivos, borra los caracteres forasteros y te entrega bytes equivocados. Y nombra tu helper con honestidad, para que el siguiente desarrollador sepa que la cadena es base64url y no un error tipográfico. La misma extensión puede acortarse en toolchains más nuevos: las betas más nuevas de los SDK incluyen ahora una opción nativa .base64URLAlphabet que hace el cambio de alfabeto dentro del framework, con una opción .omitPaddingCharacter correspondiente, y Foundation de código abierto lleva las mismas opciones detrás de un marcador de disponibilidad para toolchains posteriores. Hasta que lleguen a tu objetivo mínimo de despliegue, la extensión de cuatro líneas es la respuesta portable, y por construcción seguirá funcionando en todas las plataformas.
JSON y APIs: el base64 que nunca pediste
Este sorprende a más gente que trabaja con Codable, así que se gana su propia sección: la estrategia por defecto de JSONEncoder para una propiedad Data es ya base64. Si un struct Codable tiene un campo Data, el codificador lo empaca automáticamente con base64 estándar, y JSONDecoder lo desempaca automáticamente de vuelta. Sin opción, sin configuración, sin ceremonia.
import Foundation
struct Snapshot: Codable {
let name: String
let icon: Data
}
let snap = Snapshot(name: "cat", icon: Data("🐱".utf8))
let json = try JSONEncoder().encode(snap)
print(String(decoding: json, as: UTF8.self))
// el icono cruzó el cable como "8J+QsQ=="
La propiedad icon cruzó el cable como 8J+QsQ== porque ese es el estilo de casa. Hay alternativas, y las dos que realmente vas a encontrar son .custom, que te entrega los datos y un codificador y te deja decidir la representación, y la más nueva .deferredToData, que se lo deja a la propia instancia de datos. En el momento en que una API quiere base64url en vez del estándar, .custom es donde enchufa tu extensión de la sección anterior:
import Foundation
extension Data {
var base64URLEncoded: String {
base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}
struct Snapshot: Codable {
let name: String
let icon: Data
}
let encoder = JSONEncoder()
encoder.dataEncodingStrategy = .custom { data, enc in
var container = enc.singleValueContainer()
try container.encode(data.base64URLEncoded)
}
let json = try encoder.encode(Snapshot(name: "cat", icon: Data("🐱".utf8)))
print(String(decoding: json, as: UTF8.self))
// el icono cruzó el cable como "8J-QsQ"
Una advertencia que separa una función que funciona de un incidente de producción: una cadena JSON no puede contener un salto de línea en bruto. Si envuelves un payload con una opción .lineLength e interpolas el resultado en un documento JSON sin escapar, no has creado ningún valor JSON; has creado un error de sintaxis con acento base64, y el parser lo demostrará. La salida envuelta pertenece a cuerpos de correo y archivos de certificado. Todo lo que vive dentro de JSON, URLs o cadenas de consulta recibe la cadena simple sin envolver.
Data URIs: la imagen dentro de una cadena
El truco favorito de la web es meter los bytes de un archivo directamente en una URL: data:{mime};base64,{payload}. Construir uno en Swift es una lectura, una codificación y una concatenación de cadenas:
import Foundation
let gif = Data(base64Encoded: "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")!
let uri = "data:image/gif;base64," + gif.base64EncodedString()
print(uri.hasPrefix("data:image/gif;base64,R0lGODlh")) // true
print(gif.count) // 42
El ejemplo reconstruye el famoso GIF transparente de 42 bytes (existen más pequeños y no transparentes, pero este es el que todo el mundo incrusta) en un data URI que un navegador renderizará sin una segunda petición. En las plataformas de Apple la dirección contraria es una línea: el mismo Data que empaquetaste se alimenta directamente a UIImage(data:) o NSImage(data:). El intercambio es el tamaño, y se compone: una imagen de 100 kilobytes se convierte en una cadena de más de 133.000 caracteres antes incluso de añadir el prefijo data:image/png;base64,. Los data URIs brillan para iconos, avatares y activos diminutos, e hinchan en silencio el ancho de banda para fotos destacadas, así que guárdalos para las cosas pequeñas.
JWTs: sellando las dos primeras partes
El lado de codificación de un JSON Web Token son dos sellados más una firma, y el sellado es tu extensión base64url con el padding descartado, que es exactamente lo que el formato exige. La cabecera y el payload son documentos JSON, y ambas partes reciben el mismo tratamiento:
import Foundation
extension Data {
var base64URLEncoded: String {
base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}
func seal(_ text: String) -> String {
Data(text.utf8).base64URLEncoded
}
let header = seal(#"{"alg":"HS256","typ":"JWT"}"#)
let claims = seal(#"{"sub":"42","role":"editor"}"#)
print("\(header).\(claims).signature-here")
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsInJvbGUiOiJlZGl0b3IifQ.signature-here
Dos recordatorios. La tercera parte separada por puntos es una firma criptográfica calculada sobre las dos primeras, y es la única parte del token que ofrece alguna garantía: la cabecera y los claims son JSON plano con abrigo de trinchera, así que los secretos nunca van dentro. Y fíjate en cómo el padding desaparece en seal(): los decodificadores JWT del otro lado (incluido el del artículo hermano) lo devuelven con un relleno de módulo, así que las dos direcciones del viaje se encuentran en terreno común.
Cabeceras HTTP: Basic y el resto
La antigua cabecera Authorization: Basic pide un nombre de usuario y una contraseña, unidos por un signo de dos puntos, empacados con base64 estándar, porque en una cabecera + y / son inofensivos y la cuestión del dialecto no surge:
import Foundation
let credentials = "editor:s3cret"
let header = "Basic " + Data(credentials.utf8).base64EncodedString()
print("Authorization: " + header)
// Authorization: Basic ZWRpdG9yOnMzY3JldA==
La misma nota al pie ruidosa de siempre: el empaquetado por sí solo no aporta seguridad, y la cabecera es tan segura como la conexión HTTPS que la lleva. El primo moderno, Authorization: Bearer, lleva en su lugar un JWT, así que la receta de sellado de la sección de JWT es lo que va por el cable allí. El único lugar donde la cuestión del dialecto sí surge en HTTP es la cadena de consulta: si tu API deja que un identificador viaje en una URL, ese identificador debería ser base64url, o como mínimo base64 estándar codificado por porcentaje, nunca el alfabeto estándar en bruto con su + dejado a merced de ser leído como un espacio.
Adjuntos de correo: el contrato de los 76 caracteres
Cuando tu app produce un adjunto que debe sobrevivir a los orígenes de 7 bits de SMTP, el contrato es el de MIME: base64 envuelto a los 76 caracteres con fines de línea CRLF, y una cabecera Content-Transfer-Encoding: base64 diciéndole al receptor qué esperar. La sección de opciones ya mostró la grafía; aquí está la forma completa de un cuerpo envuelto:
import Foundation
let attachment = Data((0..<400).map { UInt8(65 + $0 % 26) })
let body = attachment.base64EncodedString(options: [.lineLength76Characters,
.endLineWithCarriageReturn, .endLineWithLineFeed])
let lines = body.components(separatedBy: "\r\n")
print(lines.count) // 8 líneas
print(lines.map { $0.count }.max() ?? 0) // 76, la más larga
print(body.hasSuffix("\r\n")) // false, la última línea se queda desnuda
La factura de tamaño de este dialecto es la famosa: el impuesto del alfabeto de 4/3 más un salto de línea cada 76 caracteres aterriza cerca del 137 por ciento del original, y el viejo atajo de la ingeniería de correo "multiplica el original por 1.37 y añade unos 800 bytes de cabeceras" sigue sirviendo para estimar a ojo el tamaño de los adjuntos en un cliente de correo. Es folclore con aritmética correcta, y es el único lugar de este artículo donde la sobrecarga del 33 por ciento crece un segundo decimal.
Configuración, entorno y bases de datos: escondiendo el guion bajo
Existe una clase silenciosa de trabajos donde la única virtud de base64 es que su salida es un juego de caracteres pequeño y predecible: guardar un blob binario o un valor estructurado en un lugar que quiere texto plano. Variables de entorno que deben sobrevivir a un archivo de configuración del shell, columnas en una base de datos que prefiere varchar a blob, un archivo LDAP con su marcador base64, un código QR que lee letras con más fiabilidad que bits. El patrón es el mismo en todas partes: decide los bytes, codifica, guarda la cadena, decodifica en el otro extremo.
import Foundation
struct FeatureFlags: Codable {
var betaToolbar: Bool
var maxRetries: Int
}
do {
let flags = FeatureFlags(betaToolbar: true, maxRetries: 5)
let json = try JSONEncoder().encode(flags)
let storable = json.base64EncodedString()
print(storable)
guard let packed = Data(base64Encoded: storable) else {
print("decode failed, that is odd")
exit(1)
}
let restored = try JSONDecoder().decode(FeatureFlags.self, from: packed)
print(restored.betaToolbar, restored.maxRetries)
} catch {
print(error)
}
Dos trampas viven aquí. La primera es el doble envoltorio: dos capas de integración que ambas codifican "amablemente", así que el valor que guardas es base64 de base64, y el lector que decodifica una vez obtiene un muro de letras y cree que la función está rota. Codifica exactamente una vez, en exactamente un límite, y dilo en un comentario. La segunda es la deriva de dialecto por entorno: si el valor va a viajar alguna vez por una URL, un campo de formulario o un shell que destroza + y /, guarda la grafía base64url en su lugar, porque el juego de caracteres es todo el punto del formato.
Archivos: el recorrido de ida y vuelta del .b64
El trabajo de "convierte este archivo en un archivo de texto .b64" es una lectura, una llamada y una escritura:
import Foundation
let source = URL(fileURLWithPath: "photos/cat.png")
let archive = URL(fileURLWithPath: "photos/cat.b64")
let bytes = try Data(contentsOf: source)
try Data(bytes.base64EncodedString().utf8).write(to: archive)
// más tarde, posiblemente en otro proceso
let packed = try String(contentsOf: archive, encoding: .utf8)
let restored = Data(base64Encoded:
packed.trimmingCharacters(in: .whitespacesAndNewlines))
if let restored = restored {
try restored.write(to: URL(fileURLWithPath: "photos/cat-copy.png"))
} else {
print("the .b64 file was not base64 after all")
}
El trimmingCharacters del viaje de vuelta está allí porque lo que escribió el archivo pudo haber añadido un fin de línea, y el decodificador estricto trata un salto de línea final como un veredicto de nil. Ese recorrido de ida y vuelta regresa byte a byte, lo que debes verificar la primera vez que lo despaches. Para archivos lo bastante grandes como para que el uso de memoria interese, no codifiques todo el buffer de golpe. Base64 tiene una propiedad encantadora que hace exacto el streaming: cada tres bytes de entrada producen cuatro caracteres de salida independientes, así que mientras cada trozo que codifiques sea un múltiplo de tres bytes, la salida concatenada es idéntica a codificar el archivo entero de una vez. Rompe la alineación y la salida cambia, porque un límite de trozo parte un grupo de tres bytes a mitad del flujo:
import Foundation
func streamEncode(_ input: InputStream, output: OutputStream, lineLength: Int = 76) throws {
input.open()
output.open()
defer { input.close(); output.close() }
var buffer = [UInt8](repeating: 0, count: 65_536)
var pending = [UInt8]()
var line = ""
var lineCount = 0
func addText(_ text: String) {
line += text
while line.count > lineLength {
if lineCount > 0 { _ = output.write(Array("\r\n".utf8), maxLength: 2) }
_ = output.write(Array(String(line.prefix(lineLength)).utf8), maxLength: lineLength)
line = String(line.dropFirst(lineLength))
lineCount += 1
}
}
func flushGroup(_ group: [UInt8]) {
addText(Data(group).base64EncodedString())
}
while input.hasBytesAvailable {
let n = input.read(&buffer, maxLength: buffer.count)
if n < 0 { throw CocoaError(.fileReadUnknown) }
if n == 0 { break }
pending.append(contentsOf: buffer[0..<n])
let groups = pending.count / 3
if groups > 0 {
flushGroup(Array(pending[0..<(groups * 3)]))
pending.removeFirst(groups * 3)
}
}
if !pending.isEmpty {
flushGroup(pending)
}
if !line.isEmpty {
if lineCount > 0 { _ = output.write(Array("\r\n".utf8), maxLength: 2) }
_ = output.write(Array(line.utf8), maxLength: line.utf8.count)
}
}
La memoria pico es un buffer de lectura más la línea actual, sin importar el tamaño del archivo, y la salida envuelta coincide exactamente con la grafía de un solo golpe de .lineLength76Characters. La misma regla de múltiplos de tres, con los papeles invertidos, es la que usa el decodificador por streaming del artículo hermano, así que los dos lados del viaje comparten una verdad aritmética.
Payloads grandes y la factura de memoria
Hagamos la aritmética que necesitarás la próxima vez que alguien pregunte "¿podemos pasar esto por base64?". Cada tres bytes de entrada se convierten en cuatro caracteres de salida, así que el tamaño se multiplica por 4/3: un archivo de 100 kilobytes se convierte en una cadena de 133.336 caracteres, un archivo de 10 megabytes en 13.333.336 caracteres, y así sucesivamente. El padding añade como máximo dos caracteres al final, un error de redondeo para cualquier cosa de más de unos pocos bytes, y la entrada vacía es la única exención, donde la oficina de impuestos concede un solo pase gratis y el resultado es la cadena vacía. Tres consecuencias prácticas. Primero, presupuesta antes de empezar: si tu payload ya está cerca de un límite (la zona cómoda de unos 2.000 caracteres de una URL, el contrato de un campo JSON, el ancho de una columna de base de datos), divide el límite entre 1.33 antes de codificar, no después (y entre 1.37 cuando hay envoltura). Segundo, mientras empacas sostienes los bytes originales y la cadena empaquetada al mismo tiempo, así que el conjunto de trabajo es aproximadamente 2,33 veces el original, y las funciones de streaming de arriba son la salida de emergencia cuando ese número deja de ser cómodo. Tercero, el impuesto es de un solo sentido en la práctica: lo pagas cuando empacas y tus bytes regresan cuando alguien desempaca, así que la pregunta real nunca es "¿base64 es caro?" sino "¿el camino de solo texto en el que estoy lo exige?".
Los errores que muerden
- El paso de juego de caracteres que puede fallar.
String.data(using:)puede contestarnil(prueba.asciicon un carácter acentuado), y desenvolverla a la fuerza es el clásico upgrade de una entrada mala a una app caída. Protege la conversión, no solo la llamada base64, que es la parte fácil. - El estilo de casa CRLF. Una opción
.lineLengthsin opción de fin de línea produce CRLF por defecto. Si tu formato quiere solo LF y olvidaste la opción, tu salida lleva retornos de carro que nunca debió tener. - La trampa de solo CR.
.endLineWithCarriageReturnpor sí solo produce fines de línea de solo CR, estilo Mac antiguo. Si querías CRLF (y para MIME es así), pasa las dos opciones de fin de línea. - Envoltura dentro de JSON. Un salto de línea en bruto dentro de una cadena JSON es JSON inválido, punto final. Base64 envuelto interpolado en un documento es un error de sintaxis con acento base64. Mantén la salida envuelta en cuerpos de correo y archivos de certificado.
- El BOM pasajero. La conversión
.utf16simple antepone un BOM de dos bytes que viaja hasta tu salida empaquetada y confunde a los decodificadores que no lo esperaban. Usa.utf16LittleEndiano.utf16BigEndiancuando necesitas UTF-16 sin la etiqueta. - La deriva de dialecto. El estándar y base64url son alfabetos distintos, y el RFC lo dice por escrito. Un
+que sobrevive hasta una cadena de consulta se convierte en un espacio; un-que llega a un decodificador estándar permisivo se borra. Elige el dialecto en el límite y mantenlo. - La mayúscula es una letra. El alfabeto distingue
Adea. Un copiar-pegar plegado en mayúsculas o una llamada entusiasta a poner en mayúsculas corrompe los datos en silencio, porque ambas versiones siguen pasando todas las comprobaciones del alfabeto. Base64 es sensible a mayúsculas y minúsculas de la manera en que lo es un número de pasaporte. - La regla de alineación. Los codificadores de streaming deben cortar los trozos en múltiplos de tres bytes. Un trozo desalineado cambia la salida, y el cambio es silencioso: la cadena sigue decodiéndose, a los datos equivocados.
- El doble envoltorio. Dos capas que ambas codifican producen base64 de base64. El lector que decodifica una vez ve letras donde deberían estar bytes, y el incidente se escribe solo.
- El muro de disponibilidad. Las nuevas opciones nativas (
.base64URLAlphabet,.omitPaddingCharacter) existen en las betas más nuevas de los SDK y en Foundation de código abierto detrás de un marcador de disponibilidad, pero no en cada toolchain que tu CI va a tocar. Si las adoptas, protege las opciones con comprobaciones de disponibilidad para que el mismo código fuente compile en Xcode más antiguos y en Linux. En el toolchain estable actual, la extensión de cuatro líneas compila en todas partes donde las opciones no. - Base64 no es cifrado. Si el requisito es confidencialidad, has elegido la herramienta equivocada por una categoría entera. El trabajo de base64 es hacer que los bytes viajen, y hace exactamente ese trabajo, ni más.
Cómo mandarlo
- Codifica bytes, no deseos. Decide la forma de bytes antes de llamar al método, UTF-8 por defecto y nombrado explícitamente cuando no lo sea, y protege el paso
data(using:)que puede fallar, porque ahí es donde los datos se pierden de verdad. - Sin envolver por defecto, envuelto por contrato. La salida simple de una sola línea es correcta para JSON, APIs y la mayoría de las bases de datos; recurre a las opciones de envoltura de 64/76 solo cuando el formato receptor lo exija, y paga por las dos opciones de fin de línea cuando quieras decir CRLF.
- Un dialecto por límite. Base64 estándar para destinos centrados en texto, base64url para cualquier cosa que toque una URL o un nombre de archivo, y nunca los dos en el mismo documento. Escribe la conversión una vez, nómbrala con honestidad y reutilízala.
- Presupuesta la sobrecarga. Multiplica por 4/3 antes de empezar (por 1,37 cuando entra la envoltura), y procesa en streaming con trozos alineados a 3 bytes cuando el payload sea lo bastante grande como para que el conjunto de trabajo incomode.
- No uses cinta de embalar como candado. Si el requisito es secreto, quédate en el estante de base64 y toma el cifrado en su lugar.
Una breve historia del empaquetado
El alfabeto con el que empacas y las longitudes de línea a las que envuelves son fósiles de cuatro décadas de discusiones sobre cuánto binario puede sobrevivir un camino de solo texto, y la posición de Swift en esa historia es corta pero interesante:
- Los años 80, la era de la misma máquina. Los primeros codificadores de esta familia existían para mover archivos por teléfono entre sistemas que daban por hecho que el otro extremo era una máquina igual a la suya. uuencode en UNIX usaba mayúsculas, dígitos y puntuación, y sus diseñadores encontraron un truco que ahorraba potencia de cálculo: el alfabeto ocupa posiciones ASCII consecutivas, así que codificar era literalmente "sumar 32" sin ninguna tabla de búsqueda. BinHex, el primo nacido en el TRS-80 en 1981, saltó al Apple II, se convirtió en el formato del Macintosh clásico en 1984 y apostó distinto: sus 64 caracteres omiten
7,O,W,g,oy casi la mitad de las minúsculas. - 1987, el alfabeto recibe una dirección. RFC 989, la primera especificación de Correo con refuerzo de privacidad, estandarizó los 64 caracteres exactos que tecleas hoy, envolvió la salida a 64 caracteres por línea, y usó
=para el padding y*para marcar datos codificados pero no cifrados. Cada bloque estilo PEM que alguna vez hayas pegado en la configuración de un servidor es un descendiente de este documento. - 1996, la era liberal. MIME (RFC 2045) tomó el alfabeto para los adjuntos de correo y movió la envoltura a 76 caracteres, añadiendo la regla que hizo segura la producción de envolturas: los decodificadores deberían ignorar los saltos de línea. Los codificadores aprendieron a envolver; los decodificadores aprendieron a perdonar. La opción de 76 caracteres de Swift es un recuerdo vivo de exactamente esa discusión.
- 2003 a 2006, las reglas se endurecen. RFC 3548 (2003) declaró que el padding no debe saltarse (salvo que un formato diga lo contrario) y que los decodificadores deben rechazar caracteres fuera del alfabeto; RFC 4648 (octubre de 2006) cerró la familia y añadió el alfabeto URL-safe, precisamente para que los identificadores largos pudieran vivir en URLs sin codificar por porcentaje cada carácter especial. La convención de "sin padding en el dialecto URL" nació en el mismo documento, porque un carácter de pad en una URL normalmente se convierte en
%3D, lo que derrota el propósito. - 2013 a 2014, la API ya está aquí. La clase
NSDatade Apple llevaba años empacando base64, y la API basada en opciones con las cuatro opciones de envoltura llegó en iOS 7, en 2013, antes de que Swift existiera siquiera. Cuando Swift 1.0 llegó el 9 de septiembre de 2014, heredó un codificador total con cuatro opciones de envoltura y el alfabeto de 64 letras de 1987, y el carácter no ha cambiado desde entonces. - 3 de diciembre de 2015, el toolchain sale del edificio. Swift se abrió ese día, y con él el base64 de Foundation cruzó a Linux y más tarde a Windows. "Codificar base64 en Swift lejos de una máquina de Apple" tiene apenas una década: un invitado muy joven a una fiesta que empezó en 1987.
- 2023 a 2026, la reescritura y el dialecto URL. La reescritura de Foundation (el proyecto swift-foundation) movió
Dataa un núcleo de Swift puro, y en 2025 una propuesta de la comunidad añadió opciones nativas base64url y de omisión de padding. A fecha de escribir esto, las betas más nuevas de los SDK y el toolchain de código abierto distribuyen las opciones de codificación, el resto de la familia madura en Foundation de código abierto detrás de marcadores de disponibilidad, y la extensión de la comunidad sigue siendo el puente portable mientras tanto.
Pequeñas delicias
- Un megabyte se empaqueta a exactamente 1.333.336 caracteres base64, el impuesto de 4/3 más dos caracteres de padding, exacto hasta el dígito. La única entrada que escapa al impuesto por completo es la vacía: nada entra, nada sale.
- El codificador es total de una manera en que el decodificador no lo es. Nunca devuelve nil, nunca lanza, nunca se niega. El único fallo de todo el pipeline vive aguas arriba, en el paso del juego de caracteres, y por eso el método se siente mucho más sereno que su primo.
- Codifica la palabra
hélloen UTF-8 y se convierte enaMOpbGxv; codifícala en UTF-16 little-endian y se convierte enaADpAGwAbABvAA==. Misma palabra, dos pasaportes distintos, ambos válidos, ninguno intercambiable. - La palabra de prueba del mundo base64 es
foobar, y se empaqueta aZm9vYmFy. Si alguna vez has visto un ejemplo base64 en el mundo real, hay buenas probabilidades de que foobar estuviera involucrado. - El famoso GIF transparente de 1x1 es de 42 bytes y abre con la palabra mágica
GIF89a, y por eso el prefijoR0lGODlhaparece en más codebases de la Tierra que casi cualquier otra cadena base64. - Tu struct
Codableprobablemente lleva años enviando base64 sin que te des cuenta: la estrategia por defecto deDatadeJSONEncoderempaca con base64 estándar, y por eso un campoDatacruza el cable como una cadena con padding en vez de como un array de números. - El padding nunca supera dos caracteres, nunca. Un payload de 1 byte termina en
==, un payload de 2 bytes termina en=, y un payload de 3 bytes termina en nada. La gramática entera del grupo final cabe en una uña. - Swift es 27 años más joven que el alfabeto con el que empaca. El lenguaje salió en 2014; las 64 letras se estandarizaron en 1987 y no han cambiado desde entonces.
Esa es la caja de herramientas de empaquetado completa: un método total, un paso que puede fallar que viene antes, cuatro opciones de envoltura con un estilo de casa CRLF, una extensión base64url de cuatro líneas, una regla de alineación de 3 bytes para streaming y una sobrecarga de 4/3 que es el precio de entrada al camino de solo texto. La codificación es donde pagas la factura de base64, y ahora conoces cada partida antes de firmar. En el momento en que das la vuelta al viaje y empiezas a abrir lo que otros empaquetaron, las nil regresan, los veredictos de espacios en blanco y el punto ciego del mando permisivo toman el escenario. El artículo relacionado de decodificación dirige la obra completa en esa mitad del recorrido de ida y vuelta, así que cuando las letras empiecen a llegar, ya sabrás exactamente cómo abrirlas.
Última actualización: 2026-09-08
Artículo relacionado: Decodificación Base64 en Swift: una guía completa