Swiftでプッシュ通知用のDevice Token(Data型)を16進数文字列へ変換する方法


以下の書き方がオススメです

let token = deviceToken.map { String(format: "%.2hhx", $0) }.joined()

The English version is here: How to convert an APNs token to String from Data?

以下、以前微妙な書き方をしていてSwift 3移行時にうまく変換できなくなってしまった反省や、他の書き方などの紹介となっています。


UIApplicationDelegateapplication(_:didRegisterForRemoteNotificationsWithDeviceToken:)で渡ってくるdeviceToken(デバイストークン)はData型(Swift 2.2まではNSData型)ですが、それを16進数文字列に変換するやり方について紹介します。

僕はSwift 2.2までこんな感じで書いてしまっていました。

let token = (deviceToken.description.trimmingCharacters(in: CharacterSet(charactersIn: "<>")) as NSString).replacingOccurrences(of: " ", with: "")

(詳しくは後述しますが、元々良くない書き方でした。Swiftにまだ慣れてない時にググってとりあえず動くコードで対処しちゃった匂いがします🤔)

Swift 3.0・Xcode 8で実行すると、この結果がおかしくなっていました。

(lldb) po token
"32bytes"

(lldb) po token.description
"32 bytes"

本当は、8a66140da4c2daa37aacc56aaaaa3aed5354329acdec8f766e557f784515da97のような64文字を期待していました。
(そういえば、 http://qiita.com/mono0926/items/df03c61adc56934e2e7a#device-tokenサイズが大きくなる-ios-9リリースタイミングでは無いです- に書いてたDevice Tokenのサイズ大きくなる対応がまだですね🤔)

Swift 3.0での対応法

気に入った順に3通りの解を載せておきます。

mapdeviceTokenの要素を文字列に変換してjoin

let token = deviceToken.map { String(format: "%.2hhx", $0) }.joined()

reduceを使う

上の例と似たような感じですが、mapjoinではなくreduceで処理しました。
初めはこっちで書いちゃいましたが、よく考えたら上の例の方が読みやすいと思って書き換えました。好みの範囲かもしれません(例えば、コードレビューで、reduceよりmapjoinでやった方が良いなどとは僕は指摘しません)。

let token = deviceToken.reduce("") { $0 + String(format: "%.2hhx", $1) }

NSDataにキャストする

Swift 3.0でNSData型のdeviceTokenData値型にブリッジされて、deviceToken.descriptionの結果が変わってしまった、というのがこの問題の本質であり、ではキャストでNSDataに戻すとどうなる?と試したら、適切な結果が得られました。
参考: swift-evolution/0069-swift-mutability-for-foundation.md at master · apple/swift-evolution

let token = ((deviceToken as NSData).description.trimmingCharacters(in: CharacterSet(charactersIn: "<>")) as NSString).replacingOccurrences(of: " ", with: "")

いい加減な対応に見える気がしますが、意外とありな気がします( ´・‿・`)
ただ、descriptionは文字通り説明的な値なので、こういう値を処理に使うのはやはり適切ではないかなと思いました(つまり、元々の書き方が良くなかったと反省)。

UInt8 → 16進数文字列への変換方法

上記処理内に、UInt8 → 16進数文字列への変換処理がありますが、それについても2通り良さそうな書き方思いつきましたが、前者使うべきですねという結論になりました。

Data型のUInt8型要素をdataElementとしたとしてコード例を示します。

Stringinit(format:)イニシャライザーを利用

フォーマット指定子を使った方法です。

String(format: "%.2hhx", dataElement)

Stringinit(_:radix:uppercase:)イニシャライザーを利用

フォーマット指定子は特に今回のように普段あまり使わない種類は、使う時も読む時も分かりにくいですよね。というわけでやっている処理が分かりやすいこちらを採用したいと思ったものの、dataElementが15以下の時に一桁になってしまうのでダメでした(´・︵・`)
Stringinit(format:)イニシャライザーを利用した例では、この桁も考慮された書き方になっています。

String(dataElement, radix: 16)

無理矢理0詰めするとしたら、こんな感じですかね( ´・‿・`)

var s = "00" + String(dataElement, radix: 16)
let range = s.range(of: s)
let end = s.endIndex
let start = s.index(end, offsetBy: -2)
s[start..<end]

というわけで、1つ目の書き方がお勧めですヽ(・ω・`)

個人的には、このように定義して使っています。

extension String {
    public init(deviceToken: Data) {
        self = deviceToken.map { String(format: "%.2hhx", $0) }.joined()
    }
}

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data!) {
    let token = String(deviceToken: deviceToken)
}

http://stackoverflow.com/a/24979958/1524942 にも色々載っていましたが、mapjoin使うのが今思いつく中ではベストの書き方かなと個人的には思いました。