SwiftUI中蓝牙事件修改对象变量时视图未实时更新问题
问题根因
视图不实时刷新由4个核心问题导致,和蓝牙回调的异步特性相关但并非无解:
- 所有CoreBluetooth的代理方法默认运行在系统创建的后台串行队列,直接在回调中修改UI绑定的状态属性不会触发主线程UI刷新,必须显式切回主队列操作
BTPeripheral类中需要驱动UI更新的状态属性(比如data、RSSI)没有加@Published修饰,即使类遵守了ObservableObject协议,属性变更时也不会对外发送更新通知@Published修饰的数组仅在数组本身的元素增减、元素引用替换时触发更新,数组内部元素的属性变更不会被数组的发布者感知,原有视图没有订阅单个BTPeripheral实例的更新事件,自然收不到变更通知- 原有
PeripheralDetailRow中每次渲染都遍历数组查找index的写法性能很差,还存在数组越界崩溃的风险
修复步骤
1. 修正BTPeripheral类定义
给需要驱动UI更新的属性加上@Published修饰,所有属性修改操作强制切回主队列:
class BTPeripheral: NSObject, ObservableObject, Identifiable, CBPeripheralDelegate { weak var manager: BTDataManager? var peripheral: CBPeripheral? var id = UUID() // 需要更新UI的属性必须加@Published修饰 @Published var RSSI: Int? @Published var data = "Test" // 示例:蓝牙特征值更新回调的正确写法 func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { guard error == nil, let newData = characteristic.value else { return } let parsedStr = String(data: newData, encoding: .utf8) ?? "" // 所有UI相关状态修改必须切主队列 DispatchQueue.main.async { self.data = parsedStr } } }
2. 修正BTDataManager的回调线程配置
所有CBCentralManager代理回调中涉及修改@Published属性的操作必须在主队列执行,推荐初始化中央管理器时直接指定主队列,从根源避免漏切线程的问题:
class BTDataManager: NSObject, ObservableObject, CBCentralManagerDelegate { @Published var peripherals: [BTPeripheral] = [] private var centralManager: CBCentralManager! override init() { super.init() // 指定主队列调度代理回调,后续无需在每个代理方法里手动切线程 centralManager = CBCentralManager(delegate: self, queue: .main) } // 示例:扫描到外设的回调 func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { // 先做外设去重逻辑,避免重复添加 guard !peripherals.contains(where: { $0.peripheral?.identifier == peripheral.identifier }) else { return } let newPeri = BTPeripheral() newPeri.peripheral = peripheral newPeri.RSSI = RSSI.intValue newPeri.manager = self peripherals.append(newPeri) } // 其余CBCentralManagerDelegate方法保持相同线程规则即可 }
优化提示:如果需要解析体量较大的蓝牙原始数据,可以先在后台队列完成解析逻辑,仅在最终给
@Published属性赋值时切回主队列,避免阻塞UI。
3. 修正视图层的订阅逻辑
移除冗余的数组索引查找逻辑,直接在列表行中订阅单个BTPeripheral实例的更新事件:
struct PeripheralList: View { @EnvironmentObject var modelData: BTDataManager var body: some View { List { ForEach(modelData.peripherals) { peripheral in // 把单个外设实例作为被观察对象传入子视图 PeripheralDetailRow(peripheral: peripheral) } } } } struct PeripheralDetailRow: View { // 直接订阅单个外设实例的更新,无需从环境对象中二次查找 @ObservedObject var peripheral: BTPeripheral var body: some View { VStack(alignment: .leading) { Text(peripheral.id.uuidString.suffix(5)) Text(peripheral.data) .font(.caption) } } }
额外注意事项
BTPeripheral中对BTDataManager的引用要保持weak修饰,避免循环引用导致内存泄漏- 如果需要更新已存在外设的属性(比如实时RSSI、连接状态),直接修改对应
BTPeripheral实例的@Published属性即可,不需要替换整个数组元素 - 蓝牙回调中如果有需要更新UI的错误提示,同样必须切回主队列触发弹窗、Toast等组件
内容的提问来源于stack exchange,提问作者Jonathan
相关产品推荐
相关产品推荐

