📦URLSession.DataTaskPublisher

負責透過 URL 到網路抓資料的 Publisher。

參考資料

將 Data 轉為 Swift 型別

下面的範例示範如何從網路擷取 JSON 資料,然後轉為自己的 Swift 型別: 👉 Processing URL Session Data Task Results with Combine - Convert Incoming Raw Data to Your Types

import Foundation  // URL
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

// consoles
var con = ""
let console = PlaygroundConsole.default    // 🐶 PlaygroundConsole

// Post
struct Post: Codable {
    let userId: Int
    let id    : Int
    let title : String
    let body  : String
}

// 🌀 URL extension
let posts: URL = "http://jsonplaceholder.typicode.com/posts"

let cancellable = URLSession.shared

    // ⭐️ produces a tuple: (data: Data, response: URLResponse)
    .dataTaskPublisher(for: posts)
    
    // ⭐️ use map(_:) to convert contents of tuple to another type.
    // ⭐️ use tryMap(_:) to inspect `response` before inspecing `data`,
    //    throw an error if the response is unacceptable.
    .tryMap() { tuple -> Data in
        guard 
            let httpResponse = tuple.response as? HTTPURLResponse,
            httpResponse.statusCode == 200 
            else { throw URLError(.badServerResponse) }
        return tuple.data 
    }
        
    // ⭐️ convert data to their own types
    .decode(type: [Post].self, decoder: JSONDecoder())
    
    // ⭐️ receive final results
    .sink(receiveCompletion: { 
        print ("Received completion: \($0).", to: &con); con 
    }, receiveValue: { 
        print ("Received posts: \($0.prefix(3)).", to: &con); con 
        print("posts: ", $0.count); console
    })

錯誤處理

  • 暫時的錯誤可用 .retry(_:) 處理。

  • 無法排除的錯誤可用 .catch(_:) 與 .replaceError(with:) 處理

/*
 * Retry Transient Errors
 * Catch and Replace Persistent Errors
 * -----------------------------------
 */

let fallbackURL: URL = "some fallback url"
let fallbackSession = URLSession.shared

session
    .dataTaskPublisher(for: url)
    
    // ⭐️ transient errors:
    .retry(1)
    
    // ⭐️ persistent errors:
    //    use catch(_:) to replace error with another publisher
    //    use replaceError(with:) to replace error with an element you provide
    .catch() { _ in
        fallbackSession.dataTaskPublisher(for: fallbackURL)  
    }
    
    .sink(receiveCompletion: { 
        print("Received completion: \($0).") 
    }, receiveValue: { 
        print("Received data: \($0.data).") 
    })

Scheduling Operators

/*
 * Move Work Between Dispatch Queues With Scheduling Operators
 * -----------------------------------------------------------
 */

session
    .dataTaskPublisher(for: url)
    
    // ⭐️ specify how you want later operators in the chain and your subscriber,
    //    to schedule the work
    .receive(on: DispatchQueue.main)
    
    .sink(receiveCompletion: { 
        print ("Received completion: \($0).") 
    }, receiveValue: { 
        print ("Received data: \($0.data).")
    })

分享網路擷取資料

/*
 * Share Result with Multiple Subscribers
 * --------------------------------------
 */

var set = Set<AnyCancellable>()

let shared = session
    .dataTaskPublisher(for: url)
    
    // ⭐️ use share() to support multiple downstream subscribers
    .share()

shared
    .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).")
    }).store(in: &set)

shared
    .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.")
        }
    }).store(in: &set)

⚠️ 注意:

  • URL session starts loading data as soon as the URLSession.DataTaskPublisher has unsatisfied demand from a downstream subscriber. In this case, that happens when the first sink subscriber attaches.

  • If you need extra time to attach other subscribers, use .makeConnectable() to wrap the Publishers.Share publisher with a ConnectablePublisher.

  • After connecting all subscribers, call .connect() on the connectable publisher to allow the data load to begin.

Last updated