本文内容

本文将介绍SwiftUI中的Publisher。

什么是Publisher

Publisher对象能够发布数据。作为一个协议,它的的签名是: Publisher<Output, Failure>,其中Output是Publisher发布的数据的类型,而Failure是Publisher抛出的异常的类型。

有哪些Publisher

Publisher在SwiftUI中可谓无处不在,可以说它是SwiftUI的一个核心机制。在一个ObservableObject对象中,每个标注了@Published的属性,都会有一个对应的Publisher,其名称为:$原属性名。比如下边的一个ObservableObject

class MyViewModel: ObservableObject {
    @Published var foo: Int
}

其属性self.foo对应的Publisher,就是self.$foo

其他的一些常见Publisher包括:

  1. URLSessiondataTaskPublisher:发布从一个URL获取到的数据
  2. Timerpublish(every:):定期发布当前日期和时间,类型为Date
  3. NotificationCenterpublisher(for:):订阅系统事件,在收到一个系统事件后发布通知。

订阅Publisher

要订阅Publisher,只需要调用Publisher的公有方法:sink(receiveCompletion:receiveValue:)。该方法的两个参数均是闭包,当Publisher产生了异常或关闭了,就会触发receiveComplection,而当Publisher发布了新的数据,就会触发receiveValue

如果这个Publisher不会抛出异常(类型为Publisher<Output, Never>,也就是说Failure = Never),那么你就可以用sink(receiveValue:)重载方法,它省去了receiveCompletion函数,让我们能写出更加简练的代码。再次强调,请注意一定要确保Publisher不会抛出异常,否则使用该方法会导致编译错误。

下面的UIImageView使用URLSession.shared.dataTaskPublisher(for:)方法从某URL(可以是文件路径,也可以是网络地址)下载图片并将它展示出来。该方法返回的就是一个Publisher,我们使用.sink方法订阅这个Publisher,好在下载完图片后,让Publisher调用设置好的回调函数,将新图片展示在界面上。

class URLImageView: View {
    private var imageFetchCancleable: AnyCancelable?
    
    var body: some View {
        Image(from: UIImage(data: data))
    }

    init(from url: String) {
        downloadFrom(url: url)
    }

    func downloadDataFrom(url: String): Data {
        URLSession.shared.dataTaskPublisher(for: url)
            .sink(receiveCompletion: { result in

            }, receiveValue: { [weak self] data in
                self.data = data
            })
    }
}

其他常用方法

  1. assign(to:on)
  2. replaceError(with:)
  3. map(_ transform:)
  4. mapError(_ transform:)
  5. receive(on:):决定订阅函数在哪个任务队列执行,默认是后台队列(Background Queue)。如果你想让Publisher别的任务队列,就需要用到这个方法。receive(on:Dispatch.main)就指定在主队列中,通常是需要在订阅函数中更新UI是会这么做。

取消侦听

sink函数会返回一个AnyCancelable对象,我们可以调用其中的cancel()方法来手动取消侦听:

let cancellable = publisher.sink({ value in ... })
cancellable.cancel()

不过,我们不必总是这么做,因为AnyCancellable对象会在其销毁时,会自动帮我们调用cancel(),取消侦听Publisher(doc)。

我们可以利用这一个特点,将AnyCancellable定义为对象属性,这样只要对象还存活,那么这个对象就可以获取Publisher的最新数据:

class Program {
    let cancellable: AnyCancellable?

    func listenChanges() {
        cancellable = createPublisherFoo()
            .sink { error in
                // 处理错误
            } receiveValue: { value in
                // 接收更新
            }
    }
}

onReceive

如果AnyCancellable赋值给函数的一个局部变量,那么当离开变量的作用域后,侦听随即停止。这是一个坑:当我们调用完sink函数就结束了,随即AnyCancellable取消订阅Publisher,导致sink里边的代码永远不会被执行。如果里边代码是用来更新试视图的,那么视图将不会更新。

class Program {
    func listenChanges() {
        let cancellable = createPublisherFoo()
            .sink { error in
                // 这个函数永远不会执行!
            } receiveValue: { value in
                // 这个函数也永远不会执行!
            }
        // 出了这个函数,订阅就没了!
    }
}

View订阅Publisher

someView.onReceive { thingsPublisherPublishes in
    // 实现与thingsPublisherPublishes相关的代码
}

当回调执行后,将会更新视图。

相关资料

  1. Medium: Efficient resource handling — resource acquisition is initialization (RAII) in Swift
  2. Swift By Sundell: Managing self and cancellable references when using Combine
  3. 苹果开发文档:Publisher
  4. 苹果开发文档:receive(on:options:)
  5. 苹果开发文档:AnyCanellable
  6. 苹果开发文档:Receiving and Handling Events with Combine