iPhoneアプリ開発 ネットワークプログラミング (1)

作成日:2021/02/10

お役立ちコラム

iPhoneアプリ開発 ネットワークプログラミング (1)

今回と次回の2回に分けて iOS ネットワークプログラミングの基礎をご紹介します。
iOS は POSIX の socket API も提供しています。
しかし Apple はそのような下層の API よりも、上層の API があればそれを使うことを推奨しています。
上層の API は名前解決、キャッシュ、タイムアウトなどをあらかじめ実装してあるため、自身で実装するよりもミスが少ないからです。

通信がHTTPベースであれば URLSession クラスを利用できます。
今回は URLSession を紹介します。

通信が独自プロトコルである場合は、Networkフレームワークを用いて TLS, TCP, UDP, WebSocket でのデータの送受信ができます。

またネットワーク上のサービスを自動検出したり名前解決したりするための Bonjour があります。
次回は Bonjour について紹介する予定です。

HTTPを用いたデータの取得

それでは簡単な例から紹介していきましょう。
画像データの URL を渡して指定の ImageView に表示させます。
簡単と言っても Swift 言語の構文としては少し複雑です。
下記のような関数の実装です。

func displayImage(imagePath: String, imageView: UIImageView)

下記のように呼び出して Wikipedia のロゴを表示します。

let imagePath = "https://www.wikimedia.org/static/images/project-logos/enwiki-2x.png"
displayImage(imagePath: imagePath, imageView: self.imageView)

displayImage() 関数の実装は下記の通りです。

上記ソースコードで (1) ~ (8) で表記されている箇所を説明します。

(1) let url = URL(string: imagePath)

URLSession オブジェクトにはリクエスト先を String ではなく URL オブジェクトで渡します。
そのために URL クラスのイニシャライザで String を URL オブジェクトに変換します。
URL オブジェクトの作成に失敗すると戻り値は nil になります。
例えば HTTP で許されない文字が入っていると失敗しますので、本来はあらかじめ不正文字がないようにエンコーディングしておく必要があります。
addingPercentEncoding メソッドは URL 不正文字を % エンコーディングします。
例えば空白文字があると %20 と置き換えます。

if let imagePath = originalString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
displayImage(imagePath: imagePath, imageView: self.imageView)
}

(2) let request = URLRequest(url: url)

URL での HTTP リクエストを管理する URLRequest オブジェクトを作ります。

(3) let session : URLSession = URLSession.shared

HTTP 通信を管理するメインのオブジェクトを作ります。
ここでは shared セッションを作成しましたが、これは最も基本的なリクエストのためのセッションです。
データ取得の途中進捗 (しんちょく) を得る、アプリの終了後もダウンロードを続ける、などの複雑な処理はできません。

設定を指定してセッションを作るには、下記のようにconfigurationを指定します。

let session : URLSession = URLSession(configuration: .default)

default設定の動作はsharedに似ていますが、さまざまな設定が可能です。

let session : URLSession = URLSession(configuration: .ephemeral)

ephemeral 設定も shared に似ていますが、キャッシュやクッキーを書き込まず、すべてメモリ上で処理され、セッション終了後にデータは破棄されます。
認証などに使います。

let session : URLSession = URLSession(configuration: . background(withIdentifier: "mysession"))

background 設定は、アプリが動いていなくてもアップロードやダウンロードを継続します。
アプリが再起動した際にセッションを識別できるように識別子を指定します。

(4) let task : URLSessionTask = session.dataTask(with: request, completionHandler: {(data, response, error) -> Void in

セッションオブジェクトの dataTask メソッドはクロージャで HTTP リクエストが完了したときのハンドラーを定義します。
ハンドラーには data, response, error が渡されます。
URLSession の dataTask は HTTP の GET リクエストのためのタスクで、サーバーからデータをメモリに取得します。
URLSession の uploadTask はファイルを Web サービスに PUT または POST でアップロードするタスクです。
URLSession の downloadTask はサーバーからファイルを一時的な場所にダウンロードするタスクです。

ではハンドラーの中のコードを見ていきましょう。

(5) if error != nil {}

ハンドラーに渡された error オブジェクトが nil でなければ何らかのエラー(例えばタイムアウトで HTTP サーバーに接続できない)が発生しました。
error オブジェクトの localizedDescription メンバー変数でエラー内容が分かります。

(6) if response.statusCode == 200 {}

response オブジェクトの statusCode が 200 なら、HTTP の GET リクエストは正常終了してデータを取得できています。
200 以外の場合は statusCode をエラーメッセージに表示するようにします。
例えば 403 なら認証が拒否された(Forbidden)ため、404 ならファイルが見つからなかった(Not Found)ためにデータを取得できませんでした。

(7) DispatchQueue.main.async {
if let image = UIImage(data: data!) {
imageView.image = image
}
}

ここはとても重要なポイントです。
このハンドラー(completionHandler)のコードはメインスレッドではなく、非同期のバックグラウンド・スレッドで動作します。
UIImageView に取得した画像を表示したいのですが、メインスレッド以外で UI API をコールすると次のようなエラーでアプリがクラッシュします。

Main Thread Checker: UI API called on a background thread:

そのため、必ず DispatchQueue.main.async でメインスレッドの処理キューにディスパッチしてから imageView.image = image を実行する必要があります。

このコードはひとつの画像データの取得ですが、100個、200個といった大量のデータを一括で取得する場合、UIProgressView で進捗(しんちょく)を表示したいと思いますが、その場合もsetProgress()メソッドを DispatchQueue.main.async の中に入れる必要があります。

(8) task.resume()

URLSessionTaskのresume()メソッドで、実際のHTTPセッションが開始されます。
このメソッドはHTTPセッションを開始したらすぐに終了しますので、画像データを取得する前にdisplayImage()関数も完了します。
そしてメインスレッドは

displayImage(imagePath: imagePath, imageView: self.imageView)

の次の行の処理に移ります。
その後 HTTP セッションが完了した時点で completionHandler の処理が開始されます。
そのため(5)〜(7)の処理が実行されるのは、(8)よりも後になるので注意してください。
displayImage() 関数の戻り値として取得した画像を返すことはできません。
あくまで completionHandler の中で画像取得後の処理をする必要があります。
複雑ですが、これはメインスレッドをブロックしないための仕組みです。

上記ステップの(7)、(8) は初心者が間違いやすいポイントですのでご注意ください。
なおエラーログの出力には os_log() メソッドを使いましたが、それには OSLog フレームワークをインポートする必要があります。

import os.log

App Transport Security (ATS)

今回の例は https: で画像データを取得しましたが、iOS 9 以降では App Transport Security (ATS) というプライバシー機能によりhttpによる平文でのリソースの送受信はブロックされます。
しかし社内イントラネットで http を多用している場合は、ATS を無効にすることもできます。

下記のように Xcode で info.plist を表示し、+ボタンをクリックすると新しいキーを追加できます。
ここで App Transport Security Settings を追加してください。

その後、Allow Arbitrary Loads をデフォルトの NO から YES にするとATSが無効になり、http での通信が許可されます。

セキュリティーリスクがありますので、アプリの接続先サーバーが固定の場合以外は、この設定は避けてください。

URL クエリパラメータ

URL クエリーパラメータを送信したい場合は、URL オブジェクトではなく URLComponents オブジェクトを使います。

if var urlComponents = URLComponents(string: "https://webserver.local/search") {
urlComponents.query = "type=account&firstname=Joseph&lastname=Biden"
guard let request = urlComponents.url else {
os_log("Error: URLComponents.url is nil”)
return
}
// この後は同じです。HTTPセッションはdataTaskを使ってください。
let session : URLSession = URLSession.shared
let task : URLSessionTask = session.dataTask(with: request, completionHandler: {(data, response, error) -> Void in
//  HTTPリクエストが完了したときのハンドラー
})
task.resume ()
}

前の例では data には画像が入っていましたので UIImage オブジェクトに変換しましたが、クエリーの戻り値に応じて String などに変換してください。
戻り値が XML や JSON などの構造を持つ場合は data を解析する必要があります。
自社のコードで解析をしても良いですが、XML の構文解析には XMLParser が用意されています。

JSON 解析には JSONDecoder が用意されています。

まとめ

以上、HTTPセッションの結果をメモリ上の data に受け取る dataTask を中心に紹介しました。
メモリではなく iPhone のローカルストレージに保存するには URLSession の downloadTask を使用します。
URLSession だけでも downloadTask やデリゲート処理など、さらに深掘りしたい実践的な課題はつきませんが、今回は初心者向けのコラムとして HTTP セッションの基礎情報をお届けしました。
次回は Bonjour をご紹介します。
どうぞお楽しみに。

ご意見・ご感想をお聞かせください

Xcodeでの開発用にはMacレンタルをご利用ください。

追加で開発用機材が必要な時にご利用ください。

お気軽にお問い合わせください

ページの先頭に戻る