【Swift】JSONデータをフェッチして、モデルオブジェクトを作成する

# この記事は何？

Swiftコードを使ったWebプログラミングの基本を学ぶ。

- リクエストの作成と送信
- データの受信とデコード
- JSONデータからのモデルオブジェクト作成

> Swiftを基礎から学ぶには
自著、工学社より発売中の「[まるごと分かるSwiftプログラミング](https://amzn.asia/d/7YPWJwl)」をお勧めします。変数、関数、フロー制御構文、データ構造はもちろん、構造体からクロージャ、エクステンション、プロトコル、クロージャまでを基礎からわかりやすく解説しています。

# iTunes Search API

iTunes Search APIを利用すると、iTunes StoreとApple Books Storeのコンテンツを検索できる。
検索できるコンテンツは本、映画、ポッドキャスト、音楽、ミュージックビデオ、オーディオブック、テレビ番組など多岐にわたる。
詳しい情報は、iTunes Search APIの[ドキュメント](https://performance-partners.apple.com/search-api)を参照。

検索リクエストは基本的に、以下のようなURLで構成される。\
例）*https://itunes.apple.com/search?parameterkeyvalue*

実際は、上記URLの *parameterkeyvalue* 部分がクエリに置き換わる。
クエリはキー・値のペア。\
例）*key1=value1*

クエリはアンパサンド記号`&`を使用して、キー・値ペアを連結できる。\
例）*key1=value1&key2=value2&key3=value3*

クエリにはさまざまなキーがある。
以下に、`term`キー、`entity`キー、`limit`キーなどを使った検索クエリの例を示す。
例）デヴィッド・ボウイの曲を検索して、最初の5アイテムを返すURL
*https://itunes.apple.com/search?media=music&term=david%20bowie&entity=song&limit=5*

実際、ブラウザのアドレスバーに上記URLを入力すると、結果のJSONファイルをダウンロードできる。

# リクエストを作成する

Swiftコードを使ってリクエストURLを構築できる。
ここでは、その方法を解説するために「デヴィッド・ボウイの曲を検索して、最初の5アイテムを返すURL」を例に挙げる。
このリクエストURLには、以下のクエリが含まれている。

- *term=david+bowie*
- *media=music*
- *entity=song*
- *limit=5*

```swift:リクエストURLを作成するSwiftコード
import Foundation

// ベースとなるURL
var urlComponents = URLComponents(string: "https://itunes.apple.com/search")!

// クエリ部分
let queryItems = ["term": "david bowie", "media": "music", "entity": "song", "limit": "5"].map {
    URLQueryItem(name: $0.key, value: $0.value)
}
// URLにクエリを設定
urlComponents.queryItems = queryItems

// 構築したリクエストURLは、`URLComponents`インスタンスの`url`プロパティで確認できる。
urlComponents.url   // https://itunes.apple.com/search?media=music&term=david%20bowie&entity=song&limit=5
```

# リクエスト送信
リクエストを送信してWebからデータを取得するには、`URLSession.shared`の`data(from:)`メソッドを呼び出す。
その際、URLを構築した`urlComponents`の`url`プロパティを使指定する。
メソッドの返り値は、定数のタプル`(data, response)`に設定する。
なお、非同期関数なので、メソッドの呼び出しに`try`および`await`をマークする。
そして、コード全体を`Task`ブロックにラップする。
`data(from:)`メソッドが完了したら、得られたデータが有効かどうかを確認する。

```swift:リクエストを送信して、JSONデータを出力する
Task {
    let (data, response) = try await URLSession.shared.data(from: urlComponents.url!)
    
    if let httpResponse = response as? HTTPURLResponse,
       httpResponse.statusCode == 200 {
        print(data)
    }
}
```

`data`オブジェクトをそのまま出力しても、バイナリデータのサイズが示されるだけ。

```console:出力されたJSONデータ
7714 bytes
```

## URLSession型

`URLSession`クラスは、接続先のURLからデータをダウンロード、またはデータをアップロードするためのAPIを提供する。
アプリはこのAPIを使用して、iOSでアプリが実行されていないとき、またはアプリが一時停止している間にバックグラウンドでダウンロードを継続することもできる。

アプリは`URLSession`型インスタンスを作成し、それがデータ転送タスクをグループごとに調整する。
例えば、Webブラウザアプリを作成する場合、タブまたはウィンドウごとに1つのセッションを作成する。
あるいは、インタラクティブなセッションと別に、バックグラウンドダウンロードのためのセッションを作成する場合もある。
アプリは各セッションに、特定のURLリクエストを表す一連のタスクを追加する。

詳細は、`URLSession`の[開発者向けドキュメント](https://developer.apple.com/documentation/foundation/urlsession)を参照。

## data(from:)メソッド

`data(from:)`メソッドは、指定されたURLにコンテンツを探しに行って、レスポンスとデータを非同期的に取得する。
詳細は[こちらのドキュメント](https://developer.apple.com/documentation/foundation/urlsession/3767353-data)を参照。

```swift:data(from:delegate)メソッドの宣言
func data(
    from url: URL, 
    delegate: (URLSessionTaskDelegate)? = nil
) async throws -> (Data, URLResponse)
```

**パラメータ**

- `url`\
リクエストの送信先となるURLのインスタンス。

- `delegate`\
コールバックを受け取るデリゲート。
既定値として`nil`が設定されているので、省略可能。

**返り値**

非同期的に割り当てられる、URLコンテンツを含む`Data`型インスタンスとレスポンスのタプル。

- `Data`\
受信したデータのバイナリ。

- `URLResponse`\
レスポンスに関連付けられたメタデータ。

# データをデコードする

**デコード**とは、ある形式に符号化されたデータを元の状態に戻すこと。
例えば、動画データは記録の際に圧縮形式にエンコードされて、再生時は一時的にデコードされる。

ここでは、Webから取得したデータの具体的な内容を表示するため、文字列にデコードする。
先ほどのコードを、以下のように修正する。

```swift:レスポンスのデータを文字列にデコードする
Task {
    let (data, response) = try await URLSession.shared.data(from: urlComponents.url!)
    
    if let httpResponse = response as? HTTPURLResponse,
       httpResponse.statusCode == 200,
       let string = String(data: data, encoding: .utf8) {
        print(string)
    }
}
```

`String(data:encoding:)`イニシャライザを使って、バイナリデータを文字列にデコードする。
文字コード形式は`.utf8`を指定する。

## init(data:encoding)イニシャライザ

`data`パラメータに渡されたオブジェクトを、Unicode文字に変換した文字列を返す。
変換の際、`encoding`パラメータに指定されたエンコーディングを使用する。
ただし、変換できない場合は`nil`を返す。

```swift:init(data:encoding)イニシャライザの宣言
init?(data: Data, encoding: String.Encoding)
```

データを文字列にデコードしてから、コンソールに出力した結果は以下の通り。

```console:出力結果
{
 "resultCount":5,
 "results": [
{"wrapperType":"track", "kind":"song", "artistId":3296287, "collectionId":697650603, "trackId":697651445, "artistName":"David Bowie & Queen", "collectionName":"Best of Bowie", "trackName":"Under Pressure", "collectionCensoredName":"Best of Bowie", "trackCensoredName":"Under Pressure", "collectionArtistId":551695, "collectionArtistName":"David Bowie", "collectionArtistViewUrl":"https://music.apple.com/us/artist/david-bowie/551695?uo=4", "artistViewUrl":"https://music.apple.com/us/artist/queen/3296287?uo=4", "collectionViewUrl":"https://music.apple.com/us/album/under-pressure/697650603?i=697651445&uo=4", "trackViewUrl":"https://music.apple.com/us/album/under-pressure/697650603?i=697651445&uo=4", 
"previewUrl":"https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview125/v4/88/ed/d2/88edd28f-7aa9-fc46-4ddb-0c4e9a5fd7b2/mzaf_7010235254143732396.plus.aac.p.m4a", "artworkUrl30":"https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/30x30bb.jpg", "artworkUrl60":"https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/60x60bb.jpg", "artworkUrl100":"https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/100x100bb.jpg", "collectionPrice":11.99, "trackPrice":1.29, "releaseDate":"1981-10-26T08:00:00Z", "collectionExplicitness":"explicit", "trackExplicitness":"notExplicit", "discCount":1, "discNumber":1, "trackCount":20, "trackNumber":13, "trackTimeMillis":237520, "country":"USA", "currency":"USD", "primaryGenreName":"Rock", "isStreamable":true}, 
{"wrapperType":"track", "kind":"song", "artistId":551695, "collectionId":697650603, "trackId":697651126, "artistName":"David Bowie", "collectionName":"Best of Bowie", "trackName":"Space Oddity", "collectionCensoredName":"Best of Bowie", "trackCensoredName":"Space Oddity", "artistViewUrl":"https://music.apple.com/us/artist/david-bowie/551695?uo=4", "collectionViewUrl":"https://music.apple.com/us/album/space-oddity/697650603?i=697651126&uo=4", "trackViewUrl":"https://music.apple.com/us/album/space-oddity/697650603?i=697651126&uo=4", 
"previewUrl":"https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview115/v4/8d/34/1d/8d341d3d-978d-8885-4807-ff12d02659ec/mzaf_17330704255932500816.plus.aac.p.m4a", "artworkUrl30":"https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/30x30bb.jpg", "artworkUrl60":"https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/60x60bb.jpg", "artworkUrl100":"https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/100x100bb.jpg", "collectionPrice":11.99, "trackPrice":1.29, "releaseDate":"1969-07-01T07:00:00Z", "collectionExplicitness":"explicit", "trackExplicitness":"notExplicit", "discCount":1, "discNumber":1, "trackCount":20, "trackNumber":1, "trackTimeMillis":316333, "country":"USA", "currency":"USD", "primaryGenreName":"Rock", "isStreamable":true}, 
{"wrapperType":"track", "kind":"song", "artistId":551695, "collectionId":697650603, "trackId":697651447, "artistName":"David Bowie", "collectionName":"Best of Bowie", "trackName":"Let's Dance (Single Version)", "collectionCensoredName":"Best of Bowie", "trackCensoredName":"Let's Dance (Single Version)", "artistViewUrl":"https://music.apple.com/us/artist/david-bowie/551695?uo=4", "collectionViewUrl":"https://music.apple.com/us/album/lets-dance-single-version/697650603?i=697651447&uo=4", "trackViewUrl":"https://music.apple.com/us/album/lets-dance-single-version/697650603?i=697651447&uo=4", 
"previewUrl":"https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview115/v4/f0/f6/ab/f0f6abb6-09d7-ee3d-a0a9-194371032711/mzaf_6603400039706973488.plus.aac.p.m4a", "artworkUrl30":"https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/30x30bb.jpg", "artworkUrl60":"https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/60x60bb.jpg", "artworkUrl100":"https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/100x100bb.jpg", "collectionPrice":11.99, "trackPrice":1.29, "releaseDate":"1983-03-07T08:00:00Z", "collectionExplicitness":"explicit", "trackExplicitness":"notExplicit", "discCount":1, "discNumber":1, "trackCount":20, "trackNumber":14, "trackTimeMillis":245427, "country":"USA", "currency":"USD", "primaryGenreName":"Rock", "isStreamable":true}, 
{"wrapperType":"track", "kind":"song", "artistId":551695, "collectionId":697650603, "trackId":697651127, "artistName":"David Bowie", "collectionName":"Best of Bowie", "trackName":"Changes", "collectionCensoredName":"Best of Bowie", "trackCensoredName":"Changes", "artistViewUrl":"https://music.apple.com/us/artist/david-bowie/551695?uo=4", "collectionViewUrl":"https://music.apple.com/us/album/changes/697650603?i=697651127&uo=4", "trackViewUrl":"https://music.apple.com/us/album/changes/697650603?i=697651127&uo=4", 
"previewUrl":"https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview115/v4/12/57/8d/12578d54-7b2a-0e63-5c89-0f7628cc4b15/mzaf_17685057866611130999.plus.aac.p.m4a", "artworkUrl30":"https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/30x30bb.jpg", "artworkUrl60":"https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/60x60bb.jpg", "artworkUrl100":"https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/100x100bb.jpg", "collectionPrice":11.99, "trackPrice":1.29, "releaseDate":"1971-12-17T08:00:00Z", "collectionExplicitness":"explicit", "trackExplicitness":"notExplicit", "discCount":1, "discNumber":1, "trackCount":20, "trackNumber":2, "trackTimeMillis":214573, "country":"USA", "currency":"USD", "primaryGenreName":"Rock", "isStreamable":true}, 
{"wrapperType":"track", "kind":"song", "artistId":551695, "collectionId":697650603, "trackId":697651436, "artistName":"David Bowie", "collectionName":"Best of Bowie", "trackName":"Heroes (Single Version)", "collectionCensoredName":"Best of Bowie", "trackCensoredName":"Heroes (Single Version)", "artistViewUrl":"https://music.apple.com/us/artist/david-bowie/551695?uo=4", "collectionViewUrl":"https://music.apple.com/us/album/heroes-single-version/697650603?i=697651436&uo=4", "trackViewUrl":"https://music.apple.com/us/album/heroes-single-version/697650603?i=697651436&uo=4", 
"previewUrl":"https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview115/v4/9c/dd/87/9cdd87f6-378e-d0bf-dd77-3dac5e8cf137/mzaf_5403030196739660265.plus.aac.p.m4a", "artworkUrl30":"https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/30x30bb.jpg", "artworkUrl60":"https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/60x60bb.jpg", "artworkUrl100":"https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/100x100bb.jpg", "collectionPrice":11.99, "trackPrice":1.29, "releaseDate":"1975-11-21T08:00:00Z", "collectionExplicitness":"explicit", "trackExplicitness":"notExplicit", "discCount":1, "discNumber":1, "trackCount":20, "trackNumber":10, "trackTimeMillis":217360, "country":"USA", "currency":"USD", "primaryGenreName":"Rock", "isStreamable":true}]
}
```

# データモデルのオブジェクトを作成する

APIから得られたデータは、辞書（`results`キー、値は配列）の構造になっている。
具体的なデータが格納されているのは配列部分。
配列部分を収容するモデルオブジェクトの構造体を作成する。

今回、モデルを定義するために必要なプロパティは以下に絞ることにする。

- `trackName`（曲名）
- `artistName`（アーティスト名）
- `kind`（メディアの種類）
- `artworkUrl100`（アートワークのURL）

これらのプロパティを使用して、以下のように`StoreItem`構造体を定義する。

```swift:データモデルのStoreItem型
struct StoreItem: Codable {
        let name: String
        let artist: String
        let kind: String
        let artworkURL: String
        
        enum CodingKeys: String, CodingKey {
            case name = "trackName"
            case artist = "artistName"
            case kind
            case artworkURL = "artworkUrl100"
        }
}
```

`StoreItem`型は`Codable`プロトコルに適合させる。
そして、`CodingKeys`列挙型を使用してJSONキーをモデル上のより適切なプロパティ名にマッピングする。

## レスポンスからモデルインスタンスを作成する

JSONレスポンスからモデルインスタンスを作成するための値を取得する方法を検討する。
モデルインスタンスの作成に必要なデータは、`results`キーにネストされている。
そのデータ形式は、モデルの配列である`[StoreItem]`型。

そのためには、レスポンス自体を`SearchResponse`型として受け取る。

```swift:データモデルを配列に格納するSearchResponse型
struct SearchResponse: Codable {
    let results: [StoreItem]
}
```

# JSONDecoderを利用する

`JSONDecoder`は、JSONオブジェクトからデータ型のインスタンスをデコードできる。
ただし、`JSONDecoder`インスタンスを使用してデコードを行うには、型がCodableに準拠すること。

```swift:デコーダーを使って、JSONオブジェクトをSearchResponse型インスタンスにデコードする
Task {
    let (data, response) = try await URLSession.shared.data(from: urlComponents.url!)
    
    if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
        // デコーダーを使ってモデルオブジェクトにデコードする
        let decoder = JSONDecoder()
        let searchResponse = try decoder.decode(SearchResponse.self, from: data)

        // モデルオブジェクトの配列を出力する
        searchResponse.results.forEach { item in
            print("""
                  Name: \(item.name)
                  Artist: \(item.artist)
                  Kind: \(item.kind)
                  Artwork URL: \(item.artworkURL)
                  
                  """)
        }
    }
}
```

モデルオブジェクトの作成は、デコーダーの`decode(:from:)`メソッドによってデコードと同時に行われている。
以下は、作成されたモデルオブジェクトの出力。

```console:出力されたモデルオブジェクト
Name: Under Pressure
Artist: David Bowie & Queen
Kind: song
Artwork URL: https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/100x100bb.jpg

Name: Space Oddity
Artist: David Bowie
Kind: song
Artwork URL: https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/100x100bb.jpg

Name: Let's Dance (Single Version)
Artist: David Bowie
Kind: song
Artwork URL: https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/100x100bb.jpg

Name: Changes
Artist: David Bowie
Kind: song
Artwork URL: https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/100x100bb.jpg

Name: Dancing In the Street
Artist: David Bowie & Mick Jagger
Kind: song
Artwork URL: https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/100x100bb.jpg
```


## decode<T>()メソッド

指定された型にJSONオブジェクトをデコードして、返す。
ただし、元データが有効なJSONでなかった場合、メソッドは`DecodingError.dataCorrupted(_:)`エラーをスローする。
JSONのデコードに失敗した場合は、それに起因するエラーをスローする。

```swift:decode()メソッドの宣言
func decode<T>(
    _ type: T.Type,
    from data: Data
) throws -> T where T : Decodable
```

**パラメータ**

- `type`
JSONオブジェクトをデコードした結果となる型

- `data`
デコードしたいJSOオブジェクト。

**返り値**

デコーダーが`data`を解析できた場合、`type`パラメータに指定した型の値を返す。


# フェッチ機能を抽象化する

リクエストの作成、レスポンスの解析、デコードといったネットワーキング・コードを抽象化するために、フェッチ関数を定義する。

この新しい`fetchItems()`関数は、クエリ辞書を受け取り、`StoreItems`の配列を返す。
 非同期的な通信とエラー対処のため、宣言には`async`と`throws`をマークする。

```swift:fetchItems(matching:)関数の宣言
func tetchItems(
    matching query: [String: String]
) async throws -> [StoreItem] 
```

この関数はクエリを受け取って、URLを作成する。
そして、リクエストが完了すると、ステータスコードをチェックした上で、JSONをモデルオブジェクトにデコードする。
具体的には、`fetchItems(matching)`関数を以下のように実装できる。

```swift:fetchItems(matching:)関数の実装
// ErrorとLocalizedErrorプロトコルを採用する列挙型
enum StoreItemError: Error, LocalizedError {
    case itemsNotFound
}

func tetchItems( matching query: [String: String] ) async throws -> [StoreItem] {
    // 変数urlComponentsの宣言をここに移動
    var urlComponents = URLComponents(string: "https://itunes.apple.com/search")!
    // 渡されたqueryパラメータからqueryItemsを設定する。
    let queryItems = query.map {
        URLQueryItem(name: $0.key, value: $0.value)
    }
    urlComponents.queryItems = queryItems

    // リクエストを送信するために、URLSession.sharedのdata()メソッドを呼び出す
    let (data, response) = try await URLSession.shared.data(from: urlComponents.url!)
    
    // レスポンスのステータスコードが200でなければ、エラーをスローして終了
    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
        throw StoreItemError.itemNotFound
    }
 
    // JSONDecoderを作成し、`SearchResponse`オブジェクトにデコードする
    let decoder = JSONDecoder()
    let searchResponse = try decoder.decode(SearchResponse.self, from: data)
    
    return searchResponse.results
}
```



## フェッチ関数を呼び出す

```swift:
let query = ["term": "david bowie",
             "media": "music",
             "entity": "song",
             "limit": "5"]

Task {
    do {
        let storeItems = try await fetchItems(matching: query)
        storeItems.forEach { item in
            print(
                 """
                 Name: \(item.name)
                 Artist: \(item.artist)
                 Kind: \(item.kind)
                 Artwork URL: \(item.artworkURL)

                 """)
        }
    } catch {
        print(error)
    }
}
```

実行した結果は、以下の通り。

```console:出力結果
Name: Under Pressure
Artist: David Bowie & Queen
Kind: song
Artwork URL: https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/100x100bb.jpg

Name: Space Oddity
Artist: David Bowie
Kind: song
Artwork URL: https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/100x100bb.jpg

Name: Let's Dance (Single Version)
Artist: David Bowie
Kind: song
Artwork URL: https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/100x100bb.jpg

Name: Changes
Artist: David Bowie
Kind: song
Artwork URL: https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/100x100bb.jpg

Name: Heroes (Single Version)
Artist: David Bowie
Kind: song
Artwork URL: https://is2-ssl.mzstatic.com/image/thumb/Features115/v4/fc/65/98/fc6598f9-706b-48a2-dc37-9854f703b21e/dj.xnockfyi.jpg/100x100bb.jpg
```



# 完成

```swift:全体のコード
import Foundation

struct StoreItem: Codable {
    let name: String
    let artist: String
    let kind: String
    let artworkURL: String
    
    enum CodingKeys: String, CodingKey {
        case name = "trackName"
        case artist = "artistName"
        case kind
        case artworkURL = "artworkUrl100"
    }
}

struct SearchResponse: Codable {
    let results: [StoreItem]
}

enum StoreItemError: Error, LocalizedError {
    case itemsNotFound
}

func fetchItems(matching query: [String: String]) async throws -> [StoreItem]  {
    var urlComponents = URLComponents(string: "https://itunes.apple.com/search")!
    urlComponents.queryItems = query.map {
        URLQueryItem(name: $0.key, value: $0.value)
    }
    
    let (data, response) = try await URLSession.shared.data(from: urlComponents.url!)
    
    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
        throw StoreItemError.itemsNotFound
    }
    
    let decoder = JSONDecoder()
    let searchResponce = try decoder.decode(SearchResponse.self, from: data)
    
    return searchResponce.results
}

let query = ["term": "david bowie",
             "media": "music",
             "entity": "song",
             "limit": "5"]

Task {
    do {
        let storeItems = try await fetchItems(matching: query)
        storeItems.forEach { item in
            print(
                """
                Name: \(item.name)
                Artist: \(item.artist)
                Kind: \(item.kind)
                Artwork URL: \(item.artworkURL)

                """)
        }
    } catch {
        print(error)
    }
}
```