如何用XMLCoder基于标签正确解码XML中的Workout分段
基于XML标签正确解码不同元素的Swift解决方案
待解码的XML文件
<workout_file> <author>afaag</author> <name>afa</name> <description>ag326 wges</description> <sportType>bike</sportType> <tags> </tags> <workout> <Warmup Duration="600" PowerLow="0.25" PowerHigh="0.75"/> <Cooldown Duration="600" PowerLow="0.75" PowerHigh="0.25"/> </workout> </workout_file>
当前的Swift解码代码(基于XMLCoder)
struct Workout: Codable { let workout: [WorkoutSegment] enum CodingKeys: String, CodingKey { case workout = "workout" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) var workoutSegments = [WorkoutSegment]() var workoutContainer = try container.nestedUnkeyedContainer(forKey: .workout) while !workoutContainer.isAtEnd { if let warmUp = try? workoutContainer.decode(WarmUp.self) { print("decoding WarmUp") workoutSegments.append(WorkoutSegment(warmUp: warmUp)) } else if let coolDown = try? workoutContainer.decode(CoolDown.self) { print("decoding CoolDown") workoutSegments.append(WorkoutSegment(coolDown: coolDown)) } else { // If the next element cannot be decoded, exit the loop print("=== oops ===") break } } self.workout = workoutSegments } } struct WorkoutSegment: Codable { let warmUp: WarmUp? let coolDown: CoolDown? enum CodingKeys: String, CodingKey { case warmUp = "Warmup" case coolDown = "Cooldown" } init(warmUp: WarmUp? = nil, coolDown: CoolDown? = nil) { self.warmUp = warmUp self.coolDown = coolDown } } // <Warmup Duration="300" PowerLow="0.42449999" PowerHigh="0.70449996" /> struct WarmUp: Codable { let duration: Double let powerLow: Double let powerHigh: Double let textevent: [TextEvent]? enum CodingKeys: String, CodingKey { case duration = "Duration" case powerLow = "PowerLow" case powerHigh = "PowerHigh" case textevent = "textevent" } } struct CoolDown: Codable { let duration: Double let powerLow: Double let powerHigh: Double let textevent: [TextEvent]? enum CodingKeys: String, CodingKey { case duration = "Duration" case powerLow = "PowerLow" case powerHigh = "PowerHigh" case textevent = "textevent" } }
问题现象
原本期望代码能通过<Warmup>、<Cooldown>标签区分不同元素,但实际运行时:
- 原代码只会解码第一个出现的元素类型(比如XML中Warmup在前时,只打印
decoding WarmUp,Cooldown被忽略) - 调换XML中两个元素的顺序后,又会把两个元素都解码成第一个出现的类型(比如Cooldown在前时,打印两次
decoding CoolDown) - 修改为同时尝试解码两种类型后,会重复解码同一个元素(打印两次
decoding WarmUp)
核心原因是WarmUp和CoolDown的结构完全一致(属性名、类型都相同),XMLCoder在尝试用其中一个类型解码另一个标签的元素时,会因为属性匹配成功而错误解码,无法区分标签名。
解决方案
要基于标签名正确区分元素,需要先获取当前元素的标签名称,再选择对应的类型解码。具体修改如下:
1. 添加动态CodingKey
用于读取XML元素的标签名:
private struct DynamicCodingKey: CodingKey { var stringValue: String var intValue: Int? init?(stringValue: String) { self.stringValue = stringValue self.intValue = nil } init?(intValue: Int) { self.stringValue = "\(intValue)" self.intValue = intValue } }
2. 优化WorkoutSegment为枚举(可选,更简洁)
用枚举替代带可选值的结构体,更清晰地表示不同类型的训练段:
enum WorkoutSegment: Codable { case warmUp(WarmUp) case coolDown(CoolDown) enum CodingKeys: String, CodingKey { case warmUp = "Warmup" case coolDown = "Cooldown" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) if let warmUp = try container.decodeIfPresent(WarmUp.self, forKey: .warmUp) { self = .warmUp(warmUp) } else if let coolDown = try container.decodeIfPresent(CoolDown.self, forKey: .coolDown) { self = .coolDown(coolDown) } else { throw DecodingError.dataCorrupted( DecodingError.Context( codingPath: decoder.codingPath, debugDescription: "无法识别的WorkoutSegment类型" ) ) } } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { case .warmUp(let warmUp): try container.encode(warmUp, forKey: .warmUp) case .coolDown(let coolDown): try container.encode(coolDown, forKey: .coolDown) } } }
3. 修改Workout的解码逻辑
在循环中先获取元素的标签名,再匹配对应的类型解码:
struct Workout: Codable { let workout: [WorkoutSegment] enum CodingKeys: String, CodingKey { case workout = "workout" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) var workoutSegments = [WorkoutSegment]() var workoutContainer = try container.nestedUnkeyedContainer(forKey: .workout) while !workoutContainer.isAtEnd { // 获取当前元素的键容器,用于读取标签名 let elementContainer = try workoutContainer.nestedContainer(keyedBy: DynamicCodingKey.self) guard let elementKey = elementContainer.allKeys.first else { continue } // 根据标签名选择对应的解码类型 switch elementKey.stringValue { case "Warmup": let warmUp = try elementContainer.decode(WarmUp.self, forKey: elementKey) workoutSegments.append(.warmUp(warmUp)) print("decoding WarmUp") case "Cooldown": let coolDown = try elementContainer.decode(CoolDown.self, forKey: elementKey) workoutSegments.append(.coolDown(coolDown)) print("decoding CoolDown") default: print("未知元素: \(elementKey.stringValue)") continue } } self.workout = workoutSegments } }
4. 补充缺失的TextEvent结构体
原代码中未定义TextEvent,需补充(根据实际XML中的字段调整):
struct TextEvent: Codable { // 示例:根据实际XML的textevent属性添加字段 let text: String? enum CodingKeys: String, CodingKey { case text = "Text" } }
效果验证
修改后运行代码,会正确打印:
decoding WarmUp decoding CoolDown
并且workout数组中会包含正确的WarmUp和CoolDown类型的元素。
内容的提问来源于stack exchange,提问作者app4g
相关产品推荐
相关产品推荐

