LoginSignup
349
276

More than 1 year has passed since last update.

【Swift】URLSessionまとめ

Last updated at Posted at 2018-07-04

経緯

日頃から使っているURLSessionですが、
実際に使っている機能は結構限られており、
いまいち使いこなせていない気がしていました。

そう思ってWWDCの動画を改めて見たり、
あれやこれやとURLSessionについて調べてみたのでまとめたいと思います。

基本的にはAppleのドキュメントなどを参考にしています。
URLSession Programming Guide
URLセッションプログラミングガイド
Advances in Networking Part 2
Optimizing Your App for Today’s Internet
Use async/await with URLSession

URLローディングシステムとは?

HTTPSや自作のプロトコルが使用されているURLから特定されるリソースを非同期で提供するシステム。
これを実現するためにURLSessionとそれに関連するクラスを使用します。

URLSessionとは?

関連するネットワーク上のデータ転送処理群(=URLSessionTask)をまとめるクラス
1つのSessionで繰り返しURLSessionTaskの作成が可能。

URLSessionTaskとは?

URLで特定されるリソースを実際に取得し、アプリにデータを返却したり、
リモートサーバからファイルのダウンロードやアップロードを行います。

URLSessionConfigurationとは?

URLSessionの設定を提供するクラス。
タイムアウト値、キャッシュやクッキーの扱い方、
端末回線での接続の許可などが変更できます。

補足情報 NSCopyingの挙動

  • URLSession, URLSessionTask
    このオブジェクトはコピーすると同じオブジェクトを取得します。

  • URLSessionConfiguration
    このオブジェクトはコピーする新しいコピーを取得します。


URLSessionの種類

shared

シングルトンのURLSessionインスタンス。URLSessionConfigurationは設定できません。
基本的なリクエストはこれを使えばデータの取得が可能です。

shared以外

下記の3つのURLConfigurationと共にURLSessionの初期化を行います。

3つのURLSessionConfiguration

.default(デフォルトセッション)

sharedと同じような設定ですが、設定のカスタマイズが可能です。
ディスクの保存されるキャッシュ、認証情報、クッキーを使用します。

.ephemeral(一時セッション)

こちらもsharedと同じような設定ですが、
キャッシュ、クッキー、認証情報を端末内のディスクに書き込まず、メモリーに保持します。
セッションが破棄されるとキャッシュやクッキーも破棄されます。
(いわゆるプライベートモードです。)

.background(バックグラウンドセッション)

アップロード、ダウンロードをバックグラウンド(アプリが起動していない状態)で行います。

Appleドキュメント

URLSessionConfigurationで変更できる設定

一部だけ紹介します。

networkServiceType

OSにどのようにデータを扱うかのヒントを与えます。
これを与えることでOSが決める処理の優先度を変えることができます。

例えばprefetchなど後ほど必要な情報の場合、処理の優先度が低くなり、
取得前に処理のキャンセルを行うなどバッテリーやリソースの消費を抑える
といったことも可能になります。

allowsCellularAccess

端末回線時に通信を行うかどうかを決めます。

例えば、動画のダウンロードを行っている場合、端末回線を使用すると
かなりの通信料を消費してしまいます。
そのような場合はfalseに設定することでwifi接続時のみ実行するなどの
調整が行えます。

デフォルトはtrueです。

waitsForConnectivity(iOS11以降)

URLSessionの設定を満たした接続状態になっていない場合に、
接続可能状態になるまで待つか、即座にエラーにするかを決めます。

例えば、allowsCellularAccessがfalseで端末回線の接続しかできない場合、
URLSessionは処理を行うことができません。
この場合、
URLSessionTaskDelegateのurlSession(_:taskIsWaitingForConnectivity:)一度だけ呼ばれ、
接続可能になるまで待ちます。

func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {
    // 例えばアラートを出すなど
    DispatchQueue.main.async {
        let alert = UIAlertController(title: "接続エラー", message: "wifiの接続状況を確認してください", preferredStyle: .alert)
        let action = UIAlertAction(title: "OK", style: .default, handler: nil)
        alert.addAction(action)
        self.present(alert, animated: true, completion: nil)
    }
}

今まではReachabilityなどで接続確認、リトライなど必要でしたが、このプロパティを用いることで、マニュアルでの処理が不要になります。

Apple的にも常にtrueにすることを推奨しています。
※例外として、即座に完了されなければいけないまたは中止しなければいけない株の取引などには不適切だそうです。

バックグラウンドセッションの場合は自動でtrueになります。

ちなみに機内モードにすると動作が簡単に確認できます。

注意点

一度接続が確立されたあとの接続エラーに関しては、NSURLErrorConnectionLostなどのエラーが起きます。

この場合の対応として、下記のAppleの回答がありますので参考までにURLを記載します。
(内容が難しいのですが、GETなどリソースの修正がない場合は自動でリトライ、
POSTなどの場合は状況に応じて個々に対応していくべきというようなことかと思います。)

Apple Q&A

isDiscretionary

バックグラウンド時に有効になるプロパティで、
trueにすることで実行タイミングをシステムに任せ、
最適なタイミングで(リソースの使用状況やバッテリーの状況を測って)タスクを実行してくれるようになります。

allowsConstrainedNetworkAccess

バックグラウンド時に有効になるプロパティで、
trueにすることで実行タイミングをシステムに任せ、
最適なタイミングで(リソースの使用状況やバッテリーの状況を測って)タスクを実行してくれるようになります。

allowsConstrainedNetworkAccess(iOS13以降)

ユーザが省データモードをONにしている場合に
ネットワーク通信を行うかどうかを設定します。

これをtrueにすることで
ユーザの操作から発生したタスクなど
素早いレスポンスが必要な場合に
Cellularなどの遅い回線で通信することを抑え
Wifiなどの早い回線が利用可能になるまで
データの送受信を待機させることができます。

下記の条件が満たされた場合
この設定が適用されたURLSessionから生成されたTaskは全て
NSURLErrorNetworkUnavailableReason.constrained
を返します。

  • 使用可能な通信インターフェイスがCellularのみ
  • allowsConstrainedNetworkAccessがfalse
  • ユーザが省データモードをONにしている

allowsExpensiveNetworkAccess(iOS13以降)

フレームワークがexpensiveであると判断したURLSessionでの通信を
許可するかどうかを設定します。

これをfalseにすることで
ユーザの操作から発生したタスクなど
素早いレスポンスが必要な場合に
CellularやPersonal hotspotなどのexpensiveな回線で通信することを抑え
Wifiなどのnonexpensiveな回線が利用可能になるまで
データの送受信を待機させることができます。

下記の条件が満たされた場合
この設定が適用されたURLSessionから生成されたTaskは全て
NSURLErrorNetworkUnavailableReason.expensive
を返します。

  • 使用可能な通信インターフェイスが全てexpensive
  • allowsExpensiveNetworkAccessがfalse

他のプロパティに関してはAppleドキュメントをご参照ください。

URLSessionTaskの種類

URLSessionDataTask

Dataオブジェクトを経由してデータの送受信を行います。
主に小さいデータやサーバと対話的にデータのやり取りを実行する場合に使います。

URLSessionUploadTask

DataTaskに似ていてデータのアップロードを行いますが、
ファイルやストリーム、バックグラウンドでのアップロードも行うことができます。

URLSessionDownloadTask

ファイルからデータを取得します。また、バックグラウンドでのダウンロードとアップロードができます。

URLSessionStreamTask

TCP/IP接続をしてデータを取得します。URLSessionDataTaskを継承しています。

コメントでご指摘いただきましたので訂正します。

URLSessionを介して作成されたTCP/IPコネクションのインターフェイスを提供します。

The URLSessionStreamTask class provides an interface a TCP/IP connection created via URLSession.

URLかURLRequestか、completionHandlerのがあるかないか

各Taskのメソッドを見てみると主に引数がURLかURLRequestか、
completionHandlerがあるかないかでメソッドが分かれています。

func dataTask(with: URL) -> URLSessionDataTask
func dataTask(with: URL, completionHandler: (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
func dataTask(with: URLRequest) -> URLSessionDataTask
func dataTask(with: URLRequest, completionHandler: (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask

func downloadTask(with: URL) -> URLSessionDownloadTask
func downloadTask(with: URL, completionHandler: (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask
func downloadTask(with: URLRequest) -> URLSessionDownloadTask
func downloadTask(with: URLRequest, completionHandler: (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask
func downloadTask(withResumeData: Data) -> URLSessionDownloadTask
func downloadTask(withResumeData: Data, completionHandler: (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask

func uploadTask(with: URLRequest, from: Data) -> URLSessionUploadTask
func uploadTask(with: URLRequest, from: Data?, completionHandler: (Data?, URLResponse?, Error?) -> Void) -> URLSessionUploadTask
func uploadTask(with: URLRequest, fromFile: URL) -> URLSessionUploadTask
func uploadTask(with: URLRequest, fromFile: URL, completionHandler: (Data?, URLResponse?, Error?) -> Void) -> URLSessionUploadTask
func uploadTask(withStreamedRequest: URLRequest) -> URLSessionUploadTask

URLかURLRequestか

これはURLConfigurationの設定を変更するかしないかの違いによります。
URLRequestはURLConfigurationと同じようなプロパティを持っており、
その値を変更することでURLConfigurationの設定を上書きできます。

completionHandlerがあるかないかの違い

completionHandlerを使用するとサーバと対話的に処理を行います。
サーバに接続し、返って来たレスポンスをその場で処理する場合に使用します。

一方で、completionHandlerがないパターンはデリゲートメソッドを使用します。
セッションを作成する際にデリゲートを設定することで、
様々な処理をデリゲートメソッドで行うことが可能です。

バックグラウンドの場合はデリゲートで処理を行います。

completionHandlerのとり得る値について(2018/10/23 追記)

Appleのドキュメントによると

If the request completes successfully, 
the data parameter of the completion handler block contains the resource data, 
and the error parameter is nil. 
If the request fails, the data parameter is nil 
and the error parameter contains information about the failure. 
If a response from the server is received, 
regardless of whether the request completes successfully or fails, 
the response parameter contains that information.

と書かれており、
completionHandlerは、

  • URLSessionResponseがあるかないか
  • DataまたはErrorのどちらか一方

の組み合わせで決まるようですので、
パターンとしては、

  1. URLResponse と Data
  2. URLResponse と Error
  3. Dataのみ
  4. Errorのみ

の4通りが考えられます。

async/awaitを使ったAPI(2021/11/24 追記)

Swift5.5で追加されたasync/await記法に対応したAPIが追加されました。
追加された経緯に関しては下記の2つがあります。

  1. Objective-Cのcompletion handler形式のAPIを自動でasyncの形式に変換するSwift Concurrencyの機能による追加

Calling Objective-C APIs Asynchronously

変換されたAPIは、Appleのドキュメントに下記のように記載されています。

Concurrency Note

また、どういったAPIが変換されたかは下記が参考になるかと思います。
(Objective-Cから自動変換されたAPI)[https://github.com/DougGregor/swift-concurrency-objc/blob/xcode-12-2-beta-3-concurrency/iOS/Foundation/NSURLSession.swift]

これらはConcurrencyモジュールのバックデプロイ機能によってiOS13以降で使用可能です。

  1. iOS15で新規に追加されたもの

一方で、新しいプロパティや型を使用している場合など
これまでのcompletion handler形式と互換性のないものはiOS15以上のみで使用可能です。

主に下記のようなものがあります。

func bytes(for: URLRequest, delegate: URLSessionTaskDelegate?) -> (URLSession.AsyncBytes, URLResponse)
func bytes(from: URL, delegate: URLSessionTaskDelegate?) -> (URLSession.AsyncBytes, URLResponse)

func data(for: URLRequest, delegate: URLSessionTaskDelegate?) -> (Data, URLResponse)
func data(from: URL, delegate: URLSessionTaskDelegate?) -> (Data, URLResponse)

func download(for: URLRequest, delegate: URLSessionTaskDelegate?) -> (URL, URLResponse)
func download(from: URL, delegate: URLSessionTaskDelegate?) -> (URL, URLResponse)
func download(resumeFrom: Data, delegate: URLSessionTaskDelegate?) -> (URL, URLResponse)

func upload(for: URLRequest, from: Data, delegate: URLSessionTaskDelegate?) -> (Data, URLResponse)
func upload(for: URLRequest, fromFile: URL, delegate: URLSessionTaskDelegate?) -> (Data, URLResponse)

bytes(for:delegate:)
bytes(for:delegate:)

data(for:delegate:)
data(from:delegate:)

download(for:delegate:)
download(from:delegate:)
download(resumeFrom:delegate:)

upload(for:from:delegate:)
upload(for:fromFile:delegate:)

新しい機能としては

  • メソッドの引数にURLSessionTaskDelegateを渡せることができ、個々のURLSessionTaskごとにdelegateを設定できるように(※ただし、バックグラウンドのURLSessionはサポートされていません)
  • byteメソッドでURL上のデータを1行1行非同期に取得して処理をすることが可能に

などがあります。
これらについては、WWDC2021のUse async/await with URLSessionで紹介されているので、こちらを参考にしてみてください。

Xcode13.3の追加で下記に関してはiOS13.0から利用可能(2022/2/19 追記)

Beta3で下記のAPIは削除されていました。(2021/2/24追記)
次回以降のBetaでも経過を追っていきたいと思います。

func data(for request: URLRequest) async throws -> (Data, URLResponse)
func data(from url: URL) async throws -> (Data, URLResponse)
func upload(for request: URLRequest, from bodyData: Data) async throws -> (Data, URLResponse)
func upload(for request: URLRequest, fromFile fileURL: URL) async throws -> (Data, URLResponse)

data(for:)
data(from:)

upload(for:from:)
upload(for:fromFile)

delegate: URLSessionTaskDelegate?は引数にありません。

基本的な使い方

iTunesのSearchAPIを使った例を示します。

デフォルトの設定で良い場合は、
URLSessionのシングルトンインスタンスであるsharedを使ってURLを指定してデータを取得できます。


let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles")!

let task = URLSession.shared.dataTask(with: url) { data, response, error in
    
    // ここのエラーはクライアントサイドのエラー(ホストに接続できないなど)
    if let error = error {
        print("クライアントエラー: \(error.localizedDescription) \n")
        return
    }
    
    guard let data = data, let response = response as? HTTPURLResponse else {
        print("no data or no response")
        return
    }
    
    if response.statusCode == 200 {
        print(data)
    } else {
        // レスポンスのステータスコードが200でない場合などはサーバサイドエラー
        print("サーバエラー ステータスコード: \(response.statusCode)\n")
    }

}
task.resume()

(2021/11/24追記)asyncを使った場合は下記のようになります。
dataメソッドの結果をawaitし、同期関数と同様に上から下に流れるように処理が実行されます。
エラーが起きる可能性があるためtryも付けています。

let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles")!

do {
    let (data, response) = try await URLSession.shared.data(from: url)

    guard let response = response as? HTTPURLResponse else {
        print("invalid response")
        return
    }

    if response.statusCode == 200 {
        print(data)
    } else {
        // レスポンスのステータスコードが200でない場合などはサーバサイドエラー
        print("サーバエラー ステータスコード: \(response.statusCode)\n")
    }
} catch {
    // ここのエラーはクライアントサイドのエラー(ホストに接続できないなど)
    print("クライアントサイドエラー: \(error.localizedDescription) \n")
}

設定を変更したい場合

URLConfigurationの設定を変更し、URLSessionインスタンスを作成します。


// URLSessionConfigurationの設定を変更
let config = URLSessionConfiguration.default
config.waitsForConnectivity = true

// 新しくURLSessionインスタンスを作成
let session = URLSession(configuration: config)

let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles")!

let task = session.dataTask(with: url) { data, response, error in
    
    // ここのエラーはクライアントサイドのエラー(ホストに接続できないなど)
    if let error = error {
        print("クライアントサイドエラー: \(error.localizedDescription) \n")
        return
    }
    
    guard let data = data, let response = response as? HTTPURLResponse else {
        print("no data or no response")
        return
    }
    
    if response.statusCode == 200 {
        print(data)
        
        // ...これ以降decode処理などを行い、UIのUpdateをメインスレッドで行う

    } else {
        // レスポンスのステータスコードが200でない場合などはサーバサイドエラー
        print("サーバサイドエラー ステータスコード: \(response.statusCode)\n")
    }

}
task.resume()

リクエストごとに設定を変更したい場合

すでに記載済みですがURLRequestのプロパティを設定することで、
URLConfigurationの設定を上書きすることができます。

まずURLを使った場合のURLRequestは

URLをそのまま使った結果

let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles")!

let task = URLSession.shared.dataTask(with: url)

/*
Optional(https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles)
  ▿ some: https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles
    ▿ url: Optional(https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles)
      ▿ some: https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles
        - _url: https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles #0
          - super: NSObject
    - cachePolicy: 0
    - timeoutInterval: 60.0
    - mainDocumentURL: nil
    - networkServiceType: __ObjC.NSURLRequest.NetworkServiceType
    - allowsCellularAccess: true
    ▿ httpMethod: Optional("GET")
      - some: "GET"
    - allHTTPHeaderFields: nil
    - httpBody: nil
    - httpBodyStream: nil
    - httpShouldHandleCookies: true
    - httpShouldUsePipelining: false
▿ https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles
  ▿ url: Optional(https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles)
    ▿ some: https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles
      - _url: https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles #0
        - super: NSObject
  - cachePolicy: 0
  - timeoutInterval: 60.0
  - mainDocumentURL: nil
  - networkServiceType: __ObjC.NSURLRequest.NetworkServiceType
  - allowsCellularAccess: true
  ▿ httpMethod: Optional("GET")
    - some: "GET"
  - allHTTPHeaderFields: nil
  - httpBody: nil
  - httpBodyStream: nil
  - httpShouldHandleCookies: true
  - httpShouldUsePipelining: false
*/
dump(task.currentRequest)

同様にURLRequestを使用した場合、

URLRequestを使った結果

let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles")!

var request = URLRequest(url: url)  // inspect with Show Result button

let taskWithRequest = URLSession.shared.dataTask(with: request)

/*
Optional(https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles)
  ▿ some: https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles
    ▿ url: Optional(https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles)
      ▿ some: https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles
        - _url: https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles #0
          - super: NSObject
    - cachePolicy: 0
    - timeoutInterval: 60.0
    - mainDocumentURL: nil
    - networkServiceType: __ObjC.NSURLRequest.NetworkServiceType
    - allowsCellularAccess: true
    ▿ httpMethod: Optional("GET")
      - some: "GET"
    - allHTTPHeaderFields: nil
    - httpBody: nil
    - httpBodyStream: nil
    - httpShouldHandleCookies: true
    - httpShouldUsePipelining: false
▿ https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles
  ▿ url: Optional(https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles)
    ▿ some: https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles
      - _url: https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles #0
        - super: NSObject
  - cachePolicy: 0
  - timeoutInterval: 60.0
  - mainDocumentURL: nil
  - networkServiceType: __ObjC.NSURLRequest.NetworkServiceType
  - allowsCellularAccess: true
  ▿ httpMethod: Optional("GET")
    - some: "GET"
  - allHTTPHeaderFields: nil
  - httpBody: nil
  - httpBodyStream: nil
  - httpShouldHandleCookies: true
  - httpShouldUsePipelining: false
*/
dump(taskWithRequest.currentRequest)

設定値は同じになります。

URLRequestの設定値を変えると

URLRequestの設定値を変えた結果

let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles")!

var request = URLRequest(url: url)  // inspect with Show Result button

request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
request.networkServiceType = .background
request.allowsCellularAccess = false

let taskWithRequest = URLSession.shared.dataTask(with: request)

/*
Optional(https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles)
  ▿ some: https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles
    ▿ url: Optional(https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles)
      ▿ some: https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles
        - _url: https://itunes.apple.com/search?media=music&entity=song&lang=ja_jp&term=beatles #0
          - super: NSObject
    - cachePolicy: 1
    - timeoutInterval: 60.0
    - mainDocumentURL: nil
    - networkServiceType: __ObjC.NSURLRequest.NetworkServiceType
    - allowsCellularAccess: false
    ▿ httpMethod: Optional("GET")
      - some: "GET"
    - allHTTPHeaderFields: nil
    - httpBody: nil
    - httpBodyStream: nil
    - httpShouldHandleCookies: true
    - httpShouldUsePipelining: false
*/
dump(taskWithRequest.currentRequest)

リクエストの設定が変わりました。

DataTaskを使用したPOSTやPUTの場合

httpBodyプロパティへの値の変更やhttpMethodプロパティの変更などが必要になるため
URLRequestが必須となります。


let url = URL(string: "https://example.com/messages")!

var request = URLRequest(url: url)  // inspect with Show Result button

// POSTにメソッド設定
request.httpMethod = "POST"

// ヘッダーにcontent-typeを設定(JSONを送るのでapplication/jsonにしている)
request.addValue("application/json", forHTTPHeaderField: "content-type")

// Message構造体を作成
struct Message: Codable {
    let user: String
    let content: String
}
let encoder = JSONEncoder()
let message = Message(user: "me", content: "Hello!!!")
do {
    // EncodeしてhttpBodyに設定
    let data = try encoder.encode(message)
    request.httpBody = data
} catch let encodeError as NSError {
    print("Encoder error: \(encodeError.localizedDescription)\n")
}

let task = URLSession.shared.dataTask(with: request) { data, response, error in
    
    if let error = error {
        print("クライアントエラー: \(error.localizedDescription) \n")
        return
    }

    // EncodeしてhttpBodyに設定
    guard let data = data, let response = response as? HTTPURLResponse,
        response.statusCode == 201 else {
            print("No data or statusCode not CREATED")
            return
    }
    print("success")
}
task.resume()


print(task.currentRequest.httpBody) // nil

最後にtask.currentRequest.httpBodyを出力してみると、
httpBodyプロパティはnilになります。これはデータがサーバに送られたためです。

アップロード

Uploading Data to a Website
を参考にしています。

リクエストのボディコンテンツを3種類の方法でアップロードすることが可能です。

  • Dataオブジェクトを使用する uploadTask(with:from:), uploadTask(with:from:completionHandler:),
  • ファイルを使用する uploadTask(with:fromFile:),uploadTask(with:fromFile:completionHandler:)
  • ストリームを使用する uploadTask(withStreamedRequest:)

この場合URLSessionUploadTaskを使用します。
URLSessionUploadTaskは
対話的に処理する方法とバックグラウンドの両方に対応しています。

下記ではDataオブジェクトを使用します。

アップロードするDataを用意する

アップロード可能なデータとしては、
画像データをJPEGやPNGデータ形式に変換したり、
文字列データをUTF-8でエンコードしたり、
Encodableに適合した構造体をJSONEncoderでJSON形式に変換するなど
多様なデータをアップロードすることができます。(サーバが対応していれば)

URLRequestを用意する

URLSessionUploadTaskはURLRequestを引数に取ります。
URLRequestではhttpMethodプロパティをサーバの要求に合わせてPOSTまたはPUTにし、
ヘッダーにContent-typeなどを設定します。

※URLSessionは設定されたデータからサイズを自動で計算するため、
Content-Lengthの設定は不要です。


let url = URL(string: "https://example.com/post")!
var request = URLRequest(url: url)
request.httpMethod = "POST"

// JSONの場合はapplication/json
request.setValue("application/json", forHTTPHeaderField: "Content-Type")

URLSessionUploadTaskの作成と開始

設定したURLRequestからURLSessionUploadTaskを作成し、処理を開始します。
下記の例では、completionHandlerを用いてレスポンスを処理しています。


let task = URLSession.shared.uploadTask(with: request, from: uploadData) { data, response, error in
    if let error = error {
        print ("クライアントエラー: \(error)")
        return
    }
    guard let response = response as? HTTPURLResponse,
        (200...299).contains(response.statusCode) else {
        print ("サーバエラー")
        return
    }
    if let mimeType = response.mimeType,
        mimeType == "application/json",
        let data = data,
        let dataString = String(data: data, encoding: .utf8) {
        print ("got data: \(dataString)")
    }
}
task.resume()

デリゲートを使用することもできる

completionHandlerがないメソッドを呼んだ場合、
デリゲートを実装して処理をすることができます。

例えばアップロードの進行状況を示したい場合、URLSessionDataDelegateの
urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data)
を用いることで進捗状況が把握できます。

※iOS11よりURLSessionTaskでprogressプロパティが使えるようになりました。

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {

    // fractionCompletedは0.0~1.0の間で完了状態を示す
    let progress = Float(dataTask.progress.fractionCompleted)
    
    DispatchQueue.main.async {
        progressView.setProgress(progress, animated: true)
    }
}

progress

URLSessionStreamTaskを使用する場合の注意点

  • ストリームを使用する場合、
    サーバがリクエストする可能姓の追加のヘッダ情報を追加する必要があります。
    (コンテンツタイプやコンテンツの長さなど)

  • セッションはデータを巻き戻して読み込み直すことができないため、
    リトライする場合などのために
    URLSessionTaskDelegateurlSession(_:task:needNewBodyStream:)
    を実装する必要があります。

func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) {
    self.closeStream()

    var inStream: InputStream? = nil
    var outStream: OutputStream? = nil
    Stream.getBoundStreams(withBufferSize: 4096, inputStream: &inStream, outputStream: &outStream)
    self.outputStream = outStream

    completionHandler(inStream)
}

ダウンロード(フォアグラウンド)

流れとしては他の処理と同様ですが、

URLSessionDownloadTaskには
cancel(byProducingResumeData completionHandler: @escaping (Data?) -> Void)
があるため、処理の一時停止からダウンロードを再開するための処理も考えたいと思います。

ダウンロードの状況を管理するクラスを用意する

例えば複数同時にダウンロードをする場合など、
それぞれのダウンロード状況を管理する必要があります。
(どのくらいダウンロードできているのか、そもそも今ダウンロード中なのかなど)

例えば下記のようなクラスを用意します。


final class Download: NSObject {
  
  var url: URL

  // 現在ダウンロード中か
  var isDownloading = false 
  
  // 進行状況
  var progress: Float = 0
  
  var task: URLSessionDownloadTask?
  
  // ダウンロードが中断された時のダウンロード済のデータ
  var resumeData: Data?
  
  init(url: URL) {
    self.url = url
  }
}

URLSessionDownloadTaskを用意して各処理を実装する。

他の処理と同様にTaskを作成し、処理を開始します。

ダウンロードは自動でダウンロードが開始されるというよりは、
ボタンを押してダウンロードを開始するということが多いと思いますので、
ボタンが押された時に処理を開始する例を記載しました。

また、ダウンロードのキャンセルや一時停止、再開の処理も合わせて記載しています。

※サンプルで作成したアプリの処理の一部を抜粋しています。


let url = URL(string: "https://audio-ssl.itunes.apple.com/apple-assets-us-std-000001/Music4/v4/a0/05/df/a005df47-d4d5-1fd1-eefc-77553fa59689/mzaf_5617395189778548804.plus.aac.p.m4a")!

// ダウンロード処理中のDownloadクラスを保持
var activeDownloads: [URL: Download] = [:]

// ダウンロード開始処理
func startDownload() {

  let download = Download(url: url)
  download.task = URLSession.shared.downloadTask(with: url) { location, response, error in
    
    // ダウンロードデータの一時保存URL
    print(location)

    // ...ローカルファイルへ保存を行う
  }
  download.task!.resume()
  download.isDownloading = true
  activeDownloads[download.url] = download
}

// 一時停止処理
func pauseDownload() {
    guard let download = activeDownloads[url] else { return }
    if download.isDownloading {
        download.task?.cancel(byProducingResumeData: { data in
        // ここで途中までダウンロードしていたデータを保持しておく
        download.resumeData = data
        })
        download.isDownloading = false
    }
}

// キャンセル処理
func cancelDownload() {
    if let download = activeDownloads[url] {
        download.task?.cancel()
        activeDownloads[url] = nil
    }
}

// ダウンロード再開処理
func resumeDownload() {
    guard let download = activeDownloads[url] else { return }

    // pause時に保存していたresumeDataがある場合はそこから続きのダウンロードを行う
    if let resumeData = download.resumeData {
        download.task = URLSession.shared.downloadTask(withResumeData: resumeData)
        download.task!.resume()
        download.isDownloading = true
    } else {
        download.task = URLSession.shared.downloadTask(with: download.url)
        download.task!.resume()
        download.isDownloading = true
    }
}

バックグラウンド

バックグラウンドでデータの送受信を行う場合、デリゲートメソッドを実装します。
あるデリゲートメソッドは呼ばれる順番が決まっており、
それぞれのタイミングで必要な処理というものもあります。

Downloading Files in the Background
を参考にしています。

バックグラウンド用のURLSessionを作成する

  1. .backgroundのURLSessionConfigurationインスタンスを作成する。この際にidentifierを設定します。
    ※システムはこのidentifierを用いてTaskとURLSessionを紐づけているため、
    1つのURLConfigurationに対して1つのURLSessionしか作成できないということになります。

  2. バックグランドタスク完了後にシステムがアプリを起動するためには、
    URLSessionConfigurationのsessionSendsLaunchEventsプロパティがtrueにする必要があります(デフォルトはtrue)

  3. 急を要さないタスクに関しては、URLSessionConfigurationのisDiscretionaryプロパティをtrueにします。
    こうすることでシステムが都合の良いタイミングでタスクを実行するようになります。(都合が良いというのはネットワークの接続状況などを考慮してくれるということです)

  4. URLSessionインスタンス作成時に上記で設定したURLConfigurationを渡します。
    また、バックグラウンド時のイベントを受け取るためにデリゲートを設定しなければなりません。

private lazy var urlSession: URLSession = {
    let config = URLSessionConfiguration.background(withIdentifier: "BackgroundSession")
    config.isDiscretionary = true
    config.sessionSendsLaunchEvents = true
    return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()

delegateQueueに関して

delegateQueueにnilを設定すると、アプリはシリアルなOperation Queueを作成します。
このQueueの中で全てのデリゲートメソッドとcompletionHandlerは実行されます。
独自で設定する場合にもシリアルなOperation Queueを作成して渡します。
シリアルでないと、処理の実行順序が変わってしまい結果が異なってきてしまう可能性があります。

ちょっと複雑なのですが、
このQueueが制御するのはデリゲートメソッドとcompletionHandler
URLSessionのネットワーク通信の同時接続を制御するものではありません。

URLSessionの同時接続はhttpMaximumConnectionsPerHostプロパティの値を変更します。iOSのデフォルトは4です。
※注意点としてこの値は1セッションごとの値であり、仮に複数セッションを使っていた場合は、この上限を超えます。

また、アプリが提供するOperation Queueは並行処理の上限を設定することができません。
例えば、10Mの画像を50個ダウンロード使用とする場合、同時並行処理の上限がないと、
タスクの完了に時間がかかりタイムアウトになる可能性があります。
最適な数としては4か5と下記のURLでは記載がありますが、
それぞれの使用状況に合わせて検討する必要があるのかと思います。

さらに、ある処理を行った後にある処理を行いたい場合などは
Operationのdependencyを活用することで処理順序の制御ができるようになります。

参考にした情報:
https://stackoverflow.com/questions/21918722/how-do-i-use-nsoperationqueue-with-nsurlsession

DonwloadTaskを作成する

  1. completionHandlerのないdownloadTask(with:)でURLDownloadTaskを作成します。

  2. (iOS11以降)earliestBeginDateプロパティを設定するとダウンロードをスケジューリングできます。
    設定された日時以降に処理が開始されるようになります。
    ただし、本当の開始日時はシステムが判断するので正確に設定した時間に開始される訳ではありません。
    さらにURLSessiontaskDelegateのurlSession(_:task:willBeginDelayedRequest:completionHandler:)
    本当に処理が開始される時点で挙動の変更を行うことも可能です。
    これはその時点ではすでに情報が古くなっている可能性がある場合など
    最新の情報に入れ換えが可能になります。

func urlSession(_ session: URLSession, task: URLSessionTask, willBeginDelayedRequest request: URLRequest, completionHandler: @escaping (URLSession.DelayedRequestDisposition, URLRequest?) -> Void) {
    
    var updatedRequest = request 

    // 何かヘッダーの値を変更する
    updatedRequest.addValue("...", forHTTPHeaderField: "...")
    completionHandler(.useNewRequest, updatedRequest)

    // .continueLoadingはそのまま継続
    // .cancelならば中止
}

ちなみに、バックグラウンドセッション以外に設定しても影響は何もありません。

earliestBeginDate

  1. (iOS11以降)システムに効率良くスケジューリングしてもらうために、
    countOfBytesClientExpectsToSend

    countOfBytesClientExpectsToReceive
    を設定します。
    送受信の上限のバイト数の推定値を設定します。
    設定がない場合はNSURLSessionTransferSizeUnknownが設定されます。
    (要はシステムが勝手に決めるよ、と)
    ※推定値にはボディデータに加えてヘッダーのバイト数も計算に入れる必要があります。

  2. resume()でタスクを開始します。

let backgroundTask = urlSession.downloadTask(with: url)
// 1時間後にダウンロードを開始する予定
backgroundTask.earliestBeginDate = Date().addingTimeInterval(60 * 60)
// 最大200バイトのデータを送る予定
backgroundTask.countOfBytesClientExpectsToSend = 200
// 最大500キロバイトのデータを受け取る予定
backgroundTask.countOfBytesClientExpectsToReceive = 500 * 1024
// タスク開始
backgroundTask.resume()

アプリが再起動した際の処理を定義する

  • アプリがバックグランド状態の場合、システムはアプリを停止状態にします。
    (バックグラウンドタスクは別プロセスで実行しています)

  • ダウンロードが完了した際に、システムはアプリを再起動させ、
    UIApplicationのapplication(_:handleEventsForBackgroundURLSession:completionHandler:)を呼び出します。

この際にやるべきことが2つあります。

  1. 引数の最後のcompletionHandlerはappDelegateなどの変数として保存する必要があります。
    これはcompletionHandlerを呼ぶことでシステムにバックグラウンド処理が完了したことを知らせ、
    システムはアプリから最新のスナップショットを取得し、アプリスウィッチャーの画像を更新します。
    ※注意点

この時点でcompletionHandlerを呼んでしまうとその後のデリゲートメソッドは呼ばれなくなります。
また、新しくURLSessionを作成する前に保存する必要があります(とドキュメントに書いてあります)。
これはURLSessionDelegateのurlSessionDidFinishEvents(forBackgroundURLSession:)
の中で使用します。

  1. 引数に設定されているidentifierからバックグラウンドセッションを再作成する必要があります。
    ただし、他の場所、例えば起動後最初に表示するViewControllerなどでバックグラウンドセッションの再作成が完了している場合は、
    すでにタスクとセッションの紐付けは完了しているため不要です。

注意点

ユーザが意図的にアプリを終了(アプリスウィッチャーでタスクキル)させた場合、バックグランド処理は全てキャンセルされ、アプリの再起動も行われません。


func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
    backgroundSessionCompletionHandler = completionHandler

    // ここは不要な場合が多い
    let configuration = URLSessionConfiguration.background(withIdentifier: identifier)
    let downloadssession = URLSession(configuration: configuration, delegate: SearchViewController(), delegateQueue: nil)
    let downloadService = DownloadService()
    downloadService.downloadsSession = downloadssession
}

バックグランド処理完了後の処理を定義する

  • 全てのイベントが完了したあと、
    URLSessionDelegateのurlSessionDidFinishEvents(forBackgroundURLSession:)が呼ばれます。
    この際上記で保存したcompletionHandlerを最後の呼ぶことでシステムにバックグランドタスクが完了したことを伝えます。
    Appleのドキュメントによると、これを呼ぶことでシステムがアプリを再度一時停止しても良いということを知らせる役割があるようです。

注意点

保存しているcompletionHandlerはUIKitに含まれているため、メインスレッドで呼び出す必要があります。

func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    DispatchQueue.main.async {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate,
            let backgroundCompletionHandler =
            appDelegate.backgroundCompletionHandler else {
                return
        }
        backgroundCompletionHandler()
    }
}

ダウンロード完了後の完了処理、エラー処理を定義する

アプリ再開後(またはフォアグラウンドに移行後)、
URLSessionDownloadDelegateのurlSession(_:downloadTask:didFinishDownloadingTo:)が呼ばれます。

引数のdownloadTaskのresponseを見ることで、
サーバからエラーが返ってきていないかなどの判定をすることができます。
(※クライアントサイドのエラーはここでは取得できません)

成功していた場合、引数locationにデータをダウンロードした一時保存ファイルのURLが格納されているため、そのデータをコピーして別ファイルに保存するようにします。

注意点

locationのURLが有効なのはデリゲートメソッド内のみです。

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
                didFinishDownloadingTo location: URL) {
    guard let httpResponse = downloadTask.response as? HTTPURLResponse,
        (200...299).contains(httpResponse.statusCode) else {
            print ("server error")
            return
    }
    do {
        let documentsURL = try
            FileManager.default.url(for: .documentDirectory,
                                    in: .userDomainMask,
                                    appropriateFor: nil,
                                    create: false)
        let savedURL = documentsURL.appendingPathComponent(
            location.lastPathComponent)
        try FileManager.default.moveItem(at: location, to: savedURL)
    } catch {
        print ("file error: \(error)")
    }
}

クライアントサイドのエラーを取得するには?

URLSessionTaskDelegateのurlSession(_:task:didCompleteWithError:)で処理をします。

このメソッドは種類に関わらずタスクが完了した際に呼ばれ、
もし最後の引数のerrorがnon-nilであった場合にエラーを処理します。

バックグランド処理で気をつけなければならないこと

バックグランドでの処理はアプリのプロセスとは別で行われます。
そのため、システムのリソースの状態などにより以下のような制限を考える必要があります。

  • URLSessionは全てのデータ転送に対して必ずデリゲートを設定する必要があります。

  • HTTPまたはHTTPSのみをサポートしています。

  • サーバからリダイレクト要求が来た場合はそのままリダイレクトされます。
    urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)を実装しても呼ばれません。

  • アップロードを行う際はファイルからのデータ転送しかサポートしていません。
    (Dataやstreamはアプリが終了した時点で失敗します。)

サンプル

下記のURLを参考にダウンロードやの動作確認用にサンプルを作成してみました。
https://github.com/stzn/URLSessionSample

参考URL:https://www.raywenderlich.com/158106/urlsession-tutorial-getting-started

URLSessionTaskMetrics

iOS 10からURLSessionTaskMetricsクラスが追加され、
通信に関する様々な情報を取得することができるようになりました。

URLSessionTaskMetrics

3つのプロパティを持つ

// URLSessionTaskTransactionMetricsクラスの配列
var transactionMetrics: [URLSessionTaskTransactionMetrics]

// タスクがインスタンス化されてから完了するまでの時間
var taskInterval: NSDateInterval

// タスク実行中にリダイレクトされた回数
var redirectCount: Int

URLSessionTaskTransactionMetrics

3つに分かれています。

  1. リクエストとレスポンス
var request: URLRequest
var response: URLResponse

2.それぞれの処理の開始終了日時

URLSessionの通信は下記の図のような流れで行われ、
それぞれのタイミングの開始終了日時を取得できます。

23287374-c86a-4b5c-80d4-c1d596f20f85.png

// タスクがリソースの取得を開始した時間
var fetchStartDate: Date?

// タスクがリソースの名前解決をする直前の時間
var domainLookupStartDate: Date?

// タスクがリソースの名前解決を完了した後の時間
var domainLookupEndDate: Date?

// タスクがサーバとのTCPコネクションを確立する直前の時間
var connectStartDate: Date?

// タスクがサーバとのTLSハンドシェイクを開始する直前の時間
var secureConnectionStartDate: Date?

// タスクがサーバとのTLSハンドシェイクを完了した後の時間
var secureConnectionEndDate: Date?

// タスクがサーバとの接続を完了した後の時間
var connectEndDate: Date?

// タスクがサーバへのリソースのリクエストを開始する直前の時間
var requestStartDate: Date?

// タスクがサーバへのリソースのリクエストを完了した後の時間
var requestEndDate: Date?

// タスクがサーバから最初のレスポンスのバイトを取得した後の時間
var responseStartDate: Date?

// タスクがサーバから最後のレスポンスのバイトを取得した後の時間
var responseEndDate: Date?

3.プロトコルとコネクション

// 使用されたプロトコル名
var networkProtocolName: String?

// プロキシが使用されたか
var isProxyConnection: Bool

// 永続コネクションが使用されたか
var isReusedConnection: Bool

// リソースの種類
var resourceFetchType: URLSessionTaskMetrics.ResourceFetchType

enum ResourceFetchType {
    case unknown // 不明
    case networkLoad // ネットワークから取得
    case serverPush // サーバプッシュから取得
    case localCache // ローカルキャッシュから取得
}

実際に取得する

データの取得方法は簡単で
URLSesssionTaskDelegateのurlSession(URLSession:task:didFinishCollecting)を実装するだけです。

取得例
func urlSession(_ session: URLSession, task: URLSessionTask,
                didFinishCollecting metrics: URLSessionTaskMetrics) {
    print(metrics)
}
(Task Interval) <_NSConcreteDateInterval: 0x6000006208e0> (Start Date) 2018-07-03 23:15:40 +0000 + (Duration) 0.385012 seconds = (End Date) 2018-07-03 23:15:40 +0000
(Redirect Count) 0
(Transaction Metrics) (Request) <NSURLRequest: 0x600000200300> { URL: https://audio-ssl.itunes.apple.com/apple-assets-us-std-000001/Music/v4/15/84/c1/1584c1fc-687f-88d1-001f-468b26cc6a6e/mzaf_949527388120129365.plus.aac.p.m4a }
(Response) (null)
(Fetch Start) 2018-07-03 23:15:40 +0000
(Domain Lookup Start) (null)
(Domain Lookup End) (null)
(Connect Start) (null)
(Secure Connection Start) (null)
(Secure Connection End) (null)
(Connect End) (null)
(Request Start) 2018-07-03 23:15:40 +0000
(Request End) 2018-07-03 23:15:40 +0000
(Response Start) 2018-07-03 23:15:40 +0000
(Response End) (null)
(Protocol Name) (null)
(Proxy Connection) NO
(Reused Connection) YES
(Fetch Type) Unknown

(Request) <NSURLRequest: 0x6040000156e0> { URL: https://audio-ssl.itunes.apple.com/apple-assets-us-std-000001/Music/v4/15/84/c1/1584c1fc-687f-88d1-001f-468b26cc6a6e/mzaf_949527388120129365.plus.aac.p.m4a }
(Response) <NSHTTPURLResponse: 0x604000434660> { URL: https://audio-ssl.itunes.apple.com/apple-assets-us-std-000001/Music/v4/15/84/c1/1584c1fc-687f-88d1-001f-468b26cc6a6e/mzaf_949527388120129365.plus.aac.p.m4a } { Status Code: 200, Headers {
    "Accept-Ranges" =     (
        bytes
    );
    "Access-Control-Allow-Origin" =     (
        "*"
    );
    Connection =     (
        "keep-alive"
    );
    "Content-Length" =     (
        1000714
    );
    "Content-Type" =     (
        "audio/x-m4a"
    );
    Date =     (
        "Tue, 03 Jul 2018 23:15:40 GMT"
    );
    Etag =     (
        "\"0c09d3cd4e5c8cca49bb08ba1e056d30\""
    );
    "Last-Modified" =     (
        "Fri, 28 Mar 2014 00:28:41 GMT"
    );
    "X-Cache" =     (
        "TCP_HIT from a72-246-188-22.deploy.akamaitechnologies.com (AkamaiGHost/9.3.4.1.2-22867550) (-)"
    );
} }
(Fetch Start) 2018-07-03 23:15:40 +0000
(Domain Lookup Start) 2018-07-03 23:15:40 +0000
(Domain Lookup End) 2018-07-03 23:15:40 +0000
(Connect Start) 2018-07-03 23:15:40 +0000
(Secure Connection Start) 2018-07-03 23:15:40 +0000
(Secure Connection End) 2018-07-03 23:15:40 +0000
(Connect End) 2018-07-03 23:15:40 +0000
(Request Start) 2018-07-03 23:15:40 +0000
(Request End) 2018-07-03 23:15:40 +0000
(Response Start) 2018-07-03 23:15:40 +0000
(Response End) 2018-07-03 23:15:40 +0000
(Protocol Name) http/1.1
(Proxy Connection) NO
(Reused Connection) NO
(Fetch Type) Network Load

キャッシュについて

Accessing Cached Data
を参考にしています。

URL Loading Systemでは、メモリとディスクの両方にレスポンスをキャッシュします。
ネットワーク通信からのキャッシュはURLCacheクラスも用い、シングルトンインスタンスのsharedに直接アクセスできます。
また、独自にインスタンスを作成することもでき、URLSessionConfigurationに設定をすることで利用可能です。

キャッシュが可能なのはHTTP、HTTPSプロトコルのみです。

Cache Policyの設定

Cache Policyはリクエストごとに設定することができます。
どのように動作するかは、URLRequest.CachePolicy次第で変わってきます。
各キャッシュの設定による動作の違いは下記のようになります。

キャッシュポリシー名 ローカルキャッシュの使用 ネットワークからの取得
reloadIgnoringLocalCacheData 使用しない 必ず行う
returnCacheDataDontLoad 必ず使用する 行わない
returnCacheDataElseLoad 最初に使用可能か試す ローカルキャッシュが使用できない場合に行う
useProtocolCachePolicy プロトコル次第 プロトコル次第

useProtocolCachePolicyについて

下記のような流れで動作します。

  1. キャッシュされたレスポンスが存在しない場合、ソース元からデータを取得します。
  2. 上記以外の場合、キャッシュの再検証が不要であったり、キャッシュが最新である(有効期限が切れていない)場合は
    キャッシュされたレスポンスを返します。
  3. キャッシュされたレスポンスが有効期限切れ、または再検証の必要がある場合、ソース元にレスポンスに変更がないか
    HEADリクエストを送ります。変更があった場合はデータを取得、ない場合はキャッシュを返します。

46788d50-fb95-48f2-8360-b8c2a4bf1648.png

HTTPのByte Rangeリクエストの場合

いわゆるバイトを分割してダウンロードをするような場合は
reloadIgnoringLocalCacheDataを使うようにすることとドキュメントには記載があります。

usePrococolCachePolicyのHTTPSのキャッシュの注意点

この場合、レスポンスはディスクに保存されますが、
セキュリティ的に保存しておくのがよろしくないデータも保存されてしまう可能性があります。
これを避けるためにstoreCachedResponse(_:for:)を使って独自にキャッシュを行う必要があります。

URLSessionDataDelegateのurlSession(_:dataTask:willCacheResponse:completionHandler:)
レスポンスごとのキャッシュを処理することが可能です。
注意点として、このメソッドが呼ばれるのはdefaultセッションのdataTaskとuploadTaskのみです。

引数にはCachedURLResponseオブジェクトとcompletionHandlerが渡ってきます。
このcompletionHandlerは必ず呼ばなければならず、引数には下記の3つパターンがあります。

  • 引数で渡ってきたCachedURLResponseオブジェクトをそのまま渡す
  • nilを渡す(キャッシュを防ぐ)
  • storagePolicyuserInfoを修正した新しいCachedURLResponseオブジェクトを渡す

下記の例ですと、HTTPS通信の場合はメモリーにのみキャッシュをするようにしています。

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask,
                willCacheResponse proposedResponse: CachedURLResponse,
                completionHandler: @escaping (CachedURLResponse?) -> Void) {
    if proposedResponse.response.url?.scheme == "https" {
        let updatedResponse = CachedURLResponse(response: proposedResponse.response,
                                                data: proposedResponse.data,
                                                userInfo: proposedResponse.userInfo,
                                                storagePolicy: .allowedInMemoryOnly)
        completionHandler(updatedResponse)
    } else {
        completionHandler(proposedResponse)
    }
}

Cookieについて

ステートレスなREST APIを使用する場合など、
URLリクエストの状態を確認するためにcookieを使って認証の確認などを行います。
※iOSのアプリケーション間でCookieを共有することはありません。

HTTPCookieクラスとHTTPCookieStorageクラスを使います。

HTTPCookie

HTTPCookieStorage

HTTPCookieStorageはシングルトンのsharedインスタンスを持っており、
Cookieの取得、保存、削除などが可能です。

例えば、レスポンスで返ってきたCookieを保存する場合

if let fields = response.allHeaderFields as? [String: String], let url = response.url {
    for cookie in HTTPCookie.cookies(withResponseHeaderFields: fields, for: url) {
        HTTPCookieStorage.shared.setCookie(cookie)
    }
}

取得をする場合

if let cookies = HTTPCookieStorage.shared.cookies(for: url) {
    for cookie in cookies {
        // 特定の名前のCookieを取得
        if (cookie.name == name) {
            return cookie.value
        }
    }
}

削除する場合

if let cookies = HTTPCookieStorage.shared.cookies(for: url) {
    for cookie in cookies {
        // 特定の名前のCookieを削除
        if (cookie.name == name) {
           HTTPCookieStorage.shared.deleteCookie(cookie) 
        }
    }
}
// 指定した日付以降に保存されたクッキーを削除
HTTPCookieStorage.shared.removeCookies(since: Date()) 

認証について

Handling an Authentication Challenge
を参考にしています。

URLSessionTaskを使ってサーバにリクエストを送った際に、サーバから認証要求が返ってくることがあります。
URLSessionTaskは自動でこの要求に対応しようとしますが、対応できなかった時はデリゲートメソッドが呼ばれ、
独自に対応する必要が出てきます。
デリゲートを実装していない場合はサーバから401(Forbidden)が返ってきます。

対応する2つのデリゲートメソッド

要求された認証の種類によって対応するデリゲートメソッドが下記の2つに分かれます。

  1. Task単位の認証の処理を行う。
    URLSessionTaskDelegateのurlSession(_:task:didReceive:completionHandler:)
    例: Basic認証といったユーザ名とパスワードが必要となるものなど

  2. Task単位の認証処理が行われなかった場合に、Session単位の認証の処理を行う。一度認証が満たされれば全てのTaskに影響する。
    URLSessionDelegateのurlSession(_:didReceive:completionHandler:)
    例: TLSの妥当性チェックなど

どっちのメソッドで対処すべきか

デリゲートメソッドが呼ばれた際に、引数にURLAuthenticationChallengeが渡ってきます。
その中に

URLProtectionSpaceのprotectionSpace

というプロパティが存在し、

さらに、そのプロパティが持つ

authenticationMethod

というプロパティの値から判定ができます。

※詳しくは下記のAppleドキュメントにどの定数がどちらのメソッドで対処するべきかの記載があります。
NSURLProtectionSpace Authentication Method Constants
URLProtectionSpace

以下に処理の流れを紹介します。

対処する認証の種類を決める

各メソッドのcompletionHandlerに渡すURLSession.AuthChallengeDispositionの値を変えることで、
その後の動作が変わります。

URLSession.AuthChallengeDisposition

例えば、下記の場合ですと、Basic認証以外はデフォルトの方法で対処する。
つまり、Basic認証以外はこのメソッド内では対応しないということを示しています。

let authMethod = challenge.protectionSpace.authenticationMethod
guard authMethod == NSURLAuthenticationMethodHTTPBasic else {
    completionHandler(.performDefaultHandling, nil)
    return
}

信用情報(クレデンシャル)を作成する

対処する認証要求を受け取った場合、信用情報(クレデンシャル)を作成します。
例えばBasic認証の場合、入力値からusernameとpasswordを取得します。

func credentialsFromUI() -> URLCredential? {
    guard let username = usernameField.text, !username.isEmpty,
        let password = passwordField.text, !password.isEmpty else {
            return nil
    }

    // この場合はURLSessionインスタンスに信用情報は保存される
    // 他のURLSessionインスタンスの場合は新たに信用情報が必要
    return URLCredential(user: username, password: password,
                         persistence: .forSession)
}

completionHandlerを呼ぶ

最終的に認証要求に応じるためにcompletionHandlerは必ず呼ばなければなりません。
仮に信用情報の作成に失敗した場合やユーザが認証のキャンセルを行った場合、
completionHandlerにキャンセルすることを伝えます。

guard let credential = credentialOrNil else {
    completionHandler(.cancelAuthenticationChallenge, nil)
    return
}
completionHandler(.useCredential, credential)

認証に失敗した場合どうなる?

例えば、信用情報が拒否された場合はデリゲートメソッドが再度呼ばれます。
その際
URLAuthenticationChallengeのproposedCredentialプロパティに
拒否された信用情報が格納されています。
また、previousFailureCountプロパティには失敗した回数が格納されています。
こういった値からどのように対処するのかを決めることができます。

proposedCredential

previousFailureCount

Server Trust (2021/11/24追記)

他にもSSL/TLS接続を確立するためにサーバの証明書を確認する方法もあります。
その場合NSURLAuthenticationMethodServerTrustを使います。
下記はmTLSという認証を行おうとしています。こちらを参考にさせて頂きました。

※ delegateは自動でasync形式に変換されているため、そちらを活用しています。


final class ServerTrustTaskDelegate: NSObject, URLSessionTaskDelegate {
    func urlSession(_ session: URLSession,
                    didReceive challenge: URLAuthenticationChallenge)
    async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
        let protectionSpace = challenge.protectionSpace
        if protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
            // クレデンシャルの作成
            let credential = URLCredential(identity: ... as! SecIdentity, certificates: ..., persistence: ...)
            return (.useCredential, credential)
        } else {
            return (.performDefaultHandling, nil)
        }
    }
}

ピン留め(SSL Pinning) (2021/11/24追記)

他にも、信頼できるサーバかどうかを確認するためにピン留めという方法があります。
これは中間者攻撃と呼ばれるセキュリティ問題を防ぐとされています。

  • 証明書ピン留め(Certificate Pinning)
  • 公開鍵ピン留め(Public Key Pinning)

の2種類があります。

こちらのように自分で実装をすることも可能ですが、iOS14以降はInfo.plistに公開鍵をピン留めすることができるようになりました。

詳しくはIdentity Pinning: How to configure server certificates for your appをご覧ください。

※ システムはデフォルトの設定でサーバの信頼性を評価します。上記のピン留めの対応は信頼できる証明書のセットをさらに制限したい場合などに限ります。

HTTP対応について

効率よくURLSessionを使うためのTips

WWDC2017で紹介されていたURLSessionを使用する際のTipsを
記載します。

URLSessionのタイムアウトを活用する

  • timeoutIntervalForResource(リソース全体の取得までのタイムアウト)
    リクエストを初期化してからこのURLConfigurationに関わる全セッションの全タスクが完了するまでの時間。デフォルトは7日。設定は秒数で行う。

  • timeoutIntervalForRequest(リクエストごとのタイムアウト)
    あるタスクがデータを受け取ってから次のデータを受け取るまでの時間。
    デフォルトは60秒。
    バックグラウンドのアップロード、ダウンロードタスクはタイムアウトした際に自動でリトライを行う。この制限は上記のtimeoutIntervalForResourceの設定値。

1アプリ1URLSession

  • 同時平行処理される複数のURLSessionTaskは1つのURLSessionで共有可能。
  • 独自でURLSessionを作成している場合は、finishTasksAndInvalidateやinvalidateAndCancelでデリゲートやコールバックオブジェクトへの参照をクリアしないとメモリーリークを起こす。

finishTasksAndInvalidate

invalidateAndCancel

コールバックとデリゲートは1つのURLSessionで両方使用しない

  • コールバックを使用している場合、デリゲートメソッドが呼ばれることはない
    ※例外的にtaskIsWaitingForConnectivityとdidReceiveAuthenticationChallengeは呼ばれる。

URLConfigurationの設定とタスクの優先度を考える

Default and Ephemeral Configuration + waitsForConnectivity Background Configuration Background Configuration + discretionary
アプリプロセス内 アプリプロセス外 アプリプロセス外
リトライなし timeoutIntervalForResourceまで自動リトライ timeoutIntervalForResourceまで自動リトライ
デリゲート、コールバック両方可能 デリゲートのみ デリゲートのみ
タスク即時実行。実行開始に失敗した場合は、taskIsWaitingForConnectivityが呼ばれ、ネットワーク再接続時に自動でリトライをする タスクがネットワークの状況やバッテリーの状態を配慮して実行する。 システムが最適な状況でタスクが実行されるようにスケジュールされる
緊急度高 緊急度中 緊急度低

単純なbyte streamを扱う場合はURLSessionStreamTaskを使おう

HTTP以外の通信を行う場合、例えばメールの送信などは
URLSessionStreamTaskを使うと効率が良いようです。

HTTP2を活用してレイテンシーを減らそう

URLSessionはHTTP2に自動で対応しています。
コネクションの使い回しによるネットワーク遅延の減少(毎回の3Wayハンドシェイクが不要になる)
といった恩恵が得られます。

HTTP/2 Connection Coalescing

さらに、Connection Coalescingという機能がiOS12より追加されます。
これは

  • 同じIPアドレス
  • 同じTLS認証がされたホスト名
    である場合、コネクションの再利用ができるようになりました。

例えば、
*.example.comというドメインに対する証明書に対して
menu.example.com
deliver.example.com
という2つのサブドメインがあるとします。

今までですと、両方に対してTLS認証を行う必要がありましたが、
iOS12よりこれが一度認証されていれば認証されていると見なされ、
接続時間を短くすることができます。

URLSessionインスタンスの生成数を減らす

上記でも紹介したようにURLSessionインスタンスは極力少ない方が良い。
HTTP2を活用することでよりインタンス生成の必要性を減らすことができる。

リクエストヘッダーのサイズを小さくしてスループットを上げよう

HTTPCookie

  • Cookieを保存するドメインとパスを特定する
  • 必要なものしかCookieに使用しない
  • 不要なものはCookieから削除する
  • 状態(state)はサーバで管理する

HTTPヘッダーの圧縮

  • Gzip
    HTTPとHTTPSに対応している

  • Brotil(iOS11以降)
    HTTPSのみに対応し、textとHTMLに関してはこちらの方が最適化されている

サーバからのレスポンスを考えよう

QoSの活用

細かくは言ってなかったがprefetchなどバックグラウンドで良いものはバックグラウンドで行う。
task.resume()が呼ばれたタイミングでDispatchQueueのQoSは決定される。
Delegateメソッドの呼び出しの優先順位もQoSによって決められる。

Network Service Type

だいたいは.defaultか.backgroundで良い。

iOS12で.responsiveDataが追加された。
これは.defaultよりも優先が高く、
買い物アプリを使用している際の支払いリクエストなどに活用すると、
通常よりもレスポンスの反応が良くなる。

URLSession Adaptable Connectivity

  • URLConfigurationのところでも紹介したwaitsForConnectivityは常にtrueにする
  • 不要になったタスクに関してはtask.cancel()をする

システムリソースを効率良く使用する

  • バックグラウンドセッションの使用
    isDiscretionaryをtrueにしてシステムにタスクの実行タイミングを任せる

  • キャッシュを賢く使う
    不要なものはキャッシュをしないようにデリゲートメソッドで設定する

様々なデリゲート

最後にURLSessionに関連するデリゲートメソッドの一覧を載せておきます。

デリゲート一覧

URLSession(それに関連するクラス)には様々な状況に対応するためのデリゲートがたくさんあります。主に分けると下記の5つになります。

URLSessionDelegate

/* ライフサイクルに関連したメソッド */

// URLSessionがInvalidateになった時に呼ばれる
optional func urlSession(URLSession, didBecomeInvalidWithError: Error?)

// URLSessionにキューされた全てのメッセージが伝達された時に呼ばれる
optional func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession)
/* 認証に関連したメソッド */

// URLSessionレベルの認証要求をサーバからされた際の処理をする
// URLSessionTaskが認証要求の処理に失敗した際に呼ばれる(基本的にはURLSessionTask内で完結)
// ここで認証要求を満たすとURLSessionに関わる全てのURLSessionTaskが認証を満たしているとされる
optional func urlSession(_ session: URLSession, 
              didReceive challenge: URLAuthenticationChallenge, 
       completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)

URLSessionTaskDelegate

/* ライフサイクルに関連したメソッド */

// Taskが完了した際に呼ばれる
// ここのerrorにはサーバのerrorは含まれない
// hostnameの解決ができないなどclient側の理由によるエラーが返ってくる
optional func urlSession(_ session: URLSession, 
                    task: URLSessionTask, 
    didCompleteWithError error: Error?)
/* リダイレクトに関連したメソッド */

// サーバからリダイレクトを要求された際に呼ばれる
// configurationがdefaultかephemeralの時のみ呼ばれ、
// backgroundの場合は自動でリダイレクトする
optional func urlSession(_ session: URLSession, 
                    task: URLSessionTask, 
willPerformHTTPRedirection response: HTTPURLResponse, 
              newRequest request: URLRequest, 
       completionHandler: @escaping (URLRequest?) -> Void)
/* UploadTaskに関連したメソッド */

// 定期的に呼ばれ、サーバにどのくらいデータの送信が完了したかを取得する
optional func urlSession(_ session: URLSession, 
                    task: URLSessionTask, 
         didSendBodyData bytesSent: Int64, 
          totalBytesSent: Int64, 
totalBytesExpectedToSend: Int64)


// サーバにデータを送信するために
// taskから新しいhttpBodyStreamを要求された際に呼ばれる
// ファイルURLやデータオブジェクトからhttpBodyを設定している場合は実装不要
optional func urlSession(_ session: URLSession, 
                    task: URLSessionTask, 
       needNewBodyStream completionHandler: @escaping (InputStream?) -> Void)

下記の条件の際にTaskは上記のメソッドを呼び出す

uploadTask(withStreamedRequest:)から作成されたTaskの
最初のhttpBodyStreamが提供されたとき

認証要求やサーバの回復可能なエラーが原因で
httpBodyStreamを持ったリクエストを再送信する必要がある場合で
代わりになるhttpBodyStreamが提供されたとき

uploadTask(withStreamedRequest:)

/* 認証に関連したメソッド */

// URLSessionレベル以外の認証要求をサーバからされた際の処理をする
// URLSessionDelegateを実装している場合に実装しなければならない
optional func urlSession(_ session: URLSession, 
                    task: URLSessionTask, 
              didReceive challenge: URLAuthenticationChallenge, 
       completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)

// (earlyBeginDateプロパティで設定された)遅延開始時刻を持つバックグラウンドセッションタスクの開始準備が整ったときに呼び出される
// このメソッドは、ネットワークのロードを待っている間にリクエストが古くなり、新しいリクエストに置き換える必要がある場合にのみ実装する必要がある
optional func urlSession(
    _ session: URLSession,
    task: URLSessionTask,
    willBeginDelayedRequest request: URLRequest,
    completionHandler: @escaping @Sendable (URLSession.DelayedRequestDisposition, URLRequest?) -> Void
)
/* Delayed、Waiting Taskに関連したメソッド */

// delayedURLSessionTaskが始まる際に呼ばれる
// そのまま処理を継続するか、新しいリクエストとして処理するか、
// キャンセルするかを決めることができる
optional func urlSession(_ session: URLSession, 
                    task: URLSessionTask, 
 willBeginDelayedRequest request: URLRequest, 
       completionHandler: @escaping (URLSession.DelayedRequestDisposition, URLRequest?) -> Void)

// URLSessionConfigurationのwaitsForConnectivityプロパティがtrueで、ネットワーク接続が利用できない場合に呼び出される。 
// デリゲートはこの機会を利用してユーザインターフェイスを更新できる 例: オフラインモードまたはCellular専用モードを提示する
// このメソッドは、接続が最初に利用できない場合にのみ、タスクごとに最大1回呼び出される
// バックグラウンドセッションでは waitsForConnectivityが無視されるため、バックグラウンド セッションでは呼び出されない
optional func urlSession(
    _ session: URLSession,
    taskIsWaitingForConnectivity task: URLSessionTask
)
/* Metricsに関連したメソッド */

// Sessionが特定のTaskに関連したMetrics情報を収集し終えた際に呼ばれる
optional func urlSession(_ session: URLSession, 
                    task: URLSessionTask, 
     didFinishCollecting metrics: URLSessionTaskMetrics)

urlSession(_:task:didFinishCollecting:)

/* Taskの生成に関連したメソッド */ iOS16以降

// タスクが作成されたという通知
// このメソッドは、タスクが送信する最初のメッセージであり、再開する前にタスクを構成する場所を提供する
// 注意: このdelegateメソッドはdelegateQueueにディスパッチされない。タスク作成メソッドがリターンする前に同期的に呼び出される
optional func urlSession(
    _ session: URLSession,
    didCreateTask task: URLSessionTask
)

urlSession(_:didCreateTask:)

Appleドキュメント

URLSessionDataDelegate

/* ライフサイクルに関連したメソッド */

// サーバから最初にデータ(ヘッダー)を受け取った際に呼ばれる
// multipart/x-mixed-replaceが必要な場合は必須になる
// この時点で処理のキャンセルをすることもできる
optional func urlSession(_ session: URLSession, 
                dataTask: URLSessionDataTask, 
              didReceive response: URLResponse, 
       completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)


// 上記のdidReceiveメソッドで
// URLSession.ResponseDisposition.becomeDownloadが指定され
// dataTaskからdownloadTaskに変わった際に呼ばれる
// これが呼ばれると元のdataTaskのデリゲートは呼ばれなくなる
optional func urlSession(_ session: URLSession, 
                dataTask: URLSessionDataTask, 
               didBecome downloadTask: URLSessionDownloadTask)

// 上記のdidReceiveメソッドで
// URLSession.ResponseDisposition.becomeStreamが指定され
// dataTaskからstreamTaskに変わった際に呼ばれる
// これが呼ばれると元のdataTaskのデリゲートは呼ばれなくなる
// 元々繋がっていたURLリクエストに対してはstreamTaskは読み取り専用になり
// URLSessionStreamDelegateのwriteClosedForメソッドが即座に呼ばれる
// configurationのhttpShouldUsePipeliningをfalseにすることで
// リクエストのパイプライン化(分割)を不可にすることができる
// 個々のリクエストの設定はURLRequestのhttpShouldUsePipelining
optional func urlSession(_ session: URLSession, 
                dataTask: URLSessionDataTask, 
               didBecome streamTask: URLSessionStreamTask)

/* データ受信に関連したメソッド */

// データを受信した際に呼ばれる
// 受信データは多くの異なるオブジェクトのまとめてあるものが多いため
// 可能な時はenumeratedBytesを使うようにする
// ※bytesを使うと受信データは1つのメモリーブロックに入っている
// 受信データは前回以降に受信したデータのみを受け取る
optional func urlSession(_ session: URLSession, 
                dataTask: URLSessionDataTask, 
              didReceive data: Data)

enumerateBytes

bytes

/* キャッシュに関連したメソッド */

// 全てのデータ受信が完了した後に呼ばれ、
// レスポンスをキャッシュに保存するかどうかを決める
// このメソッドが実装されていない場合、configurationのポリシーに従う
// ある特定のURLのキャッシュを防いだり、userInfoのデータを変更する場合に使う
optional func urlSession(_ session: URLSession, 
                dataTask: URLSessionDataTask, 
       willCacheResponse proposedResponse: CachedURLResponse, 
       completionHandler: @escaping (CachedURLResponse?) -> Void)

レスポンスがキャッシュされる条件
-HTTPまたはHTTPSのURLであること
-リクエストが成功している(ステータスコードが200~299)
-サーバから来たレスポンスである
-configurationでキャッシュの保存を許可している
-URLRequestでキャッシュの保存を許可している
-サーバレスポンスのヘッダーのでキャッシュの保存が許可されている(ヘッダーがある場合)
-レスポンスサイズがキャッシュできる範囲の大きさであること(例えばディスクキャッシュサイズの5%以下であること)

Appleドキュメント

URLSessionDownloadDelegate

/* ライフサイクルに関連したメソッド */

// ダウンロードが完了した際に呼ばれる
// locationのURLはダウンロードしたtemporaryファイルのURL
func urlSession(_ session: URLSession, 
   downloadTask: URLSessionDownloadTask, 
didFinishDownloadingTo location: URL)

/* Resumeデータに関連したメソッド */

// ダウンロードが再開された際に呼ばれる
// ダウンロード済のデータが使用できなくなっていた場合、fileOffsetは0
optional func urlSession(_ session: URLSession, 
            downloadTask: URLSessionDownloadTask, 
       didResumeAtOffset fileOffset: Int64, 
      expectedTotalBytes: Int64)


再開可能なTaskがキャンセルされた場合や失敗した場合
再開するために必要なデータを取得することができる
これを使用してdownloadTask(withResumeData:)
downloadTask(withResumeData:completionHandler:) を呼ぶことができる
これらのメソッドが呼ばれTaskが開始されると
sessionはデリゲートメソッドを即時に呼びダウンロードが再開されたことを
教えてくれる

/* プログレスに関連したメソッド */

// 定期的に呼ばれ、ダウンロードの進行状況の処理をする
optional func urlSession(_ session: URLSession, 
            downloadTask: URLSessionDownloadTask, 
            didWriteData bytesWritten: Int64, 
       totalBytesWritten: Int64, 
totalBytesExpectedToWrite: Int64)

Appleドキュメント

URLSessionStreamDelegate

/* 再ルーティングに関連したメソッド */

// より良いルートを利用可能になった際に呼ばれる
// 例えば端末回線を使用していてWifiが利用可能になった際など
// 待機中のタスクを完了させたり、タスクの新規作成などを検討する
optional func urlSession(_ session: URLSession, 
betterRouteDiscoveredFor streamTask: URLSessionStreamTask)
/* ストリームの完了に関連したメソッド */

// captureStreamsメソッドが呼ばれ、
// Task内の読み書き処理が全て完了した際に呼ばれる
optional func urlSession(_ session: URLSession, 
              streamTask: URLSessionStreamTask, 
               didBecome inputStream: InputStream, 
            outputStream: OutputStream)

captureStream

/* streamのクローズに関連したメソッド */

// 読み取り側のソケットが閉じた際に呼ばれる
// 読むデータがなくなった場合にも呼ばれる
// 呼ばれたからといってEOFに到達したという訳ではない
optional func urlSession(_ session: URLSession, 
           readClosedFor streamTask: URLSessionStreamTask)           

// 書き込み側のソケットが閉じた際に呼ばれる
// 書くデータがなくなった場合にも呼ばれる
optional func urlSession(_ session: URLSession, 
          writeClosedFor streamTask: URLSessionStreamTask)

Appleドキュメント

iOS15(macOS Monterey)以降のHTTP/3サポート

iOS15(macOS Monterey)以降、HTTP/3とその基盤となるQUICプロトコルがサポートされました。

HTTP/3とは?

2015年5月に規格化された HTTP/2よりウェブサイトの表示速度をさらに高速化するための通信プロトコルのバージョン。
通信プロトコルなどインターネット技術の標準化を推進する IETF(Internet Engineering Task Force)は、2021年5月27日(米国時間)に、通信プロトコルである QUIC(Quick UDP Internet Connections)を標準化した技術仕様を承認しています。
https://datatracker.ietf.org/doc/html/rfc9000

HTTP/2と比較して:

  • コネクションのセットアップがより早くなり、接続リクエストがすぐに送られる
    - 独立したストリームそれぞれ独立しているため、パケットロスの影響をお互いに受けない
    - 一方でそれらは一つのTCPコネクションで共有されているおり、アイドル時間が少なく効率が良い
    - QUICがベースになっている

QUICとは?

インターネット黎明期から使われている TCP(Transmission Control Protocol)の代替を目指して、Google によって実験的に開発された UDP(User Datagram Protocol)上で動作するトランスポート層プロトコルです。

下記のような特徴があります:

  • TCPと同じ概念を用いているが、end-to-endの暗号化、多重化ストリーム、認証機能を提供している
  • QUICのセキュリティはTCP1.3プロトコルの上に構築されている
  • TCPのような3wayハンドシェイクを1回のラウンドトリップに減らしている
  • 多重化ストリームによってhead of line blocking(パケットロスを防ぐためにパケットロス時に後続のパケットで待ちが発生する現象)が起きない
  • 受信したパケットに関するより複雑な情報を他のエンドポイントに通信でき、TCPの制限に苦しむことはなく、パケットロスからの回復性が向上する
  • 接続の移行をサポートしているため、新しくセッションを確立することなしにさまざまなネットワークインターフェイス(cellularとwifiなど)を超えてシームレスに接続を移行できる

URLSessionとHTTP/3

詳細はこちら:TN3102: HTTP/3 in Your App

実装例

URLSessionを使っている場合、iOS15(macOS Monterey)以降はデフォルトでサポートされており、サーバでHTTP/3をサポートすればそのまま使うことができます。

※ サーバはDraft29以降を基にサポートする必要があります。

下記のコードで使われているかどうかが確認できます。

コード例

下記を実行します。

  1. iOS15以降のデバイスを使用する(シミュレータではHTTP/3をサポートしていないため)
  2. 設定 > デベロッパ > HTTP/3をオンにする(Xcode13.3 beta2だと設定してなくてもHTTP/3が使われる)
  3. Xcode13.0以降でSwiftUIアプリを作成する
  4. 下記のコードをコピーする
  5. デバイスで実行する
  6. Testボタンをタップする

// HTTP3App.swift

@main
struct HTTP3App: App {
    let networkManager = NetworkManager()
    var body: some Scene {
        WindowGroup {
            ContentView(networkManager: networkManager)
        }
    }
}

// ContentView.swift

import SwiftUI
import os.log

struct ContentView: View {
    var networkManager: NetworkManager

    var body: some View {
        VStack(alignment: .leading) {
            Button("Test HTTP3") {
                networkManager.testHTTP3Request()
            }.padding(.vertical, 20)
        }.padding(40)
    }
}

class NetworkManager: NSObject, URLSessionDataDelegate {

    private var session: URLSession!

    func testHTTP3Request() {

        if self.session == nil {
            let config = URLSessionConfiguration.default
            config.requestCachePolicy = .reloadIgnoringLocalCacheData
            self.session = URLSession(configuration: config, delegate: self, delegateQueue: .main)
        }

        let urlStr = "https://google.com"
        let url = URL(string: urlStr)!
        var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 60.0)
        request.assumesHTTP3Capable = true
        os_log("task will start, url: \(url.absoluteString)")
        self.session.dataTask(with: request) { (data, response, error) in
            if let error = error as NSError? {
                os_log("task transport error \(error.domain) / \(error.code)")
                return
            }
            guard let data = data, let response = response as? HTTPURLResponse else {
                os_log("task response is invalid")
                return
            }

            guard 200 ..< 300 ~= response.statusCode else {
                print("task response status code is invalid; received \(response.statusCode), but expected 2xx")
                return
            }
            os_log("task finished with status \(response.statusCode), bytes \(data.count)")
        }.resume()

    }
}

extension NetworkManager {
    func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
        let protocols = metrics.transactionMetrics.map { $0.networkProtocolName ?? "-" }
        os_log("protocols: \(protocols)")
    }
}

実行すると、下記のようなログがコンソールに出力されます:

task will start, url: https://google.com
protocols: ["h3", "h3"]
task finished with status 200, bytes 14072

protocolsには使用されているHTTPトランザクションのプロトコルが示されています。これは"h2"や"http1.1"の場合があります。その場合はもう一度実行すると、URLSessionは、サーバーが前のHTTP応答で送り返したHTTP/3にサービスディスカバリオプションを適用する機会が与えられます。

HTTP/3にサービスディスカバリは次の内の一つが実行されます:

  • 推奨されるアプローチは、alpn="h3,h2"のHTTPSリソースレコードをアドバタイズするようにDNSサーバを構成する
  • または、HTTP/3をアドバタイズするAlt-Srvヘッダで応答するようにサーバを構成します。 たとえば、Alt-Srv: h3 = ":443"; ma=2592000

最初のトランザクションからHTTP/3の利用を試みるには、URLRequestassumesHTTP3CapabletrueにしてURLSessiondataTaskを実行する

サーバやネットワークがHTTP/3をサポートしていない場合は"h2"や"http1.1"にフォールバックされます。

より詳しい情報はWWDCのセッションで解説されています。

HTTPトラフィックのデバッグ

デバッグ時に有用なツールとしてXcode13でNetwork profileのテンプレートが追加され、HTTPのトラフィックを追跡できます。

Xcode Version 13.2.1 (13C100)
スクリーンショット 2022-02-13 12.21.07.png

より詳しい情報は[こちらの記事]
(https://developer.apple.com/documentation/foundation/url_loading_system/analyzing_http_traffic_with_instruments)に記載されています。

まとめ

全然まとまってないですね:cry:

URLSessionはこれからも機能の増加や変更などが入ってくると思いますので、
随時更新して頭の整理をしていきたいと思います。

間違いなどございましたらご指摘いただけるとうれしいです:bow_tone1:

悩み

ソースコードの部分はアコーディオンになるように設定しているのですが、
VSCodeだときちんと反映され、Qiitaだとなぜかうまくいかないですね:cold_sweat:

349
276
2

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
349
276