この記事は何?
Swiftコードを使って、アプリで使用するためのWebデータを取得する方法について、基本を学ぶ。
そのために、プレイグラウンド環境でURLSession
を使用するコードを実行する。
何を学ぶ?
- HTTPおよびHTTPSプロトコルとは?
- Web上でデータを送信する最も一般的な方法
- リクエストしたデータをWebサーバに伝えるためにURLを作成する方法
-
Foundation
フレームワークのAPIを使用してデータを送受信するためのタスクを作成する方法
Swiftを基礎から学ぶには
自著、工学社より発売中の「まるごと分かるSwiftプログラミング」をお勧めします。変数、関数、フロー制御構文、データ構造はもちろん、構造体からクロージャ、エクステンション、プロトコル、クロージャまでを基礎からわかりやすく解説しています。
ネットワークの仕組み
Webサイトを開くと、ブラウザはインターネットを介して目的のWebサーバまでネットワークリクエストを送信する。
Webサーバとは
リクエストが来るまで待機して、それに応答するようにプログラムされたコンピュータ。
URL
URLは「uniform resource locator」の略称。
「どのサーバがリクエストを受信するのか」や「どんなレスポンスを取得するか」を指定するのがURL。
例えば、ブラウザのアドレスバーにnintendo.co.jp
と入力すると、任天堂のホームページが表示される。
URLは主に、プロトコルとドメインで構成されている。
より複雑なURLにはサブドメイン、ポート、パス、クエリパラメータなどが含まれることがある。
プロトコル
Webプロトコルは「ブラウザとサーバが相互に通信する方法」の取り決め。
一般的なWEBサイトは、HTTPまたはHTTPSプロトコルを使用している。
HTTPは Hypertext Transfer Protocol の略。
HTTPSは Hypertext Transfer Protocol Secure の略。
HTTPのほうが一般的だったが、より安全なHTTPSプロトコルに移行しつつある。
パス
サーバ上の特定のファイルまたはサブディレクトリを参照する。
例えば、https://www.nintendo.co.jp/hardware/switch/
に移動すると、サーバはレスポンスに「ゲーム機の製品情報を表示するHTML」で応答する。
クエリパラメータ
リクエストに関して、さらに詳細な情報をサーバに伝える。
クエリ自体はSwiftの辞書に似ている。
必要な情報を含むページを生成するためにサーバへ伝える、キー・値ペアのパラメータ。
例えば、https://www.nintendo.co.jp/search/?q=ピクミン&mode=soft
は、検索ページで「ピクミン」を指定した結果を「ソフト」で絞り込んだ情報を表示する。
リクエストの種類
リクエストの種類は一般的に、GETとPOSTがある。
HTTPメソッドとも呼ばれる。
GETはサーバに「データを要求する」ために使用する。
POSTはサーバに「データのボディを送信する」ために使用する。
WEBサイトを読み込むとき、ブラウザはサーバにGETリクエストを送信する。
WEBサイトでフォームを送信すると、ブラウザはサーバにPOSTリクエストを送信する。
URLを作成する
プレイグラウンドで、リクエストを作成して送信するためのSwiftコードを実行する。
そのためには、最初にURLのインスタンスを構築する。
SwiftのURL
型を使用して、ネットワーク・リクエストで使用するURLを構築および変更する。
URL
型オブジェクトはinit?(String: String)
イニシャライザを使って、簡単に作成できる。
import Foundation
let url = URL(string: "https://www.nintendo.co.jp")! // 返り値はオプショナル
URL
型インスタンスのさまざまなプロパティを確認する。
let url = URL(string: "https://www.nintendo.co.jp/hardware/switch/")!
url.scheme // "https:
url.host // "www.nintendo.co.jp"
url.path // "/hardware/switch/"
url.query // nil
scheme
プロパティはプロトコル、host
プロパティはドメインを取得できる。
このインスタンスにクエリはないので、query
プロパティはnil
を返している。
リクエストとレスポンス
ネットワーク・リクエストを作成して送信するには、URLSession
クラスを使用する。
URLSession
型のshared
にアクセスする。
セッションは送信するすべてのリクエストを管理する。
また、セッションは各リクエストが終了したときのコードを実行する。
リクエストのデータ
まずは、URLSession
型のdata(from:delegate:)
非同期スローメソッドを使用して、データを要求する。
from
パラメータにはURL
型インスタンスを指定する。
delegate
パラメータにはオプショナルのURLSessionTaskDelegate
型インスタンスを指定する。
delegate
パラメータはデータが受信されたときにセッションを監視する必要がある場合に便利だが、今回のケースでは不要。
パラメータの既定値はnil
なので、呼び出し時に除外できる。
let (data, response) = URLSession.shared.data(from: url)
// error; 'async' call in a function that does not support concurrency
コンパイラは上のコードに対して、非同期関数の呼び出しに関するエラーを報告する。
非同期の
async
とは何か?
一般的なコードは上の行から下の行へと順番に実行される。
これは同期的な実行と呼ばれる。
一方で、Swiftは非同期的なコードの実行もサポートしている。
async
がマークされた関数は非同期関数。
つまり、コードの実行を一時停止(サスペンド)して、許可された他のコードの実行した後で、サスペンドしたコードを再開できる。
非同期的なコードの実行は並行性とも呼ばれる。
URLSession
を利用するには、並行性の理解が不可欠。
ネットワーク・リクエストがどのように機能するかを理解しておく。
あるアプリがインターネットを介してネットワーク・リクエストを送信すると、アプリにレスポンスが返送される。
このやり取りがどれくらいの速さで完了するは保証されない。
サーバからのレスポンスが非常に大きなファイルかもしれない。
ネットワーク速度が遅いかもしれない。
どちらの原因であっても、レスポンスが返ってくるまで数秒から数十秒続く恐れがある。
ユーザはストレスを感じてしまう。
Swiftの並行性システムはアクターを使用して、「非同期コードをどのように実行するか」を制御する。
原則、iOSはすべてのUIコードを実行するメインアクターを作成する。
メインアクターは、ほとんどのコードを問題なく実行できる。
しかし、メインアクターで時間のかかる操作(リクエスト送信やレスポンス待機など)を同期的に実行すると、タッチやビューの描画を処理するコードを含め、他の実行をブロックしてしまう。
すると、それらの処理が完了するまでの間、アプリはフリーズしたかのように見える。
そこで役立つのが、非同期コード。
時間のかかる作業を待っている間、URLSession
はメインアクターをブロックしないように非同期的にリクエストを送信し、レスポンスを受信する。
一時停止中のコードがレスポンスを待っている間、メインアクターはUIを更新し、ユーザの操作に反応し続けることができる。
非同期関数を呼び出すには2つの手続きがある。
- 非同期的に呼び出す関数に
async
をマークする -
Task
を使用する
プレイグラウンドのコードは何にも囲まれていないので、Task
に非同期関数を入れる。
Task
のイニシャライザは実行コードのクロージャを受け取る。
プレイグラウンドのコードを変更して、Task
にdata(from:delegate:)
関数への呼び出しを入れる。
import Foundation
let url = URL(string: "https://www.nintendo.co.jp")!
Task {
let (data, response) = URLSession.shared.data(from: url)
// error; Call can throw, but it is not marked with 'try' and the error is not handled
// error; Expression is 'async' but is not marked with 'await'
}
先ほどのエラーは解消できたが、別のエラーを報告される。
Swiftコンパイラはawait
キーワードを呼び出しにマークすることを提案している。
非同期関数が作業を実行している間にタスクがサスペンドする可能性のあるコードには、await
キーワードをマークする必要がある。
以下のように、await
をマークする
さらに、コンパイラは新たに別のエラーを報告している。
data(from:delegate:)
メソッドはスロー関数でもあるが、呼び出しコードがtry
でマークされていないから。
改めてメソッド定義を見ると、非同期であることに加えて、data(from:delegate:)
はスロー関数だと分かる。
この関数は、ネットワーク・リクエストの実行中にクライアント側でエラーが発生した場合にエラーをスローする。
もしそうなった場合、自分次第のコードで対処できる。
try
キーワードを追加して、エラーに対処できていることを示す。
なお、受け取ったデータを出力する。
コードは無事にコンパイルできるようになった。
この時点で、定数response
の値を使っていない。
そのため、コンパイラは「Immutable value 'response' was never used; consider replacing with '_' or removing it」と警告している。
Task {
let (data, response) = try await URLSession.shared.data(from: url)
print(data)
}
このプレイグラウンドを実行すると、タスクが作成される。
そして、タスクがサスペンドされている間にdata(from:delegate:)
メソッドが実行される。
データ機能が完了すると、タスクは実行を続行。
定数data
とresponse
に値が割り当てられ、print()
関数が実行される。
大抵の場合、これだけでネットワーク・リクエストを実行できる。
Swiftの並行性モデルは同期コードによく似たコードを書くことができる。
そして、data(from:delegate)
メソッドは他のことを何も停止させずに、ネットワークのレスポンスを待機できる。
ここまでのコードに問題がなければ、リクエストから返されたdata
値をコンソールに出力した結果は以下のようになる。
25850 bytes
レスポンスを確認する
サーバがリクエストに対して「適切に応答したかどうか」を確認するために、リクエストのレスポンスを使用してみる。
エラーなどの不都合があった場合、WEBサイトは404
を返す。
リクエストが正常に処理されている場合は、HTTPリクエストのステータスコードは200
を返す。
Task {
let (data, response) = try await URLSession.shared.data(from: url)
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
print(data)
}
}
基本的に、Data
型はほぼ何でも表現できるビットの塊。
今回のケースでは、レスポンスのボディはData
型。
データを正体を見るために、古いNSData
型を使用するようにコードを更新してコンソール出力する。
Task {
let (data, response) = try await URLSession.shared.data(from: url)
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
print(data as NSData)
}
}
実行した結果、不可解な値の、途中が省略されたbyte
プロパティがある。
{length = 25850, bytes = 0x0a0a0a09 0a0a0a09 0a0a090a 0a0a090a ... 0a0a0a0a 090a0a0a }
実際のbyte
プロパティの値は表示しきれないほど膨大。
人間には理解不能なバイト形式ではなく、読みやすい文字列にデータを変換する。
イニシャライザがデータを正しく解析できる場合に限り、String(deta:encoding:)
イニシャライザはデータを文字列に変換して返す。
とりあえず、encoding
パラメータに.utf8
を指定する。
また、イニシャイザをif-let
ステートメントに新しい行として追加する。
そして、新しい文字列を使用するようにprint()
関数を変更する。
let url = URL(string: "https://www.nintendo.co.jp")!
Task {
let (data, response) = try await URLSession.shared.data(from: url)
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let string = String(data: data, encoding: .utf8) {
print(string)
}
}
実行すると、コンソールにhttps://www.nintendo.co.jpページのHTMLが表示される。
HTMLが示しているのは、WEBサイトを「ブラウザでどのように表示すべきか」の説明にすぎないので、見ても退屈。
<!DOCTYPE html>
<html>
<head prefix="og: http://ogp.me/ns#">
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
<meta name="keywords" content="任天堂,Nintendo Switch,Wii U,ニンテンドー3DS,amiibo">
<meta name="description" content="任天堂株式会社のオフィシャルサイト「任天堂ホームページ」です。">
<title>任天堂ホームページ</title>
...
...
...
</body>
</html>