wiftクリーンアップ-URLSessession+Combine


出典:Processing URL Session Data Task Results with Combine - Apple Developer
非同期演算子を使用してURLからのデータを処理する方法について説明します.
ネットワーク操作は本質的に非同期操作であり,これらの非同期操作を処理する方法には多くの種類がある.
Combineも非同期を処理するフレームワークであり、ネットワーク操作を簡素化することができます.

dataTaskPublisher(for:)

URLSessionは、URLまたはURLRequestから受信したデータを発行するためのURLSession.DataTaskPublisherという組合せのパブリッシャーを提供する.dataTaskPublisher(for:)メソッドでpublisherを作成できます.これにより、次の2つのコンテンツを解放できます.
  • task成功時
    dataとURLResponseからなるtuple
  • taskが失敗した場合
    error
  • 既存のdataTask(with:completionHandler:)とは異なり、publisherはこのオプションを無効にします.
    また、完了ハンドラを使用するコードは、完了ハンドラモジュールを使用してすべての関連タスクを処理する必要があります.ただし、publisherを使用する場合は、多くのモジュールタスクをCombine演算子で置き換えることができます.

    Raw Data -> Custom Type


    ネットワーク操作が完了すると、Dataタイプの値が受信されます.
    Combineは、DataからCustom Typeに変換する操作をサポートする演算子を提供する.
    前述したように、タスクが成功すると、DataおよびURLResponseが送信され、タイプをmap(_:)またはtryMap(_:)に変更できます.DataCustom Typeに変更します.
    (Custom Type仮定Decodable.)
    Combineのdecode(type:decoder:)を使用します.
    次に、URLから受信したjsonデータをUserタイプに変換する手順を示します.
    struct User: Codable {
        let name: String
        let userID: String
    }
    let url = URL(string: "https://example.com/endpoint")!
    cancellable = urlSession
        .dataTaskPublisher(for: url)
        .tryMap() { element -> Data in
            guard let httpResponse = element.response as? HTTPURLResponse,
                httpResponse.statusCode == 200 else {
                    throw URLError(.badServerResponse)
                }
            return element.data
            }
        .decode(type: User.self, decoder: JSONDecoder())
        .sink(receiveCompletion: { print ("Received completion: \($0).") },
              receiveValue: { user in print ("Received user: \(user).")})
  • dataTaskPublisher(for: url)を使用してネットワーク操作を開始します.
  • tryMap() { element -> Data in }を使用して、元のdataTaskPublisherリリースされた継ぎ手(Data,Response)を確認し、所望の形状に加工します.
  • decode(type: User.self, decoder: JSONDecoder())からtryMap()へのデータをユーザタイプに変換します.
  • Retry + Error Handling


    ネットワーク・アクティビティは、多くのエラーが予想されるアクティビティです.そのため、私たちのアプリケーションはエラーをうまく処理する必要があります.
    場合によっては、失敗したタスクを再試行する必要がある場合があります.
    完了ハンドラを使用する場合は、再試行のためにハンドラを再作成する必要があります.

    retry(_:)


    Combineはretry(_:)行であってもよい.
    エラーが発生した場合は、指定した回数で上流パブリケーションサーバへのサブスクリプションを再作成します.
    ただし、ネットワーク・アクティビティのコストが高いため、再試行を過度に要求するべきではありません.また、すべてのリクエストが有効であることを確認する必要があります.

    catch(_:)


    エラーを別のパブリケーションサーバに変換します.
    fallback URLからデータをロードするのと同様に、他のURLSession.DataTaskPublisherとともに使用できます.

    replaceError(with:)


    エラーを開発者が提供した内容に変更します.
    エラーを他の内容に変更して、成功したときと同じ処理を実行することもできます.
    let pub = urlSession
        .dataTaskPublisher(for: url)
        .retry(1)
        .catch() { _ in
            self.fallbackUrlSession.dataTaskPublisher(for: fallbackURL)
        }
    cancellable = pub
        .sink(receiveCompletion: { print("Received completion: \($0).") },
              receiveValue: { print("Received data: \($0.data).") })
  • retry(1)エラーが発生した場合は、上流パブリケーションサーバのサブスクリプションが再び作成されます.
  • catch()エラーが発生した場合はfallBackUrlSessionを呼び出すことができます.
    この構文を実行することは、retry(1)が1回後に実行されることを意味するので、2回目errorと呼ぶことができる.
  • Dispatch Queue + Scheduling Operators

    URLSessionを完了ハンドラとして扱う場合は、ハンドラ内部のDispatch Queueなどを直接使用して特定のキューにタスクを移動する必要があります.receive(on:options:)メソッドを使用すると、サブスクリプション・サーバとオペレータのメソッド後のワークキューを指定できます.
    DispatchQueueとRunLoopは、いずれもCombineのSchedulerプロトコルを実現しているので、すぐに使用できます.
    cancellable = urlSession
        .dataTaskPublisher(for: url)
        .receive(on: DispatchQueue.main)
        .sink(receiveCompletion: { print ("Received completion: \($0).") },
              receiveValue: { print ("Received data: \($0.data).")})
  • receive(on: DispatchQueue.main),sinkのログがDispatch Queueのmainから出力されます.
  • Share Data Task Publisher


    ネットワークアクティビティのコストが高い.したがって、アプリケーションの複数の部分から1つのDataTaskPublisherを1つのリクエストで購読できます.
    Combineのshare()演算子を使用すると、複数の演算子とサブスクリプションサーバを接続できますが、上流のパブリッシャは1つの下流しか表示できません.これは、URLSession.DataTaskPublisherがこのネットワーク操作を一度だけ実行することを意味する.
    let sharedPublisher = urlSession
        .dataTaskPublisher(for: url)
        .share()
    
    cancellable1 = sharedPublisher
        .tryMap() {
            guard $0.data.count > 0 else { throw URLError(.zeroByteResource) }
            return $0.data
        }
        .decode(type: User.self, decoder: JSONDecoder())
        .receive(on: DispatchQueue.main)
        .sink(receiveCompletion: { print ("Received completion 1: \($0).") },
              receiveValue: { print ("Received id: \($0.userID).")})
    
    cancellable2 = sharedPublisher
        .map() {
            $0.response
        }
        .sink(receiveCompletion: { print ("Received completion 2: \($0).") },
               receiveValue: { response in
                if let httpResponse = response as? HTTPURLResponse {
                    print ("Received HTTP status: \(httpResponse.statusCode).")
                } else {
                    print ("Response was not an HTTPURLResponse.")
                }
        }
    )
  • share()複数のサブスクライバが許可されているが、ネットワーク操作は1回のみ可能である.

  • 2つのサブスクリプション・サーバをsharedPublisherに接続します.
  • sharedPublisherが一度だけ要求されていることを確認するには、.print(_:to:).share()演算子の上に配置してデバッグします.
  • 最初のsinkがダウンストリームサブスクリプションサーバに接続されていない場合、URLセッションはデータをロードします.
    他のサブスクリプションサーバに接続する時間が必要な場合は、makeConnectable()を使用してPublisher.SharepublisherをConnectablePublisherに変換します.
    すべてのサブスクリプション・サーバが準備ができている場合は、connect()メソッドを使用してデータのロードを開始できます.