Как декодировать перечисление структур в 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», он должен возвращать одну из двух структур, перечисленных ниже;

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 в качестве моего массива я получаю DecodingError. Однако при использовании ActivityType в качестве моего массива он будет нормально декодироваться, но не даст доступа к documentID и createdAt. Я не могу наследовать структуру Activity, поскольку она не является протоколом. Как мне структурировать это, пожалуйста?


person David Lintin    schedule 26.04.2020    source источник
comment
Можете ли вы поделиться точным сообщением об ошибке декодирования?   -  person Byron Coetsee    schedule 27.04.2020
comment
Это выброшенная ошибка в моем коде «Cannot Decode Activity»   -  person David Lintin    schedule 27.04.2020
comment
Говорит почему? Может ключ пропал? Вы пытались добавить CodingKeys в структуру Activity?   -  person Byron Coetsee    schedule 27.04.2020


Ответы (1)


Это было довольно сложно и интересно выяснить. У нас есть три сложности, которые делают это сложным:

  1. Переменные ключи кодирования
  2. Ключи кодирования, которые мы также хотим сохранить в качестве значений
  3. Перечисленные типы со связанными значениями

Вот мое решение. Это немного долго. Начнем со структуры вашей деятельности:

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. ActivityCodingKeys имеет только два поля в структуре Activity. Это потому, что 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
    }

}

Одна вещь, которая меня удивила, когда я писал это, это то, что не все CodingKeys должны присутствовать, чтобы декодер мог сгенерировать контейнер с ключом. Я использовал это, чтобы объединить title и percentage в одно перечисление. Как и в вашем решении, я try расшифровываю определенный ключ, смотрю, работает ли он, и иду дальше, если нет.

Я буду первым, кто признает, что это решение не короткое. Тем не менее, это работает, и это круто, как все это работает. Если у вас есть какие-либо вопросы или идеи, как сделать его более кратким, дайте мне знать!

person Daniel    schedule 26.04.2020
comment
Это работает отлично. Я надеюсь, что смогу расширить это с помощью дополнительной информации в структурах :) - person David Lintin; 27.04.2020