LoginSignup
0
2

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

Last updated at Posted at 2023-07-18

この記事は何?

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

何を学ぶ?

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

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

ネットワークの仕組み

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

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

URL

URLは「uniform resource locator」の略称。

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

nintendocojp.jpg

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

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

URLを作成する

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

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

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

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

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

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のイニシャライザは実行コードのクロージャを受け取る。
プレイグラウンドのコードを変更して、Taskdata(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:)メソッドが実行される。
データ機能が完了すると、タスクは実行を続行。
定数dataresponseに値が割り当てられ、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>
0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2