跳转至

第9章:网络

作为程序员,我们所做的许多工作都围绕着网络。与后端通信,获取数据,推送更新,编码和解码JSON......这是移动开发人员的日常。

Combine提供了一些精选的API,以帮助声明性地执行常见任务。这些API围绕着现代应用程序的两个关键组件展开:

  • 使用URLSession执行网络请求。
  • 使用Codable协议对JSON数据进行编码和解码。

URLSession扩展

URLSession是执行网络数据传输任务的标准方式。它提供了一个现代化的异步API,具有强大的配置选项和完全透明的背景支持。它支持各种操作,例如:

  • 数据传输任务,以检索URL的内容。
  • 下载任务以检索URL的内容并将其保存到文件中。
  • 上传任务以将文件和数据上传到URL
  • 流化任务,在双方之间流化数据。
  • 连接到网络套字节程序的Websocket任务。

其中,只有第一个,数据传输任务,暴露了一个Combine发布者。Combine使用单个API处理这些任务,具有两个变体,获取URLRequest或仅取URL

以下是如何使用此API的了解:

guard let url = URL(string: "https://mysite.com/mydata.json") else { 
  return 
}

// 1
let subscription = URLSession.shared
  // 2
  .dataTaskPublisher(for: url)
  .sink(receiveCompletion: { completion in
    // 3
    if case .failure(let err) = completion {
      print("Retrieving data failed with error \(err)")
    }
  }, receiveValue: { data, response in
    // 4
    print("Retrieved data of size \(data.count), response = \(response)")
  })

以下是此代码的情况:

  1. 保留生成的订阅至关重要;否则,它将立即取消,请求永远不会执行。
  2. 您正在使用dataTaskPublisher(for:)的重载,该重载将URL作为参数。
  3. 确保您始终处理错误!网络连接容易出现故障。
  4. 结果是一个同时带有Data对象和URLResponse元组。

如您所见,CombineURLSession.dataTask上提供了一个透明的裸骨发布者抽象,只暴露了发布者而不是闭包。

可提供可提供的支持

Codable协议是一种现代、功能强大且仅限Swift的编码和解码机制,您绝对应该了解。如果您不这样做,请帮自己一个忙,并从raywenderlich.com上的苹果文档和教程中了解它!

Foundation支持通过JSONEncoderJSONDecoderJSON进行编码和解码。您还可以使用PropertyListEncoderPropertyListDecoder,但这些在网络请求中不太有用。

在前面的示例中,您下载了一些JSON。 当然,您可以使用JSONDecoder对其进行解码:

let subscription = URLSession.shared
  .dataTaskPublisher(for: url)
  .tryMap { data, _ in
    try JSONDecoder().decode(MyType.self, from: data)
  }
  .sink(receiveCompletion: { completion in
    if case .failure(let err) = completion {
      print("Retrieving data failed with error \(err)")
    }
  }, receiveValue: { object in
    print("Retrieved object \(object)")
  })

您可以在tryMap中解码JSON,该尝试映射有效,但Combine提供了一个操作符来帮助减少样板:decode(type:decoder:)

在上面的示例中,将tryMap操作符替换为以下行:

.map(\.data)
.decode(type: MyType.self, decoder: JSONDecoder())

不幸的是,由于dataTaskPublisher(for:)会发出元组,如果不首先使用仅发送结果Data部分的map(_:),您就无法直接使用decode(type:decoder:)

唯一的优势是,在设置发布者时,您只实例化一次JSONDecoder,而不是每次在tryMap(_:)闭包中创建它。

将网络数据发布给多个订阅者

每次您订阅发布者时,它都会开始工作。在网络请求的情况下,这意味着如果多个订阅者需要结果,则多次发送相同的请求。

令人惊讶的是,像其他框架一样,Combine缺乏操作符来简化这一点。您可以使用share()操作符,但这很棘手,因为您需要在结果回来之前订阅所有订阅者。

除了使用缓存机制外,一个解决方案是使用multicast()操作符,该操作符创建一个ConnectablePublisher,通过Subject发布值。它允许您多次订阅主题,然后在您准备好后调用发布者的connect()方法:

let url = URL(string: "https://www.raywenderlich.com")!
let publisher = URLSession.shared
// 1
  .dataTaskPublisher(for: url)
  .map(\.data)
  .multicast { PassthroughSubject<Data, URLError>() }

// 2
let subscription1 = publisher
  .sink(receiveCompletion: { completion in
    if case .failure(let err) = completion {
      print("Sink1 Retrieving data failed with error \(err)")
    }
  }, receiveValue: { object in
    print("Sink1 Retrieved object \(object)")
  })

// 3
let subscription2 = publisher
  .sink(receiveCompletion: { completion in
    if case .failure(let err) = completion {
      print("Sink2 Retrieving data failed with error \(err)")
    }
  }, receiveValue: { object in
    print("Sink2 Retrieved object \(object)")
  })

// 4
let subscription = publisher.connect()

在这个代码中,您:

  1. 创建您的DataTaskPublishermap到其数据,然后进行multicast。您通过的闭包必须返回适当类型的主题。或者,您可以将现有主题传递给multicast(subject:)您将在第13章“资源管理”中了解有关multicast的更多信息。
  2. 首次订阅发布者。由于它是ConnectablePublisher,它不会立即开始工作。
  3. 第二次订阅。
  4. 准备好后,连接发布者。它将开始工作,并向所有订阅者推送价值。

使用此代码,您将发送一次请求,并与两个订阅者共享结果。

Note

确保存储所有Cancellable;否则,当离开当前代码范围时,它们将被释放和取消,在这种情况下,这将是即时的。

这个过程仍然有点复杂,因为Combine不像其他被动框架那样为此类场景提供操作符。在第18章“自定义发布者和处理背压”中,您将探索制定更好的解决方案。

关键点

  • Combine为其dataTask(with:completionHandler:)方法提供了一个基于发布者的抽象,称为dataTaskPublisher(for:)
  • 您可以使用发送Data值的发布者上的内置decode操作符解码符合Codable的模型。
  • 虽然没有操作符可以与多个订阅者共享订阅的重播,但您可以使用ConnectablePublishermulticast操作符重新创建此行为。

接下来去哪?

完成这一章做得很好!

如果您想了解有关使用Codable的更多信息,您可以查看以下资源: