How to Convert Hex to String in Swift (A Practical Guide)

In the world of iOS and macOS development, dealing with data encoding is a common task. Whether you are working with Bluetooth Low Energy (BLE) devices, cryptographic hashes, or simply parsing server responses, you will inevitably encounter hexadecimal strings (Hex). Converting between hexadecimal representations and human-readable strings (or raw Data objects) is a fundamental skill for any Swift developer.

This guide provides a comprehensive, practical walkthrough of how to convert Hex to String and vice versa in Swift. We will cover Swift 5.5+ syntax, handle edge cases, and explore extensions to make your code cleaner and more reusable.

Why Do We Need Hex to String Conversion?

Before diving into the code, let's understand the common scenarios where this conversion is necessary:

  1. Cryptography & Hashing: When you compute a SHA256 hash, the result is usually a Data object. To display it to the user or send it via JSON, you often convert it to a hex string.
  2. BLE Communication: Bluetooth devices often send data in raw hexadecimal format. You need to decode this into readable strings or integers.
  3. QR Code Scanning: Some QR codes encode binary data as hex strings to ensure compatibility across different operating systems.
  4. Debugging: Printing raw Data objects in the console is often unreadable. Converting them to hex makes debugging much easier.

Understanding the Data Types

In Swift, the bridge between Hex and Strings is usually Data (or [UInt8]).

The conversion path looks like this:
Hex String ↔ Data ↔ String

Method 1: Converting Hex String to String

This is the most common request. We want to take a string like "48656c6c6f" and turn it into "Hello".

Step 1: Clean the Input

Hex strings often come with prefixes like 0x or spaces. We need to sanitize them first.

func cleanHexString(_ hex: String) -> String {
    // Remove "0x" prefix if present
    var cleaned = hex.hasPrefix("0x") ? String(hex.dropFirst(2)) : hex
    // Remove spaces and newlines
    cleaned = cleaned.trimmingCharacters(in: .whitespacesAndNewlines)
    return cleaned
} 

Step 2: Convert Hex to Data

We need to convert the hex characters into bytes. A valid hex string must have an even number of characters (each byte is represented by two characters).

func dataFromHexString(_ hex: String) -> Data? {
    let cleaned = cleanHexString(hex)
    guard cleaned.count % 2 == 0 else { return nil }
    
    var data = Data()
    var index = cleaned.startIndex
    
    while index < cleaned.endIndex {
        let nextIndex = cleaned.index(index, offsetBy: 2)
        guard let byte = UInt8(cleaned[index..<nextIndex], radix: 16) else {
            return nil
        }
        data.append(byte)
        index = nextIndex
    }
    return data
} 

Step 3: Convert Data to String

Once we have the Data object, converting it to a String is straightforward, provided we know the encoding (usually .utf8).

func hexToString(_ hex: String) -> String? {
    guard let data = dataFromHexString(hex) else { return nil }
    return String(data: data, encoding: .utf8)
}

// Usage Example
let hexInput = "48656c6c6f20576f726c64"
if let result = hexToString(hexInput) {
    print(result) // Output: "Hello World"
} 

Method 2: Converting String to Hex String

The reverse process is equally important. You might need to send user input to a Bluetooth device in hex format.

Step 1: Get the Data from String

First, we get the UTF-8 representation of the string as Data.

func stringToHex(_ string: String) -> String {
    let data = Data(string.utf8)
    return data.map { String(format: "%02x", $0) }.joined()
}

// Usage Example
let original = "Swift"
let hex = stringToHex(original)
print(hex) // Output: "5377696674" 

Adding Formatting Options

Often, you might want the hex output in uppercase or with spaces for better readability.

extension String {
    func toHex(upperCase: Bool = false, separator: String = "") -> String {
        let data = Data(self.utf8)
        let format = upperCase ? "%02X" : "%02x"
        return data.map { String(format: format, $0) }.joined(separator: separator)
    }
}

// Usage
print("Hello".toHex())               // "48656c6c6f"
print("Hello".toHex(upperCase: true, separator: " ")) // "48 65 6C 6C 6F" 

Method 3: Handling Non-UTF8 Data (ASCII or Custom Encoding)

Not all hex data represents UTF-8 strings. Sometimes it’s ASCII, or sometimes it’s just raw binary data (like a UUID).

ASCII Example

If you know the data is ASCII, you can specify .ascii encoding.

func hexToASCII(_ hex: String) -> String? {
    guard let data = dataFromHexString(hex) else { return nil }
    return String(data: data, encoding: .ascii)
} 

Using String.Encoding

For most Western languages, UTF-8 is sufficient. If you are dealing with legacy systems, you might need .isoLatin1 or .windowsCP1252.

func hexToString(_ hex: String, encoding: String.Encoding) -> String? {
    guard let data = dataFromHexString(hex) else { return nil }
    return String(data: data, encoding: encoding)
} 

Advanced: Creating Swift Extensions

To make your code more Swifty and reusable, it’s best practice to extend the native Data and String types.

Extending Data

Add a computed property to convert Data to a hex string.

extension Data {
    var hexString: String {
        return map { String(format: "%02x", $0) }.joined()
    }
    
    var hexStringUppercase: String {
        return map { String(format: "%02X", $0) }.joined()
    }
} 

Extending String

Add a computed property or method to convert a hex string to Data or a decoded String.

extension String {
    var hexToData: Data? {
        let cleaned = self.replacingOccurrences(of: " ", with: "")
                           .replacingOccurrences(of: "0x", with: "")
        guard cleaned.count % 2 == 0 else { return nil }
        
        var data = Data()
        var index = cleaned.startIndex
        while index < cleaned.endIndex {
            let nextIndex = cleaned.index(index, offsetBy: 2)
            guard let byte = UInt8(cleaned[index..<nextIndex], radix: 16) else {
                return nil
            }
            data.append(byte)
            index = nextIndex
        }
        return data
    }
    
    func hexToString(encoding: String.Encoding = .utf8) -> String? {
        guard let data = self.hexToData else { return nil }
        return String(data: data, encoding: encoding)
    }
} 

Usage with Extensions:

let hex = "4D 61 63 42 6F 6F 6B 20 50 72 6F"
if let decoded = hex.hexToString() {
    print(decoded) // "MacBook Pro"
}

let text = "SwiftUI"
let hexData = text.data(using: .utf8)?.hexString // "53776966745549" 

Common Pitfalls and Error Handling

When working with conversions, your code must be robust to avoid crashes.

1. Odd Length Hex Strings

A hex string like "ABC" is invalid because it contains an odd number of characters. Always validate the length.

2. Invalid Characters

Hex strings should only contain characters 0-9, A-F, a-f. If a user inputs "GHI", the conversion will fail.

3. Encoding Mismatches

If you try to decode UTF-8 data that contains invalid UTF-8 sequences (e.g., random binary data), String(data:encoding:) will return nil.

Example of Robust Function

enum HexConversionError: Error {
    case invalidHexString
    case encodingMismatch
}

func safeHexToString(_ hex: String) -> Result<String, HexConversionError> {
    guard let data = dataFromHexString(hex) else {
        return .failure(.invalidHexString)
    }
    guard let result = String(data: data, encoding: .utf8) else {
        return .failure(.encodingMismatch)
    }
    return .success(result)
} 

Performance Considerations

For most iOS applications, the conversion speed is negligible. However, if you are converting large files (e.g., MBs of hex data), consider these optimizations:

Complete Code Example

Here is a complete, production-ready utility class that encapsulates everything we discussed.

import Foundation

struct HexConverter {
    
    // MARK: - Hex to String
    static func string(fromHex hex: String, encoding: String.Encoding = .utf8) -> String? {
        let cleaned = hex.trimmingCharacters(in: .whitespacesAndNewlines)
                           .replacingOccurrences(of: " ", with: "")
                           .replacingOccurrences(of: "0x", with: "")
        
        guard cleaned.count % 2 == 0 else { return nil }
        
        var bytes = [UInt8]()
        var index = cleaned.startIndex
        
        while index < cleaned.endIndex {
            let nextIndex = cleaned.index(index, offsetBy: 2)
            guard let byte = UInt8(cleaned[index..<nextIndex], radix: 16) else {
                return nil
            }
            bytes.append(byte)
            index = nextIndex
        }
        
        let data = Data(bytes)
        return String(data: data, encoding: encoding)
    }
    
    // MARK: - String to Hex
    static func hex(from string: String, uppercase: Bool = false, separator: String = "") -> String {
        let data = Data(string.utf8)
        let format = uppercase ? "%02X" : "%02x"
        return data.map { String(format: format, $0) }.joined(separator: separator)
    }
    
    // MARK: - Data to Hex
    static func hex(from data: Data, uppercase: Bool = false) -> String {
        let format = uppercase ? "%02X" : "%02x"
        return data.map { String(format: format, $0) }.joined()
    }
}

// Demo
let originalText = "Swift 5.9"
let hexRepresentation = HexConverter.hex(from: originalText)
print("Hex: \(hexRepresentation)") // "537769667420352e39"

if let decodedText = HexConverter.string(fromHex: hexRepresentation) {
    print("Decoded: \(decodedText)") // "Swift 5.9"
} 

Conclusion

Converting between hex and strings in Swift is a routine but critical task. By understanding the underlying Data type and handling optional values safely, you can build robust applications that interact with APIs, hardware, and cryptographic systems.

Remember these key takeaways:

  1. Always clean your hex input (remove 0x, spaces).
  2. Validate length – hex strings must have an even count.
  3. Specify encoding – UTF-8 is standard, but don't assume it for legacy data.
  4. Use extensions to keep your code clean and reusable.

With the techniques provided in this guide, you are now equipped to handle any hex-to-string conversion challenge in your Swift projects.