【Swift】ネットワークプログラミングの基本

# この記事は何？

Swiftコードを使って、アプリで使用するためのWebデータを取得する方法について、基本を学ぶ。
そのために、プレイグラウンド環境で`URLSession`を使用するコードを実行する。

何を学ぶ？

- HTTPおよびHTTPSプロトコルとは？
- Web上でデータを送信する最も一般的な方法
- リクエストしたデータをWebサーバに伝えるためにURLを作成する方法
- `Foundation`フレームワークのAPIを使用してデータを送受信するためのタスクを作成する方法

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

# ネットワークの仕組み

Webサイトを開くと、ブラウザはインターネットを介して目的のWebサーバまでネットワークリクエストを送信する。

> Webサーバとは
リクエストが来るまで待機して、それに応答するようにプログラムされたコンピュータ。

## URL

URLは「*uniform resource locator*」の略称。

「どのサーバがリクエストを受信するのか」や「どんなレスポンスを取得するか」を指定するのが**URL**。
例えば、ブラウザのアドレスバーに`nintendo.co.jp`と入力すると、任天堂のホームページが表示される。

![nintendocojp.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/57bd4ae5-979b-e02f-dec3-73384b8c6383.jpeg)

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`は、検索ページで「ピクミン」を指定した結果を「ソフト」で絞り込んだ情報を表示する。

![スクリーンショット 2023-07-18 21.45.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/ee70825a-8f25-94c3-07aa-448a3fa8de98.png)

**リクエストの種類**
**リクエスト**の種類は一般的に、**GET**と**POST**がある。 
HTTPメソッドとも呼ばれる。
GETはサーバに「データを要求する」ために使用する。
POSTはサーバに「データのボディを送信する」ために使用する。
WEBサイトを読み込むとき、ブラウザはサーバにGETリクエストを送信する。
WEBサイトでフォームを送信すると、ブラウザはサーバにPOSTリクエストを送信する。

# URLを作成する

プレイグラウンドで、リクエストを作成して送信するためのSwiftコードを実行する。
そのためには、最初にURLのインスタンスを構築する。

Swiftの`URL`型を使用して、ネットワーク・リクエストで使用するURLを構築および変更する。
`URL`型オブジェクトは`init?(String: String)`イニシャライザを使って、簡単に作成できる。

```swift:https://www.nintendo.co.jpを示すURLオブジェクトを作成する
import Foundation

let url = URL(string: "https://www.nintendo.co.jp")!    // 返り値はオプショナル
```

`URL`型インスタンスのさまざまなプロパティを確認する。

```swift: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`なので、呼び出し時に除外できる。

```swift: リクエストを作成する
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:)`関数への呼び出しを入れる。

```swift
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」と警告している。

```swift
Task {
    let (data, response) = try await URLSession.shared.data(from: url)
    print(data)
}
```

このプレイグラウンドを実行すると、タスクが作成される。
そして、タスクがサスペンドされている間に`data(from:delegate:)`メソッドが実行される。
データ機能が完了すると、タスクは実行を続行。
定数`data`と`response`に値が割り当てられ、`print()`関数が実行される。

大抵の場合、これだけでネットワーク・リクエストを実行できる。
Swiftの並行性モデルは同期コードによく似たコードを書くことができる。
そして、`data(from:delegate)`メソッドは他のことを何も停止させずに、ネットワークのレスポンスを待機できる。

ここまでのコードに問題がなければ、リクエストから返された`data`値をコンソールに出力した結果は以下のようになる。

```console:コンソール
25850 bytes
```

# レスポンスを確認する

サーバがリクエストに対して「適切に応答したかどうか」を確認するために、リクエストのレスポンスを使用してみる。
エラーなどの不都合があった場合、WEBサイトは`404`を返す。
リクエストが正常に処理されている場合は、HTTPリクエストのステータスコードは`200`を返す。

```swift:レスポンスを確認する
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`型を使用するようにコードを更新してコンソール出力する。

```swift
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`プロパティがある。

```console:コンソール
{length = 25850, bytes = 0x0a0a0a09 0a0a0a09 0a0a090a 0a0a090a ... 0a0a0a0a 090a0a0a }
```

実際の`byte`プロパティの値は表示しきれないほど膨大。
人間には理解不能なバイト形式ではなく、読みやすい文字列にデータを変換する。
イニシャライザがデータを正しく解析できる場合に限り、`String(deta:encoding:)`イニシャライザはデータを文字列に変換して返す。

とりあえず、`encoding`パラメータに`.utf8`を指定する。
また、イニシャイザを`if-let`ステートメントに新しい行として追加する。
そして、新しい文字列を使用するように`print()`関数を変更する。

```swift:レスポンスのデータを文字列に変換する
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サイトを「ブラウザでどのように表示すべきか」の説明にすぎないので、見ても退屈。


```console
<!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>
```