如何解码 Swift 中的结构枚举

How to Decode an Enum of Structs in Swift

很抱歉问了这么长的问题。

我正在使用 Firestore 存储在线数据,当前结构如下;

{
  "activities": { 
    "mG47rRED9Ym4dkXinXrN": {
      "createdAt": 1234567890,
      "activityType": {
        "title": "Some Title"
      }
    },
    "BF3jhINa1qu9kia00BeG": {
      "createdAt": 1234567890,
      "activityType": {
        "percentage": 50,
      }
    }
  }
}

我正在使用 JSON 可解码协议来检索数据。我有一个主要结构;

struct Activity: Decodable {
    let documentID: String
    let createdAt: Int
    let activityType: ActivityType
}

此结构包含强制性数据,例如 createdAt 和 documentID(即 "mG47rRED9Ym4dkXinXrN")。根据嵌套在 "activityType" 中的数据,它应该 return 下面列出的两个结构之一;

struct NewGoal: Decodable {
    let title: String
}

struct GoalAchieved: Decodable {
    let percentage: Double
}

我正在用可解码枚举来做这个;

enum ActivityType: Decodable {
    case newGoal(NewGoal)
    case goalAchieved(GoalAchieved)
}

extension ActivityType {

    private enum CodingKeys: String, CodingKey {
        case activityType
    }

    init(from decoder: Decoder) throws {
        let values = try? decoder.container(keyedBy: CodingKeys.self)

        if let value = try? values?.decode(GoalAchieved.self, forKey: .activityType) {
            self = .goalAchieved(value)
            return
        }

        if let value = try? values?.decode(NewGoal.self, forKey: .activityType) {
            self = .newGoal(value)
            return
        }

        throw DecodingError.decoding("Cannot Decode Activity")
    }
}

当使用 Activity 结构作为我的数组时,出现解码错误。但是,当使用 ActivityType 作为我的数组时,它可以很好地解码,但不会提供对 documentID 和 createdAt 的访问权限。我无法继承 Activity 结构,因为它是非协议的。请问我该如何构建它?

弄清楚这个问题既棘手又有趣。我们有三个并发症使这变得困难:

  1. 可变编码键
  2. 我们也想保留为值的编码键
  3. 具有关联值的枚举类型

这是我的解决方案。有点长。让我们从您的 activity 结构开始:

struct Activity {

    let documentId: String
    let createdAt: Int
    let activityType: ActivityType

}

很好很简单。现在对于顶级解码容器:

struct Activities: Decodable {

    let activities: [Activity]

    init(from decoder: Decoder) throws {
        var activities: [Activity] = []

        let activitiesContainer = try decoder.container(keyedBy: CodingKeys.self)
        let container = try activitiesContainer.nestedContainer(keyedBy: VariableCodingKeys.self, forKey: .activities)
        for key in container.allKeys {
            let activityContainer = try container.nestedContainer(keyedBy: ActivityCodingKeys.self, forKey: key)
            let createdAt = try activityContainer.decode(Int.self, forKey: .createdAt)
            let activityType = try activityContainer.decode(ActivityType.self, forKey: .activityType)

            let activity = Activity(
                documentId: key.stringValue,
                createdAt: createdAt,
                activityType: activityType)

            activities.append(activity)
        }

        self.activities = activities
    }

    private enum CodingKeys: CodingKey {
        case activities
    }

    private struct VariableCodingKeys: CodingKey {

        var stringValue: String
        var intValue: Int?

        init?(stringValue: String) {
            self.stringValue = stringValue
        }

        init?(intValue: Int) {
            return nil
        }

    }

    private enum ActivityCodingKeys: CodingKey {
        case createdAt, activityType
    }

}

您会注意到几个有趣的点:

  1. ActivityCodingKeysActivity 结构中只有两个字段。这是因为 documentId 填充了嵌套容器的键,其中包含其余数据。
  2. 我们有 VariableCodingKeys,这让我们可以使用任何键/documentId

最后,我们有 ActivityType 枚举:

enum ActivityType: Decodable {

    case newGoal(String), achievedGoal(Double)

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let title = try? container.decode(String.self, forKey: .title) {
            self = .newGoal(title)
        } else if let percentage = try? container.decode(Double.self, forKey: .percentage) {
            self = .achievedGoal(percentage)
        } else {
            throw DecodingError.keyNotFound(
                CodingKeys.title,
                DecodingError.Context(
                    codingPath: decoder.codingPath,
                    debugDescription: "Expected title or percentage, but found neither."))
        }
    }

    private enum CodingKeys: CodingKey {
        case title, percentage
    }

}

写这篇文章时让我感到惊讶的一件事是,并非所有 CodingKey 都必须存在,解码器才能生成键控容器。我用它来将 titlepercentage 组合在一个枚举中。像你的解决方案一样,我 try 解码某个密钥,看看它是否有效,如果无效则继续。

我会第一个承认这个解决方案短。不过,它确实有效,而且它的工作方式有点酷。如果您有任何使它更简洁的问题或想法,请告诉我!