Как декодировать перечисление структур в Swift - PullRequest
1 голос
/ 26 апреля 2020

Извиняюсь за длинный вопрос.

Я использую Firestore для хранения онлайн-данных и текущей структуры, как показано ниже;

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

Я использую JSON Decodable Protocol для извлечения данных , У меня есть основная структура:

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

эта структура содержит обязательные данные, такие как созданный & 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 и createAt. Я не могу наследовать структуру Activity, так как она не является протоколом. Как бы я go о структурировании этого, пожалуйста?

1 Ответ

2 голосов
/ 27 апреля 2020

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

  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 STRUCT. Это потому, что documentId заполнен ключом вложенного контейнера, который содержит остальные данные.
  2. У нас есть VariableCodingKeys, что позволяет нам работать с любым ключом / documentId.

Наконец, у нас есть ActivityType enum:

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 декодирую определенный ключ, проверяю, работает ли он, и продолжаю, если нет.

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

...