Swift Combine — Notification、URLSession、Timer等Publisher的理解与使用

Notification Publisher

SwiftCombine框架中,可以使用NotificationCenter.Publisher来创建一个能够订阅和接收通知的Publisher

swift 复制代码
// 创建一个订阅通知的Publisher
let notificationPublisher = NotificationCenter.default.publisher(for: Notification.Name("CustomNotification"))

接下来,我们可以订阅这个Publisher,并处理接收到的通知。

swift 复制代码
// 订阅通知
let cancellable = notificationPublisher.sink { notification in
    // 处理接收到的通知
    print("Received notification: \(notification)")
}

发送通知

swift 复制代码
// 发送通知
NotificationCenter.default.post(name: Notification.Name("CustomNotification"), object: nil)

下面代码中就是一个完整的例子:

swift 复制代码
class NotificationViewModel: ObservableObject {
  private var cancellable = Set<AnyCancellable>()

  func setUpNotification() {
    let notificationPublisher = NotificationCenter.default.publisher(for: Notification.Name("CustomNotification"))
    notificationPublisher
      .sink { notification in
        print("Received notification: \(notification)")
      }
      .store(in: &cancellable)
  }

  func sendNotification() {
    NotificationCenter.default.post(name: Notification.Name("CustomNotification"), object: nil)
  }
}

struct NotificationDemo: View {
  @StateObject private var viewModel = NotificationViewModel()

  var body: some View {
    Button("Send Notification") {
      viewModel.sendNotification()
    }
    .buttonStyle(BorderedProminentButtonStyle())
    .onAppear {
      viewModel.setUpNotification()
    }
  }
}

上面代码中在ViewModel中定义并且订阅了Notification Publisher,在SwiftUI界面触发NotificationCenter发送通知,随后在sink方法中收到了该通知。

除了这种用法外,有的时候也可以直接在SwiftUI界面通过onReceive方式使用。

现在SwiftUI界面定义一个Notification

swift 复制代码
// app 进入前台前的通知
let willEnterForegroundPublisher = NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)

然后设置onReceive方法:

swift 复制代码
.onReceive(willEnterForegroundPublisher, perform: { notification in
  print("Received App will enter foreground notification")
})

这样在App从后台回前台的时候就触发了这个通知,onReceive的闭包中的打印就会输出。

完整代码如下:

swift 复制代码
struct NotificationDemo1: View {
  // app回前台的通知
  let willEnterForegroundPublisher = NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)

  var body: some View {
    VStack {
      Text("Hello World")
    }
    .onReceive(willEnterForegroundPublisher, perform: { notification in
      print("Received App will enter foreground notification")
    })
  }
}

如果想在这个界面添加多个通知,那是不是要加多个onReceive方法呢?也可以不是的,比如像这样:

swift 复制代码
.onReceive(Publishers.MergeMany(willEnterForegroundPublisher, didEnterBackgroundPublisher), perform: { notification in
  print("Received App \(notification)")
})

可以通过Publishers.MergeMany方法将多个Publisher合并,然后在一个回调中处理收到通知事件。

swift 复制代码
struct NotificationDemo1: View {
  // app回前台的通知
  let willEnterForegroundPublisher = NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)

  // app进入后台通知
  let didEnterBackgroundPublisher = NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)

  var body: some View {
    VStack {
      Text("Hello World")
    }
    .onReceive(Publishers.MergeMany(willEnterForegroundPublisher, didEnterBackgroundPublisher), perform: { notification in
      print("Received App \(notification)")
    })
  }
}

URLSession Publisher

SwiftCombine框架中,URLSession.DataTaskPublisher提供了一种方便的方式来执行网络请求并处理返回的数据。

首先创建一个Publisher

swift 复制代码
// 创建一个网络请求Publisher
let url = URL(string: "https://......")!
let request = URLRequest(url: url)
let dataTaskPublisher = URLSession.shared.dataTaskPublisher(for: request)

接下来,我们可以订阅这个Publisher,并处理接收到的数据和错误。

swift 复制代码
// 订阅网络请求
let cancellable = dataTaskPublisher
    .map(\.data) // 提取返回的数据
    .decode(type: MyResponse.self, decoder: JSONDecoder()) // 解码数据为自定义类型
    .receive(on: DispatchQueue.main) // 切换到主线程处理结果
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Request completed successfully")
        case .failure(let error):
            print("Request failed with error: \(error)")
        }
    }, receiveValue: { response in
        print("Received response: \(response)")
    })

dataTaskPublisher 发送一个新的事件值时,我们将其中的 Data 通过 map 的方式提取出来,并交给 decode 这个 Operator 进行处理。decode 要求上游 PublisherOutput 类型是 Data,它会使用参数中接受的 decoder (本例中是 MyResponse) 来对上游数据进行解析,生成对应类型的实例,并作为新的 Publisher 事件发布出去。然后切换到主线程处理结果,包括刷新UI等等。

把上面的代码优化一下,具体化一下,实现一个真实的网络请求示例:

swift 复制代码
import SwiftUI
import Combine
import Foundation

struct Photo: Identifiable, Decodable {
  let id: Int
  let albumId: Int
  let title: String
  let url: String
  let thumbnailUrl: String
}

class URLSessionViewModel: ObservableObject {
  private var cancellable = Set<AnyCancellable>()
  @Published var photos: [Photo] = []
  @Published var isFetching: Bool = false

  func fetchPhotoData() {
    guard let url = URL(string: "https://jsonplaceholder.typicode.com/photos") else {
      return
    }
    isFetching = true
    let request = URLRequest(url: url)
    URLSession.shared.dataTaskPublisher(for: request)
      .map(\.data)
      .decode(type: [Photo].self, decoder: JSONDecoder())
      .receive(on: DispatchQueue.main)
      .sink { completion in
        switch completion {
          case .finished:
            print("Request completed successfully")
          case .failure(let error):
            print("Request failed with error: \(error)")
          }
      } receiveValue: { photos in
        print("Received response: \(photos)")
        self.isFetching = false
        self.photos = photos
      }
      .store(in: &cancellable)
  }
}

struct URLSessionDemo: View {
  @StateObject private var viewModel = URLSessionViewModel()

  var body: some View {
    VStack {
      if viewModel.photos.isEmpty {
        if viewModel.isFetching {
          ProgressView()
        } else {
          Button("Fetch photos data") {
            viewModel.fetchPhotoData()
          }
          .buttonStyle(BorderedProminentButtonStyle())
        }
      } else {
        List(viewModel.photos) { photo in
          PhotoView(photo: photo)
        }
        .listStyle(PlainListStyle())
      }
    }
  }
}

struct PhotoView: View {
  let photo: Photo

  var body: some View {
    HStack(spacing: 16) {
      AsyncImage(url: URL(string: photo.thumbnailUrl)) { image in
        image
          .resizable()
          .aspectRatio(contentMode: .fill)
      } placeholder: {
        Rectangle()
          .fill(Color.gray.opacity(0.3))
      }
      .frame(width: 80, height: 80)

      VStack {
        Text(String(photo.id))
          .font(.title)
          .frame(maxWidth: .infinity, alignment: .leading)

        Text(photo.title)
          .font(.headline)
          .frame(maxWidth: .infinity, alignment: .leading)
          .multilineTextAlignment(.leading)
          .lineLimit(2)
      }
    }
  }
}

上面代码中定义了一个Photo类型的数据,代码中采用了URLSession Publisher的方式请求数据,并在SwiftUI上显示,效果如下:

Timer Publisher

Timer 类型也提供了一个方法,来创建一个按照一定间隔发送事件的 Publisher。之前有一篇文章已经详细介绍过了,详见:SwiftUI中结合使用Timer和onReceive

写在最后

本文主要介绍了CombineNotificationURLSession Publisher的使用,尤其是配合SwiftUI界面的使用。不管是Notification还是URLSession都大大简化了代码,将代码流程集中化,实现了链式处理方式。

最后,希望能够帮助到有需要的朋友,如果觉得有帮助,还望点个赞,添加个关注,笔者也会不断地努力,写出更多更好用的文章。

相关推荐
开心就好20251 天前
本地执行 IPA 混淆 无需上传致云端且不修改工程的方案
后端·ios
报错小能手1 天前
ios开发方向——对于实习开发的app(Robopocket)讲解
开发语言·学习·ios·swift
wechatbot8881 天前
【企业通信】基于IPAD协议的企业微信群聊管理API:群操作功能接口设计与实现
java·ios·微信·企业微信·ipad
胖虎11 天前
我用一个 UITableView,干掉了 80% 复杂页面
ios·架构·cocoa·uitableview·ui布局
T1an-11 天前
最右IOS开发A卷笔试题3.31
c++·ios
wzl202612131 天前
《从协议层对抗折叠:iPad协议脚本在企微批量群发中的集成与优化》
ios·企业微信·ipad
茶底世界之下1 天前
Harbeth:高性能Metal图像处理库,让你的图片处理速度飞起来!
前端·github·swift
season_zhu1 天前
聊聊我最近都干了些什么,AI 时代的手动撸码人
flutter·ios·ai编程
FreeBuf_1 天前
俄罗斯关联APT组织TA446利用DarkSword漏洞工具包针对iPhone用户发起钓鱼攻击
ios·iphone
Digitally1 天前
三种将文件从iPhone传输到 Windows 11的方法
ios·iphone