SwiftUI List报错:CustomDataModel需遵循Identifiable协议
问题背景
开发Watch应用事件列表展示功能:视图打开时向iPhone端发送获取事件列表请求,目前跨设备通信链路正常,Watch端可成功接收返回的事件数据,但使用SwiftUI的List组件渲染自定义事件类型时出现类型报错。
预期交互:事件数据加载完成后正常展示列表,无事件数据时展示空状态提示文本。
注意:Watch Connectivity框架不支持直接传递自定义数据类型,现有实现中
CustomDataModel已遵循Codable协议:传输前将实例编码为Data类型,Watch端接收到Data后解码还原为模型实例。
报错信息
报错位置:CustomView中List(viewModel.eventsList)代码行
报错内容:Initializer 'init(_:rowContent:)' requires that 'CustomDataModel' conform to 'Identifiable'
已确认CustomViewModel中的eventsList可正常接收解码后的事件数据,但无法完成视图渲染。
现有实现代码
CustomDataModel
import Foundation struct CustomDataModel: Codable { var startEndDate: String? var startEndTime: String? var locationDetails: String? var actions: Dictionary<String, String> = [:] var title: String? // Has more properties func encodeIt() -> Data { let data = try! PropertyListEncoder.init().encode(self) return data } static func decodeIt(_ data:Data) -> CustomDataModel { let customDataModel = try! PropertyListDecoder.init().decode(CustomDataModel.self, from: data) return customDataModel } }
CustomViewModel
import Foundation import Combine class CustomViewModel: ObservableObject, SessionCommands { @Published var eventsList:[CustomDataModel] = [] init() { NotificationCenter.default.addObserver(forName: Notification.Name("eventsListReady"), object: nil, queue: nil, using: self.processEventsList(_:)) } @objc func processEventsList(_ notification: Notification) { if let response = (notification.object as! CommandStatus).response as? [Data] { response.forEach { event in eventsList.append(CustomDataModel.decodeIt(event)) } } } func getEventsList() { self.eventsList = [] sendMessage(["functionality": Functionality.events.rawValue, "notificationName": Notification.Name("requestEventsList")]) } }
CustomView
import SwiftUI struct CustomView: View { @ObservedObject var viewModel: CustomViewModel var body: some View { VStack { if !viewModel.eventsList.isEmpty { List(viewModel.eventsList) { item in let _ = print("##### !viewModel.eventsList.isEmpty") NavigationLink(destination: EventsDetailsView(eventItem: item)) { EventRowView(eventItem: item) }.navigationViewStyle(StackNavigationViewStyle()) } } else { let _ = print("##### viewModel.eventsList.isEmpty") VStack { Text("No events to display") }.frame(maxWidth: .infinity, maxHeight: .infinity) } }.navigationBarTitle("Back").onAppear() { self.viewModel.getEventsList() } } }
问题原因
你使用的List(_:rowContent:)构造器,要求传入的集合元素必须遵循Identifiable协议——SwiftUI通过该协议要求的唯一id属性区分不同列表项,以此完成视图复用、更新和状态匹配。当前CustomDataModel仅遵循了Codable协议,未实现Identifiable相关要求,因此触发类型报错。
修复方案
优先选择方案1,稳定性更高:
方案1:让CustomDataModel遵循Identifiable协议
如果事件本身有业务侧的唯一标识(比如eventId、日程ID这类从iPhone端传输过来的固定字段),直接将该字段作为id,不需要修改传输的数据结构,也不影响Codable编解码,示例:
struct CustomDataModel: Codable, Identifiable { // 假设事件唯一标识字段为eventId,替换为你实际的业务字段即可 var id: String { eventId } var eventId: String // 其余原有属性保持不变 }
如果暂时没有业务唯一标识,可以新增自动生成的UUID作为id,注意这种方式每次解码生成的id都不同,列表刷新时可能出现视图跳动:
struct CustomDataModel: Codable, Identifiable { let id = UUID() // 其余原有属性保持不变 }
方案2:手动指定List的id路径(无需修改模型)
如果不想调整模型结构,可以换用List的另一个构造器,手动指定元素上可作为唯一标识的字段路径,示例:
// 这里指定用title作为唯一标识,注意必须保证该字段在所有列表项中唯一,否则会出现渲染错乱 List(viewModel.eventsList, id: \.title) { item in // 原有行内容保持不变 }
额外隐藏问题修复
现有ViewModel的通知回调存在两个隐患:
- 通知回调默认不保证在主线程执行,直接在回调中修改
@Published属性触发UI更新,可能出现后台线程更新UI的崩溃 - 循环append数据会多次触发视图刷新,数据量大时存在不必要的性能损耗
将processEventsList方法修改为如下实现即可:
@objc func processEventsList(_ notification: Notification) { if let response = (notification.object as! CommandStatus).response as? [Data] { // 一次性解码所有数据 let decodedEvents = response.map { CustomDataModel.decodeIt($0) } // 切到主线程一次性赋值,触发单次视图刷新 DispatchQueue.main.async { self.eventsList = decodedEvents } } }
内容的提问来源于stack exchange,提问作者tech_human

