Search code examples
swiftswift3

How to convert Data to hex string in swift


I want the hexadecimal representation of a Data value in Swift.

Eventually I'd want to use it like this:

let data = Data(base64Encoded: "aGVsbG8gd29ybGQ=")!
print(data.hexString)

Solution

  • A simple implementation (taken from How to hash NSString with SHA1 in Swift?, with an additional option for uppercase output) would be

    extension Data {
        struct HexEncodingOptions: OptionSet {
            let rawValue: Int
            static let upperCase = HexEncodingOptions(rawValue: 1 << 0)
        }
    
        func hexEncodedString(options: HexEncodingOptions = []) -> String {
            let format = options.contains(.upperCase) ? "%02hhX" : "%02hhx"
            return self.map { String(format: format, $0) }.joined()
        }
    }
    

    I chose a hexEncodedString(options:) method in the style of the existing method base64EncodedString(options:).

    Data conforms to the Collection protocol, therefore one can use map() to map each byte to the corresponding hex string. The %02x format prints the argument in base 16, filled up to two digits with a leading zero if necessary. The hh modifier causes the argument (which is passed as an integer on the stack) to be treated as a one byte quantity. One could omit the modifier here because $0 is an unsigned number (UInt8) and no sign-extension will occur, but it does no harm leaving it in.

    The result is then joined to a single string.

    Example:

    let data = Data([0, 1, 127, 128, 255])
    // For Swift < 4.2 use:
    // let data = Data(bytes: [0, 1, 127, 128, 255])
    print(data.hexEncodedString()) // 00017f80ff
    print(data.hexEncodedString(options: .upperCase)) // 00017F80FF
    

    The following implementation is faster by a factor about 50 (tested with 1000 random bytes). It is inspired to RenniePet's solution and Nick Moore's solution, but takes advantage of String(unsafeUninitializedCapacity:initializingUTF8With:) which was introduced with Swift 5.3/Xcode 12 and is available on macOS 11 and iOS 14 or newer.

    This method allows to create a Swift string from UTF-8 units efficiently, without unnecessary copying or reallocations.

    An alternative implementation for older macOS/iOS versions is also provided.

    extension Data {
        struct HexEncodingOptions: OptionSet {
            let rawValue: Int
            static let upperCase = HexEncodingOptions(rawValue: 1 << 0)
        }
    
        func hexEncodedString(options: HexEncodingOptions = []) -> String {
            let hexDigits = options.contains(.upperCase) ? "0123456789ABCDEF" : "0123456789abcdef"
            if #available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) {
                let utf8Digits = Array(hexDigits.utf8)
                return String(unsafeUninitializedCapacity: 2 * self.count) { (ptr) -> Int in
                    var p = ptr.baseAddress!
                    for byte in self {
                        p[0] = utf8Digits[Int(byte / 16)]
                        p[1] = utf8Digits[Int(byte % 16)]
                        p += 2
                    }
                    return 2 * self.count
                }
            } else {
                let utf16Digits = Array(hexDigits.utf16)
                var chars: [unichar] = []
                chars.reserveCapacity(2 * self.count)
                for byte in self {
                    chars.append(utf16Digits[Int(byte / 16)])
                    chars.append(utf16Digits[Int(byte % 16)])
                }
                return String(utf16CodeUnits: chars, count: chars.count)
            }
        }
    }