この記事はニフティグループ Advent Calendar 2018の13日目の記事です。
TL;DR
iOSアプリではWEB APIやローカルのファイル等、外部から情報を取得して画面に表示することが多いです。
それらの情報の中には多くのIDが含まれています。
IDをキーにしてローカルにキャッシュしたり、IDをキーにして検索を実行したり……
さまざまな場面で利用されているIDを型安全に扱う方法として、IDをstructで表現する方法が有名だと思います。
本記事では以下を紹介します。
- IDを型安全に扱う方法のご紹介
- JSON内のIDを型安全に扱えるようdecodeする方法
IDを型安全に扱う方法
GitHubのAPIを参考にご説明します。※説明の都合上、簡略化しています。
以下のようなリポジトリ検索のリクエストを送信した場合のレスポンスはこちらです。
https://api.github.com/search/repositories?q=swift
{
"total_count": 127415,
"incomplete_results": false,
"items": [
{
"id": 44838949, # リポジトリのID
"full_name": "apple/swift",
"owner": {
"login": "apple",
"id": 10639145 # ユーザのID
}
},
{
"id": 27398751,
"full_name": "btrn/Swift",
"owner": {
"login": "btrn",
"id": 8502419
}
}
]
}
このレスポンスの中にIDが2種類以上存在することに気づきましたか?
それぞれ、リポジトリのIDとユーザのIDで、意味が異なるIDです。
こちらをそのままJSONに書かれている通りにSwiftの実装におこすと以下のようになります。
struct Repository {
let id: Int
let fullName: String
let owner: User
}
struct User {
let id: Int
let login: String
}
愚直に実装するとどちらのIDもInt
で表現することになると思います。
ですが、以下のような場合にバグになってしまう可能性があります。
項目 | 情報 |
---|---|
実装したい機能 | リポジトリのお気に入り |
必要な情報 | リポジトリのID(Int ) |
ミス | ユーザのID(Int )を保存してしまった |
原因 | 同じ Int を扱うのでコンパイル時に気づけなかった |
そこで以下のようにIDをstructで表現することによって型安全にすることができます。
struct Repository {
let id: ID
let fullName: String
let owner: User
struct ID {
let rawValue: Int
}
}
struct User: Decodable {
let id: ID
let login: String
struct ID {
let rawValue: Int
}
}
それぞれRepository.ID
型とUser.ID
型の別々の型になり、間違って実装したらコンパイルエラーが発生するようになりました!!💪
// IDをIntで扱うとリポジトリのIDでもユーザのIDでも、どちらでも使える
func save(from id: Int)
// リポジトリのID以外は受け取れない
func save(from id: Repository.ID)
JSON内のIDを型安全に扱えるようdecodeする方法
ここまででIDを型安全に扱う方法をご紹介してきました。
では、実際にGitHubのAPIのレスポンスをDecodable
プロトコルを利用してそれぞれの型に落としていくことを考えたいと思います。
Decodable
プロトコルに準拠
struct Repository: Decodable {
...
struct ID: Decodable {
let rawValue: Int
}
}
struct User: Decodable {
...
struct ID: Decodable {
let rawValue: Int
}
}
コンパイルエラーはでなくなりますが、structで型のネストをしてしまったので、実行時に落ちてしまいます。
現状は以下のようなレスポンスを期待していると解釈されてしまいます。
{
"id": { # 型のネスト
"rawValue": 44838949 # リポジトリのID
},
"full_name": "apple/swift",
"owner": {
"login": "apple",
"id": { # 型のネスト
"rawValue": 10639145 # ユーザのID
}
}
}
型のネストを解決
これを先程のレスポンスをデコードできるようにしつつ、型安全を保つには
RawRepresentable
プロトコルに準拠させます
https://developer.apple.com/documentation/swift/rawrepresentable
enumがIntやStringを継承すると暗黙的にrawValue
というプロパティが出現するのはこのプロトコルのおかげです。
最終的なコードはこのようになります。
struct Repository: Decodable {
let id: ID
let fullName: String
let owner: User
struct ID: RawRepresentable, Decodable {
let rawValue: Int
}
}
struct User: Decodable {
let id: ID
let login: String
struct ID: RawRepresentable, Decodable {
let rawValue: Int
}
}
let data = """
[
{
"id": 44838949,
"full_name": "apple/swift",
"owner": {
"login": "apple",
"id": 10639145
}
},
{
"id": 27398751,
"full_name": "btrn/Swift",
"owner": {
"login": "btrn",
"id": 8502419
}
}
]
""".data(using: .utf8)!
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let repositories = try! decoder.decode([Repository].self, from: data)
まとめ
- iOSアプリではアプリ外からも多くの情報を取得して表示している
- それらの情報にはさまざまなIDが含まれている
- IDは取り間違えるとバグになるため、structを用いて別々の型にすることで、コンパイラによるバグの検出が可能
-
Decodable
プロトコルを用いてstruct化したIDをdecodeするにはIDの型をRawRepresentable
プロトコルに準拠させる
最後に
明日は@5_21maimaiさんが「5年もののiOSアプリで、ついに自動テストを開始した話」をしてくれます!お楽しみに!!