You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift Combine中订阅者Sink被触发两次是否为预期行为?

问题:订阅者的Sink被触发两次是否属于预期行为?

测试代码如下:

func testExample() throws {
    let service = MockAPIService(isSuccessful: true)
    let sut = ContentViewModel(apiService: service)
    let fetchImagesExpectation = expectation(description: "Fetching images")
    
    sut.$images
        .sink { value in
            print("## count: ", value?.count)
            // fetchImagesExpectation.fulfill()
            // XCTAssertEqual(value?.count ?? 0, 5)
        }
        .store(in: &cancellables)
    sut.fetch()
    wait(for: [fetchImagesExpectation], timeout: 1)
}

控制台输出:

## count:  nil
## count:  Optional(5)

ContentViewModel代码如下:

final class ContentViewModel: ObservableObject {
    @Published private (set) var images: [ImageModel]?
    private let apiService: NetworkingService
    private var cancellable: Set<AnyCancellable>
    
    init(apiService: NetworkingService = APIService()) {
        self.cancellable = Set<AnyCancellable>()
        self.apiService = apiService
    }
    
    func fetch() {
        apiService.fetchImages()
            .receive(on: RunLoop.main)
            .sink(receiveCompletion: { result in
                if case let .failure(error) = result {
                    Logger.logError(error)
                }
            }, receiveValue: { [weak self] response in
                self?.handleRespose(response)
            })
            .store(in: &cancellable)
    }
    
    private func handleRespose(_ response: SampleImagesResponse) {
        guard let responseImages = response.sample else {
            return
        }
        
        self.images = responseImages.compactMap { item in
            if let urlString = item.imageURL,
               let url = URL(string: urlString),
               let imageID = item.id,
               let title = item.description {
                return ImageModel(imageID: imageID, title: title, url: url)
            }
            return nil
        }
    }
}

另外,当注释掉sut.fetch()时,控制台仍会打印## count: nil。


回答

这种情况属于预期行为,原因如下:

  • 第一次Sink触发是因为@Published属性的初始值发送:images属性的初始值是nil,当你订阅sut.$images时,Combine框架会立即将当前的属性值(也就是nil)发送给订阅者,这是@Published的默认行为——订阅会先收到当前的初始值,之后再接收属性的变化值。这也是注释掉sut.fetch()后仍会打印## count: nil的原因。
  • 第二次Sink触发是因为fetch()方法执行后的属性更新:调用sut.fetch()后,API请求成功返回数据,handleResponse方法会将处理后的数组赋值给images,@Published属性检测到值发生变化,就会将新的Optional(5)值发送给订阅者,从而触发第二次Sink。

综上,两次触发完全符合Combine中@Published的工作机制,是正常且预期的行为。


内容的提问来源于stack exchange,提问作者Maysam

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 05:33:30