TL;DR
struct APNSPayload: Decodable {
struct APS: Decodable {
struct Alert: Decodable {
let title: String
let body: String
}
let alert: Alert
let badge: Int?
}
struct CustomField: Decodable {
let hogeList: [String]
let fuga: String?
}
let aps: APS
let customField: CustomField?
init(decoding userInfo: [AnyHashable: Any]) throws {
let json = try JSONSerialization.data(withJSONObject: userInfo, options: [])
// snake_caseをcamelCaseに変換する場合
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
self = try decoder.decode(APNSPayload.self, from: json)
}
}
// {
// "aps" : {
// "alert" : {
// "title" : "タイトル",
// "body" : "本文",
// },
// "badge": 9,
// },
// "custom_field": {
// "hoge_list": [
// "piyo"
// ]
// }
// }
let payload: APNSPayload? = try? APNSPayload(decoding: userInfo)
モチベーション
APNS のペイロードデータは REST APIのレスポンスと同様に JSON 形式で表現されるのですが、クライアント側での取得インターフェイスが
var userInfo: [AnyHashable : Any] { get }
となっているため、ペイロード中のカスタムデータを取得するとなると、
guard let aps = userInfo["custom_field"] as? NSDictionary,
let hogeList = aps["hoge_list"] as? [String] else {
return
}
のような 辛い実装が発生することがあります。
これを、よくある REST API のレスポンスモデルへのマッピングのような形で実装したいというのがモチベーションとなります。
概要
stack overflow のSwift read userInfo of remote notification に対する回答そのままです。
userInfo: [AnyHashable: Any]
自体は JSON フォーマットを表現しているため、
-
userInfo
を JSONObject として扱い、JSONSerialization.data
で JSONData に変換 -
JSONDecoder
で JSONData を元にDecodable
へのデコードを試みる
という流れで、 Decodable
に適合した struct
へのマッピングを実現しています。
上述の例ではペイロードのキー名が snake_case で表現されている前提なのですが、実際には APNS ペイロードデータ内で利用されるキー名に応じて、 Decodable
に適合する際に enum CodingKeys: String, CodingKey
を用意する or プロパティ名の調整が必要となってきます。
(APNS標準のペイロードデータでは、 content-available
のように kebab-case が利用されています。)
参考