(2021/04/03 更新)
外部サイトからJSON形式のデータを取得する方法を解説します。
環境
この記事の情報は次のバージョンで動作確認しています。
【Swift】5.3.2
【iOS】14.4
【macOS】Big Sur バージョン 11.1
やりたい事
Appleが提供するiTunes APIにアクセスして、次のように「SwiftUI」のキーワードを含む書籍の一覧を取得し表示します。
iTunes APIの仕様
AppleのiTunes APIのサーチ機能は次のようなURLで呼び出します。
1 2 3 |
https://itunes.apple.com/search?term=swiftui&country=jp&media=ebook" |
各引数の意味は次の通りです。
引数 | 説明 |
---|---|
term | 検索キーワード |
country | 対象となる国コード。日本はjp |
media | 取得する商品の種類を指定。電子書籍はebook。 |
結果は次のようなJSON形式で取得されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
{ resultCount, results [ { trackCensoredName, artistViewUrl, trackViewUrl, artworkUrl60, artworkUrl100, fileSizeBytes, formattedPrice, trackId, trackName, releaseDate, genreIds, artistIds, kind, currency, artistId, artistName, genres, price, description } ] } |
全体ソース
はじめに全体ソースをお見せします。
次章以降で各処理の解説をします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
import SwiftUI /// APIから取得する戻り値の型 struct Response: Codable { var results: [Result] } /// 個々の書籍情報の型 struct Result: Codable { var trackId: Int // 書籍データのID var trackName: String? // 書籍タイトル var artistName: String? // 著者名 var formattedPrice: String? // 価格(テキスト形式) } struct ContentView: View { @State private var results = [Result]() // 空の書籍情報配列を生成 var body: some View { NavigationView { List(results, id: \.trackId) { item in VStack(alignment: .leading) { Text(item.trackName ?? "") // 書籍タイトル .font(.headline) Text(item.artistName ?? "") // 著者名 Text(item.formattedPrice ?? "") // 価格 } }.navigationTitle("SwiftUI書籍リスト") }.onAppear(perform: loadData) // データ読み込み処理 } /// データ読み込み処理 func loadData() { /// URLの生成 guard let url = URL(string: "https://itunes.apple.com/search?term=swiftui&country=jp&media=ebook") else { /// 文字列が有効なURLでない場合の処理 return } /// URLリクエストの生成 let request = URLRequest(url: url) /// URLにアクセス URLSession.shared.dataTask(with: request) { data, response, error in if let data = data { // ①データ取得チェック /// ②JSON→Responseオブジェクト変換 let decoder = JSONDecoder() guard let decodedResponse = try? decoder.decode(Response.self, from: data) else { print("Json decode エラー") return } /// ③書籍情報をUIに適用 DispatchQueue.main.async { results = decodedResponse.results } } else { /// ④データが取得できなかった場合の処理 print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")") } }.resume() // タスク開始処理(必須) } } |
データを格納する構造体の定義
次の箇所が、データを格納する構造体の定義部分です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/// APIから取得する戻り値の型 struct Response: Codable { var results: [Result] } /// 個々の書籍情報の型 struct Result: Codable { var trackId: Int // 書籍データのID var trackName: String? // 書籍タイトル var artistName: String? // 著者名 var formattedPrice: String? // 価格(テキスト形式) } |
ResponseはAPIから取得したJSONデータを変換して格納する構造体、Resultは個々の書籍情報が格納される構造体です。
JSON→オブジェクト変換を行う為、どちらの構造体もCodableプロトコルに準拠させます。
Codableについて知りたい場合は、別の記事「【Swift】Codableを使ったJSON変換」を参照して下さい。
いずれもAPIからの戻り値の中で、今回のアプリに必要な項目のみ定義しています。
ここに定義されていない項目は、JSON→オブジェクト変換の際に切り捨てられます。
Result構造体で、trackId以外の3プロパティは必ずしも存在していない可能性があるので、オプショナル型としています。
検索結果表示(UI)の定義
検索結果をリスト形式で表示するUI定義部です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
struct ContentView: View { @State private var results = [Result]() // 空の書籍情報配列を生成 var body: some View { NavigationView { List(results, id: \.trackId) { item in VStack(alignment: .leading) { Text(item.trackName ?? "") // 書籍タイトル .font(.headline) Text(item.artistName ?? "") // 著者名 Text(item.formattedPrice ?? "") // 価格 } }.navigationTitle("SwiftUI書籍リスト") }.onAppear(perform: loadData) // データ読み込み処理 } |
検索結果を格納する空の配列(results)を生成し、バインドしています。
.onAppearモディファイアで、NavigationViewが表示された際に、loadData()メソッドを呼び出し、バインドされた配列に検索結果をロードします。
リクエストURL(URLRequest)の生成
次がリクエストURLの生成部です。
1 2 3 4 5 6 7 8 9 10 |
/// URLの生成 guard let url = URL(string: "https://itunes.apple.com/search?term=swiftui&country=jp&media=ebook") else { /// 文字列が有効なURLでない場合の処理 return } /// URLリクエストの生成 let request = URLRequest(url: url) |
まずURL文字列からURLクラスを生成し、それを使ってURLリクエストにします。
URLクラスは引数に指定した文字列が有効なURLで無い場合はnilを返すので、guard文を使ってエラー時の処理を記述します。
URLRequestは、URLの読み込み方法を制御する為に様々なカスタマイズが可能です(例えばGETではなくPOSTを使う等)が、今回はデフォルトのまま使います。
リクエストの実行
前章で生成したURLリクエストを実行する部分です。
1 2 3 4 5 6 7 8 |
/// リクエストの実行 URLSession.shared.dataTask(with: request) { data, response, error in /// アクセス完了時の処理を記述 /// }.resume() // タスク開始処理(必須) |
URLSessionは、ネットワーク要求の管理を担当するiOSクラスです。
必要に応じて独自のセッションも作成できますが、sharedプロパティを使うとiOSが使用するために作成した共有セッションが使用できます。この共有セッションは基本的なリクエストを実行するのに適した標準セッションです。
dataTaskメソッドは、URLコンテンツの取得タスクを作成します。引数にはURLRequest、タスク完了時の処理をクロージャーで指定します。
クロージャーには次の3つのパラメーターが渡ります。
data
サーバーから返されたデータ
response
URLアクセスのレスポンス情報を格納したURLResponseオブジェクト
error
リクエストが失敗した理由を示すエラーオブジェクト。リクエストが成功した場合はnil。
リクエストが正常に完了すると、dataにリソースデータが返され、errorはnilになります。
リクエストが失敗した場合は、dataはnil、errorには失敗に関する情報が返されます。
responseには、リクエストが成功したか失敗したかに関係なく、レスポンス情報が含まれます。
重要ポイント
URLSessionオブジェクトによって生成されたタスクは自動的にバックグラウンドスレッドで実行されます。
これによりUIのインタラクティブ性を損なわずに、ネットワークリクエストの実行が可能となります。
なお、新しく初期化されたタスクは一時停止状態で開始されるため、必ず最後にresume()メソッドを呼び出してタスクを開始する必要があります。
リクエストタスク完了時の処理
URLリクエスト実行後の処理です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
if let data = data { // ①データ取得チェック /// ②JSON→Responseオブジェクト変換 let decoder = JSONDecoder() guard let decodedResponse = try? decoder.decode(Response.self, from: data) else { print("Json decode エラー") return } /// ③書籍情報をUIに適用 DispatchQueue.main.async { results = decodedResponse.results } } else { /// ④データが取得できなかった場合の処理 print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")") } |
「①データ取得チェック」
サーバーからデータが取得できたか否かをチェックします。
"if let"は対象プロパティのnil判定とアンラップ処理を同時に行う、Swiftの特徴的な記述方法です。
「②JSON→Responseオブジェクト変換」
取得したJSONデータをSwiftのオブジェクトに変換します。
詳しい解説が必要な方は、別の記事「【Swift】Codableを使ったJSON変換」を参照して下さい。
「③書籍情報をUIに適用」
UIにバインドされたresultsプロパティに取得したデータを適用します。
iOSではUI関連の作業はすべてメインスレッドで行うようにして、不要な衝突を避けるのがセオリーです。
その為の仕組みが、DispatchQueue.main.asyncです。
このメソッドは、処理をクロージャーとして指定すると、メインスレッドの処理キューに追加され、現在実行中のUI処理が完了後に処理されます。
バックグラウンドスレッド側は、クロージャーの実行を待たずに、次の処理に移れます。
「④データが取得できなかった場合の処理」
サーバーからデータが取得できなかった場合の処理を記述する箇所です。
エラーオブジェクトが存在する場合はエラーメッセージを、そうでない場合は固定メッセージを出力しています。