iOS UUID のあれこれ (Swift/iOS ≥ 6)
iPlayground 2018から戻ってきました & UUIDについて
前書き:
先週の土日、iPlayground のAppleソフトウェア開発者向けセミナーに参加してきました。このイベントの情報は同僚から教えてもらい、行く前はどんなイベントかよく知りませんでした。

二日間にわたり、全体のイベントとスケジュールはスムーズで、議題内容は以下の通りです:
-
趣味:自転車、枯れたコード、iOS/APIの進化史、ウィリーはどこ?(CoreML Vision)
-
実用的:テスト類 (XCUITest、依存性注入)、SpriteKitでのアニメーション効果の代替案、GraphQL
-
真功夫:Swift、iOS脱獄/Tweak開発、Reduxの深掘り
自転車プロジェクトが印象的でした。iPhoneをセンサーとして使い、自転車のペダルの回転を感知し、ステージ上で自転車に乗りながらスライドを切り替えました(先輩の主な目標はオープンソース版zwiftを作ることで、クライアント/サーバー通信や遅延問題、磁場干渉など多くの落とし穴も共有してくれました)。
朽ち果てたDirty Code;心に響き、思わず苦笑い;技術的負債はこうして積み重なる。開発スケジュールが厳しく、構造が悪い方法で急いで作る。後から引き継いだ人もリファクタリングする時間がなく、どんどん増えていく。最後には本当にこの道を捨てるしかなくなるかもしれない。
測試クラス(XCUITestのデザインパターン)KKBOXの先輩 が、彼らの方法やコードサンプルの詳細、遭遇した問題点や解決策を惜しみなく公開してくれました。このセッションは私たちの仕事に最も役立つ内容の一つです。テスト分野はずっと強化したいと思っていたので、ぜひじっくり研究したいです。
Lighting Talkの部分は客席で聞いていてもぜひ自分も登壇して共有したいと思いました😂 次回は早めに準備をしておこう!
懇親会では、飲み物や食べ物、会場の雰囲気がとても良く、先輩方の率直な話を聞けて、とてもリラックスでき楽しかっただけでなく、多くの職場で役立つソフトスキルも学べました。

台大バックエンドカフェ
初めての開催だと知り、本当に参加できて光栄でした。スタッフの皆さんと講演者の方々、お疲れ様でした!
セミナーに参加する目的は大きく分けて二つあります:幅を広げること、新しい知識を吸収し、生態系を理解し、普段触れない分野に触れること。そして深さを増すこと、すでに経験のある分野については見落としがないか、他の方法がないかを確認することです。
たくさんメモを取ったので、ゆっくり復習して味わいたい。
UUIDのあれこれ
なぜなら、聞いた後すぐに実際にAPPに応用したからです;この講義はZonble先輩が担当し、iPhone OS 2からiOS 12までの話を聞いて圧倒されました;業界に入ったのが遅かったため、私はiOS 11/Swift 4から書き始めたので、AppleがAPIを変更して混乱した時期を経験していません。
UUIDが取得可能から制限されるようになったのは、ある意味合理的です。善意の用途であれば、ユーザーのデバイス識別や広告、第三者が一意性を利用した広告操作に使われます。しかし、悪意のある業者がこの仕組みを使って逆に調べれば、そのスマホの持ち主がどんな人か把握できてしまいます。(例えば、旅行アプリ+台北のバスアプリ+BMWアプリ+育児ケアアプリを入れていれば、頻繁に海外に行き、子どもがいて台北に住んでいると推測できるなどの情報です)さらに、アプリに入力した個人情報と組み合わせれば、どんな利用がされるか想像もつきません。
しかし、これにより多くの正当かつ法を守るユーザーにも影響が及びました。例えば、UUIDをユーザーのデータ復号キーとして使ったり、UUIDをデバイス判定に利用していた場合、大きな影響を受けました。当時のエンジニア先輩たちは本当にすごいと思います。上司やユーザーからの厳しいクレームに対応し、機転を利かせて代替手段を見つけなければなりませんでした。
代替案:
本記事はUUIDを取得してデバイスを一意に識別することを主題としていますが、ユーザーがインストールしているアプリを知る代替方法については、以下のキーワード検索を参考にしてください:UIPasteboard pasteboardWithName: create:(クリップボードを使ってアプリ間で共有)、canOpenURL: info.plist LSApplicationQueriesSchemes(canOpenURLを使ってアプリのインストール有無を確認、info.plistに最大50件まで列挙が必要)
-
MACアドレスをUUIDとして使用することは、後に禁止されました
-
Finger Printing (Canvas/User-Agent…) :詳しくは調べていませんが、この方法は主にSafariとアプリで同じUUIDを生成するために使われます。 Deferred Deep Linking(ディファードディープリンク)で利用されます。
AmIUnique? -
ID entifier F or V endor (IDFV):現在の主流な解決策🏆
AppleはBundle IDのプレフィックスに基づいてUUIDを生成します。同じBundle IDプレフィックスの場合、同じUUIDが生成されます。例えば、com.518.workとcom.518.jobは同じデバイス上で同じUUIDを取得します。
原文のID For Vendorと同様に、同じプレフィックスは同じベンダーのアプリとみなされるため、UUIDの共有が許可されています。
ID エンティファイア F or V エンドユーザー (IDFV):
let DEVICE_UUID:String = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
// デバイスのUUIDを取得。IDFVが利用できない場合は新しいUUIDを生成
注意すべき点:同じベンダーのすべてのAPPを削除してから再インストールすると、新しいUUIDが生成されます(例:com.518.workとcom.518.jobの両方を削除し、再度com.518.workをインストールすると新しいUUIDが生成されます)
同様に、APPが1つだけの場合は、削除して再インストールすると新しいUUIDが生成されます
この特性のため、弊社の他のアプリではKey-Chainを使ってこの問題を解決しています。講演者の先輩の指摘を聞いて、この方法が正しいと確認できました!
フローは以下の通りです:

Key-ChainのUUIDフィールドに値がある場合はその値を取得し、ない場合はIDFAのUUID値を取得して書き戻す
Key-Chain 書き込み方法:
if let data = DEVICE_UUID.data(using: .utf8) {
let query = [
kSecClass as String : kSecClassGenericPassword as String,
kSecAttrAccount as String : "DEVICE_UUID",
kSecValueData as String : data ] as [String : Any]
SecItemDelete(query as CFDictionary) // 既存のアイテムを削除
SecItemAdd(query as CFDictionary, nil) // 新しいアイテムを追加
}
Key-Chain の読み取り方法:
let query = [
kSecClass as String : kSecClassGenericPassword,
kSecAttrAccount as String : "DEVICE_UUID",
kSecReturnData as String : kCFBooleanTrue,
kSecMatchLimit as String : kSecMatchLimitOne ] as [String : Any]
var dataTypeRef: AnyObject? = nil
let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
if status == noErr,let dataTypeRef = dataTypeRef as? Data,let uuid = String(data:dataTypeRef, encoding: .utf8) {
//uuid
}
もし Key-Chain の操作が面倒なら、自分でラップするかサードパーティのライブラリを使うこともできます。
完全なコード:
let DEVICE_UUID:String = {
let query = [
kSecClass as String : kSecClassGenericPassword,
kSecAttrAccount as String : "DEVICE_UUID",
kSecReturnData as String : kCFBooleanTrue,
kSecMatchLimit as String : kSecMatchLimitOne ] as [String : Any]
var dataTypeRef: AnyObject? = nil
let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
if status == noErr,let dataTypeRef = dataTypeRef as? Data,let uuid = String(data:dataTypeRef, encoding: .utf8) {
return uuid
} else {
let DEVICE_UUID:String = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
if let data = DEVICE_UUID.data(using: .utf8) {
let query = [
kSecClass as String : kSecClassGenericPassword as String,
kSecAttrAccount as String : "DEVICE_UUID",
kSecValueData as String : data ] as [String : Any]
SecItemDelete(query as CFDictionary) // 既存のアイテムを削除
SecItemAdd(query as CFDictionary, nil) // 新しいUUIDを保存
}
return DEVICE_UUID
}
}()
他のExtension Targetでも参照が必要なため、直接クロージャパラメータとしてまとめて使用しています。
Post は ZMediumToMarkdown によって Medium から変換されました。



コメント