前言
在 iOS 开发中,「实时刷新」需求随处可见:
- 天气卡片 3 秒更新一次
- 座位状态由绿变红
- 股价、比分、配送进度......
过去我们习惯用 Timer.scheduledTimer 写一个"死循环",或者把 Combine 的 Timer.publish 拼成管道。
Swift 6 以后,官方把 AsyncSequence 推到 C 位,让我们用"流"的思维解决轮询。
核心概念速览
| 概念 | 一句话说明 | 本文对应示例 |
|---|---|---|
| AsyncSequence | 一个可以 for await 遍历的异步序列,天生支持结构化并发与自动取消 |
AsyncStream<WeatherCondition> |
| AsyncStream | 官方提供的 AsyncSequence 实现,适合"自己写生产端"的场景 | pollingStream(api:) |
| Task.sleep | 非阻塞的异步"睡眠",不会卡住线程 | try? await Task.sleep(for: .seconds(3)) |
| Task.isCancelled | 结构化并发中的"取消标记",用 while 判断即可优雅退出 | while !Task.isCancelled |
| MainActor.run | 把闭包安全切回主线程,避免 UI 崩溃 | await MainActor.run { ... } |
三种实现逐行拆解
基础API
swift
enum WeatherCondition: String, CaseIterable {
case clear, stormy
}
struct WeatherResponse {
let condition: WeatherCondition
}
actor MockWeatherAPI {
func fetchWeather() async -> WeatherResponse {
try? await Task.sleep(for: .seconds(1))
return WeatherResponse(condition: WeatherCondition.allCases.randomElement()!)
}
}
定时器派:Timer.scheduledTimer
swift
// 传统写法,功能可用,但坑最多
final class TimerViewModel: ObservableObject {
@Published var weather: WeatherCondition = .clear
private var timer: Timer?
init(api: MockWeatherAPI) {
// 每 3 秒在主线程回调一次
timer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [weak self] _ in
// 必须在异步上下文调用 async 方法,所以包一层 Task
Task {
let response = await api.fetchWeather()
// 回到主线程改 UI
await MainActor.run { self?.weather = response.condition }
}
}
}
deinit {
timer?.invalidate() // 忘了写就会内存泄漏
}
}
缺点小结
- 忘记
invalidate()直接泄漏 - 后台模式下容易"卡"计时器
- 单元测试必须真跑 3 秒,CI 极慢
Combine 派:Timer.publish + Future
swift
final class CombineViewModel: ObservableObject {
@Published var weather: WeatherCondition = .clear
private var cancellables = Set<AnyCancellable>()
init(api: MockWeatherAPI) {
// 1. 主线程每 3 秒发一个日期
Timer.publish(every: 3, on: .main, in: .common)
.autoconnect()
// 2. 把日期换成异步请求
.flatMap { _ in
Future { promise in
Task {
let response = await api.fetchWeather()
promise(.success(response.condition))
}
}
}
.receive(on: RunLoop.main) // 3. 回到主线程
.assign(to: &$weather) // 4. 直接绑到属性
}
}
缺点小结
- 异步/await 与 Combine 混写,心智负担高
Future只能完成一次,不能"持续"发值,需要flatMap不断新建- 测试仍需跑真时间或使用
TestScheduler
AsyncSequence 派:AsyncStream 一统江湖
swift
final class StreamViewModel: ObservableObject {
@Published var weather: WeatherCondition = .clear
private var task: Task<Void, Never>?
init(api: MockWeatherAPI) {
// 结构化并发:启动一个子任务
task = Task {
// 直接 for await 遍历自定义流
for await update in Self.pollingStream(api: api) {
await MainActor.run { self.weather = update }
}
}
}
deinit {
task?.cancel() // 取消即停流,无需手动 invalidate
}
// MARK: - 核心工厂方法
static func pollingStream(api: MockWeatherAPI) -> AsyncStream<WeatherCondition> {
AsyncStream { continuation in
// 真正生产端跑在子任务
Task {
// 如果外部调用者取消 Task,这里会优雅退出
while !Task.isCancelled {
let update = await api.fetchWeather()
continuation.yield(update.condition) // 向下游发一个值
try? await Task.sleep(for: .seconds(3)) // 等 3 秒再采
}
continuation.finish() // 告知"我发完了"
}
}
}
}
优点小结
✅ 取消即停:Task 取消后 while 自动结束
✅ 测试友好:用 AsyncStream.makeAsyncIterator() 可以同步拿值,无需真睡 3 秒
✅ 可组装:后续加 timeout、debounce、buffer 都只要包一层序列
视图层:SeatAvailabilityView
swift
struct SeatAvailabilityView: View {
let condition: WeatherCondition
var body: some View {
Circle()
.fill(condition == .clear ? .green : .red)
.frame(width: 100, height: 100)
.overlay(Text(condition.rawValue.capitalized))
}
}
使用:
swift
struct ContentView: View {
@StateObject private var vm = StreamViewModel(api: MockWeatherAPI())
var body: some View {
SeatAvailabilityView(condition: vm.weather)
.task { // 视图消失时自动取消内部的 Task
// 如果这里还启动额外工作,可一并取消
}
}
}
取消的 3 个姿势
| 场景 | 实现方式 | 代码片段 |
|---|---|---|
| 视图消失 | .task修饰符 |
.task { ... }自动在 disappear 时 cancel |
| 手动取消 | deinit调 task?.cancel() |
见 StreamViewModel |
| 超时取消 | AsyncThrowingStream+ withTimeout |
见下方扩展 |
单元测试:Swift Testing 示例
swift
import Testing
@testable import YourModule
struct WeatherPollingTests {
// 验证流能正常 emit 值
@Test
func streamEmitsValues() async throws {
let api = MockWeatherAPI()
let stream = StreamViewModel.pollingStream(api: api)
var iterator = stream.makeAsyncIterator()
let first = try await iterator.next()
#expect(first != nil)
}
// 验证外部取消后,循环会退出
@Test
func streamCancellation() async throws {
let api = MockWeatherAPI()
let task = Task {
for await _ in StreamViewModel.pollingStream(api: api) { }
}
task.cancel()
#expect(task.isCancelled)
}
}
测试提速技巧
- 把
Task.sleep抽象成Clock.sleep,测试注入ImmediateClock即可 0 秒跑完 - 用
AsyncStream.makeAsyncIterator()可以一条一条拿值,断言更细
扩展场景
带超时的轮询
swift
extension AsyncStream {
func withTimeout<C: Clock>(_ duration: C.Instant.Duration, clock: C) async throws -> Self {
// 用 withThrowingTaskGroup 同时跑"生产值"和"倒计时"
// 哪边先到就取消另一边
}
}
假 WebSocket 一键替换
swift
struct MockWebSocket: AsyncSequence {
typealias Element = WeatherCondition
struct AsyncIterator: AsyncIteratorProtocol {
mutating func next() async -> WeatherCondition? {
// 2 秒随机一个值,模拟帧
try? await Task.sleep(for: .seconds(2))
return WeatherCondition.allCases.randomElement()
}
}
func makeAsyncIterator() -> AsyncIterator { AsyncIterator() }
}
把 for await update in MockWebSocket() 直接塞进 ViewModel,
将来换真 WebSocket 只要改一行,UI 层零改动。
总结与选型建议
- 新代码直接上
AsyncStream- 取消简单、测试快、与 Swift Concurrency 原生一致
- 老代码如果已用 Combine
- 可继续用
Timer.publish,但建议包一层AsyncPublisher逐步迁移
- 可继续用
- 纯定时器场景
- 只要最小依赖,也可以
AsyncSequence一把梭,别再写Timer了
- 只要最小依赖,也可以