Encode and decode a string
String.utf8 gives you the UTF-8 bytes; Data encodes them:
import Foundation
let enc = Data("héllo wörld".utf8).base64EncodedString()
print(enc) // aMOpbGxvIHfDtnJsZA==
if let data = Data(base64Encoded: enc),
let s = String(data: data, encoding: .utf8) {
print(s) // héllo wörld
}
There are two optionals on the decode path. Data(base64Encoded:) is nil for invalid Base64, and String(data:encoding:) is nil when the bytes are not valid UTF-8 (for example [0xFF, 0xFE]). If you prefer replacement characters over nil, use String(decoding: data, as: UTF8.self).
URL-safe Base64
Foundation does not know the URL-safe alphabet, so convert the characters yourself:
let d = Data("<<???>>".utf8)
let b64url = d.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
print(b64url) // PDw_Pz8-Pg
func base64URLDecode(_ s: String) -> Data? {
var b = s.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
b += String(repeating: "=", count: (4 - b.count % 4) % 4)
return Data(base64Encoded: b)
}
print(String(decoding: base64URLDecode("PDw_Pz8-Pg")!, as: UTF8.self)) // <<???>>
The padding step is required: Data(base64Encoded: "aGk") returns nil. JWTs, Sign in with Apple identity tokens and many push-notification payloads use this unpadded form. See Base64 vs Base64URL and padding explained for the details, or inspect a token in the JWT decoder.
Validation: when decoding returns nil
Every invalid input gives the same result, nil, with no error message. These all returned nil:
| Input | Problem |
|---|---|
PDw_Pz8-Pg== | URL-safe characters |
aGk | missing padding |
aGk=!! | junk after padding |
aGk== | too much padding |
a | impossible length |
"aGVs\nbG8K" | newline |
" aGk=" | leading space |
One case passes: aGl= (non-zero padding bits) decodes to hi. A validity check is therefore just Data(base64Encoded: s) != nil. When you need to know why a string fails, the Base64 validator reports the position and reason, and the errors guide explains each one.
For input that has line breaks or stray characters, the .ignoreUnknownCharacters option skips anything outside the alphabet:
Data(base64Encoded: "aGVs\nbG8K", options: .ignoreUnknownCharacters)
// 6 bytes: "hello\n"
Use it for MIME or PEM bodies you trust. On arbitrary input it hides errors, because URL-safe characters are skipped too and the result is silently wrong.
Line wrapping options
base64EncodedString(options:) can wrap lines at 64 or 76 characters. The line ending defaults to CRLF:
let x = Data(String(repeating: "x", count: 60).utf8)
x.base64EncodedString(options: .lineLength76Characters)
// "eHh4eHh4...eHh4\r\neHh4" (76 chars, CRLF, rest)
x.base64EncodedString(options: .lineLength64Characters)
// 64 chars, "\r\n", 16 chars (PEM-style)
For Unix line endings, combine the length option with .endLineWithLineFeed only: [.lineLength76Characters, .endLineWithLineFeed]. Without any option, output is a single line, which is what you want for JSON, URLs and headers. For email, the MIME Base64 encoder shows the 76-character layout.
Encode and decode a file
let url = URL(fileURLWithPath: "hello.txt")
let b64 = try Data(contentsOf: url).base64EncodedString()
print(b64) // aGVsbG8K
try Data(base64Encoded: "aGVsbG8K")!.write(to: URL(fileURLWithPath: "copy.txt"))
Force-unwrapping is fine in a playground; in an app, guard let the decoded Data and report an error.
Large files in chunks
Foundation has no streaming Base64 encoder. Read the file in chunks whose size is a multiple of 3 so no padding lands in the middle:
func encodeFile(from src: URL, to dst: URL) throws {
_ = FileManager.default.createFile(atPath: dst.path, contents: nil)
let input = try FileHandle(forReadingFrom: src)
let output = try FileHandle(forWritingTo: dst)
defer { try? input.close(); try? output.close() }
while let chunk = try input.read(upToCount: 3 * 64 * 1024), !chunk.isEmpty {
try output.write(contentsOf: Data(chunk.base64EncodedString().utf8))
}
}
Encoding a 300 KB test file this way gave the same string as encoding it in one call. read(upToCount:) and write(contentsOf:) need macOS 10.15.4 / iOS 13.4 or later. The size overhead guide explains why the output is about 4/3 of the input.
Pitfalls
- Expecting an error message.
Data(base64Encoded:)only says “no”. Log the input length and the first few characters to debug. - Data URI prefixes. Strings from web views often start with
data:image/png;base64,. Cut at the first comma before decoding, or the result isnil. String(describing: data). PrintingDatashows something like6 bytes, not its contents. Usebase64EncodedString()orString(decoding:as:).- Treating Base64 as secure storage. Use the Keychain for secrets; Base64 is readable by anyone. See encoding vs encryption.
FAQ
Why does Data(base64Encoded:) return nil?
The initializer returns nil for any invalid input instead of throwing: missing padding, URL-safe - or _, spaces, newlines or a data: prefix. Fix the input first, or pass options: .ignoreUnknownCharacters for wrapped input.
Does Swift support Base64URL natively?
Foundation has no URL-safe option. Replace + with - and / with _ after encoding and drop =; reverse that and restore padding before decoding.
How do I convert a UIImage to Base64 in Swift?
Get bytes first, for example image.pngData() or image.jpegData(compressionQuality: 0.8), then call .base64EncodedString() on the result. To check the output, paste it into Base64 to image.