SwiftUI — onReceive

让我们从 技术原理核心功能实际案例体系结构 四个维度,深入探讨 SwiftUI 中 onReceive 的完整使用体系。


一、技术原理:Combine 与 SwiftUI 的桥梁 #

1. Publisher 与 Subscriber 模型 #

  • Publisher:数据源的抽象,可以发送值(Output)或错误(Failure)。
  • Subscriber:接收并处理来自 Publisher 的数据。
  • Operator:在 Publisher 和 Subscriber 之间转换或处理数据流。

onReceive 是 SwiftUI 提供的一个 Subscriber 封装,专门用于在视图中订阅 Publisher,并将接收到的值转换为视图的副作用(如更新状态)。

// 原理简化版实现
func onReceive<P>(
    _ publisher: P,
    perform action: @escaping (P.Output) -> Void
) -> some View where P : Publisher, P.Failure == Never

二、核心功能:支持的 Publisher 类型 #

1. 基础类型 #

(1) Just: 立即发送一个值并结束 #

.onReceive(Just("Hello")) { value in
    print(value) // 立即输出 "Hello"
}

(2) Future: 异步操作完成后发送单个值 #

let futurePublisher = Future<String, Never> { promise in
    DispatchQueue.global().async {
        promise(.success("Data loaded"))
    }
}

.onReceive(futurePublisher) { result in
    print(result) // 异步完成后触发
}

(3) PassthroughSubject: 手动控制的信号发射器 #

let subject = PassthroughSubject<String, Never>()

.onReceive(subject) { value in
    print("Received: \(value)")
}

// 外部触发
subject.send("Message")

2. 系统集成 #

(1) NotificationCenter #

// 监听键盘高度变化
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { notification in
    guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
    print("Keyboard height: \(keyboardFrame.height)")
}

(2) Timer #

// 精确控制定时器生命周期
let timerPublisher = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

.onReceive(timerPublisher) { _ in
    self.counter += 1
}
.onDisappear {
    timerPublisher.upstream.connect().cancel() // 手动取消
}

(3) URLSession 网络请求 #

struct NetworkDemo: View {
    @State private var data: String = ""
    
    var body: some View {
        Text(data)
            .onAppear {
                let url = URL(string: "https://api.example.com/data")!
                URLSession.shared.dataTaskPublisher(for: url)
                    .map(\.data)
                    .decode(type: String.self, decoder: JSONDecoder())
                    .replaceError(with: "Error")
                    .receive(on: DispatchQueue.main)
                    .sink { self.data = $0 }
                    .store(in: &cancellables) // 需要持有 Cancellable
            }
    }
}

3. 高级组合 #

(1) 合并多个 Publisher #

let publisher1 = PassthroughSubject<String, Never>()
let publisher2 = PassthroughSubject<Int, Never>()

.onReceive(publisher1.combineLatest(publisher2)) { (text, number) in
    print("Combined: \(text), \(number)")
}

(2) 使用 debounce 防抖 #

// 用于搜索框输入防抖
let searchTextPublisher = PassthroughSubject<String, Never>()

.onReceive(
    searchTextPublisher
        .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
) { query in
    performSearch(query: query)
}

三、体系结构:与 MVVM 的深度整合 #

1. ViewModel 中的 Publisher #

class UserViewModel: ObservableObject {
    // 公开只读 Publisher
    private let loginStatusSubject = PassthroughSubject<Bool, Never>()
    var loginStatusPublisher: AnyPublisher<Bool, Never> {
        loginStatusSubject.eraseToAnyPublisher()
    }
    
    func login() {
        // 模拟网络请求
        DispatchQueue.global().async {
            self.loginStatusSubject.send(true)
        }
    }
}

2. View 层的响应 #

struct LoginView: View {
    @StateObject var viewModel = UserViewModel()
    @State private var isLoggedIn = false
    
    var body: some View {
        VStack {
            Button("Login") { viewModel.login() }
            if isLoggedIn {
                Text("Welcome!")
            }
        }
        .onReceive(viewModel.loginStatusPublisher) { success in
            isLoggedIn = success
        }
    }
}

四、实战案例大全 #

案例 1:实时表单验证 #

struct SignupForm: View {
    @State private var username = ""
    @State private var isValid = false
    
    var body: some View {
        VStack {
            TextField("Username", text: $username)
            Text(isValid ? "Valid" : "Invalid")
        }
        .onReceive(
            $username
                .debounce(for: 0.5, scheduler: RunLoop.main)
                .map { $0.count >= 5 }
        ) { isValid = $0 }
    }
}

案例 2:跨视图通信 #

// 全局事件总线
class EventBus {
    static let shared = EventBus()
    let refreshEvent = PassthroughSubject<Void, Never>()
}

struct ParentView: View {
    var body: some View {
        VStack {
            ChildView()
            OtherView()
        }
    }
}

struct ChildView: View {
    var body: some View {
        Button("Refresh All") {
            EventBus.shared.refreshEvent.send()
        }
    }
}

struct OtherView: View {
    @State private var counter = 0
    
    var body: some View {
        Text("Count: \(counter)")
            .onReceive(EventBus.shared.refreshEvent) {
                counter += 1
            }
    }
}

五、关键注意事项 #

1. 生命周期管理 #

  • 自动取消:当视图被销毁时,onReceive 会自动取消订阅
  • 手动控制:对于长期存在的 Publisher(如全局通知),需使用 onDisappear 主动取消
.onReceive(globalPublisher) { ... }
.onDisappear {
    globalSubscription?.cancel()
}

2. 线程安全 #

  • 强制主线程更新
.onReceive(publisher.receive(on: DispatchQueue.main)) { ... }

3. 错误处理 #

  • 必须处理错误
.onReceive(
    networkPublisher
        .catch { error in
            Just(NetworkError.message(error.localizedDescription))
        }
) { ... }

六、性能优化技巧 #

1. 避免重复订阅 #

// 错误:每次渲染都会新建 Publisher
.onReceive(Timer.publish(every: 1).autoconnect()) { ... }

// 正确:在 ViewModel 或全局管理
class TimerManager {
    static let shared = Timer.publish(every: 1).autoconnect()
}

.onReceive(TimerManager.shared) { ... }

2. 使用 objectWillChange 自动刷新 #

class DataModel: ObservableObject {
    @Published var items: [String] = []
}

struct ListView: View {
    @ObservedObject var model: DataModel
    
    var body: some View {
        List(model.items, id: \.self) { ... }
            .onReceive(model.objectWillChange) { 
                // 当 items 变化时自动触发
            }
    }
}

通过这个完整体系,你可以将 onReceive 灵活运用于:

  • 数据驱动 UI:实时响应后端推送、数据库变更
  • 系统集成:深度结合 CoreLocation、CoreBluetooth 等框架
  • 状态协调:复杂视图间的联动操作
  • 性能敏感场景:高频数据流的节流控制

建议结合 Combine 的 操作符文档 进一步探索高级用法!

本文共 1220 字,创建于 Feb 23, 2025
相关标签: Xcode, SwiftUI