Как я могу кодировать/декодировать словарь Swift с объектами Obj-C в качестве ключей для JSON и обратно?

У меня есть Dictionary<ObjcType, SwiftType>, который мне нужно сериализовать и десериализовать из JSON. SwiftType соответствует Codable. ObjcType — это класс, написанный на Objective-C (используется одной из библиотек, от которых я сильно завишу, поэтому не могу изменить его на Swift). Все свойства ObjcType являются либо NSString, либо BOOL.

Я провел поиск и добавил эти функции в ObjcType, чтобы придать ему некоторое подобие сериализации:

-(NSString*)GetJSON{
    NSError *writeError = nil;

    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:self options:NSJSONWritingPrettyPrinted error:&writeError];

    NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];

    return jsonString;
}

- (instancetype)initWithObject:(NSString *)jsonString {
    self = [super init];
    if (self != nil)
    {
        NSData* jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
        
        NSError *error = nil;
        NSDictionary  *object = [NSJSONSerialization
                                 JSONObjectWithData:jsonData
                                 options:0
                                 error:&error];
        for (NSString *dictionaryKey in object) {
            self.field1 = [[object valueForKey:dictionaryKey] objectForKey:@"field1"];
            self.field2 = [[object valueForKey:dictionaryKey] objectForKey:@"field2"];
            self.field3 = [[object valueForKey:dictionaryKey] objectForKey:@"field3"];
        }
    }
    return self;
}

Итак, теперь мой ObjcType может создать свой собственный текст и поместить его в этот JSON. Но я не могу понять, как сделать обертку, которая будет кодировать\декодировать этот словарь. Любая помощь?

P.S. еще лучше, если он сможет работать с модулем OrderedDictionary из OrderedCollections вместо обычного Dictionary.

JSON поддерживает только строки для ключей и не поддерживает порядок, поэтому использование OrderedDictionary ничем не поможет. Люди, вероятно, могли бы ответить на этот вопрос, но вы можете помочь им, предоставив им короткий/простой фрагмент, который они смогут легко скопировать/вставить и отредактировать, показывая тип словаря, который вы хотите сериализовать.

Alexander 08.08.2024 16:42

Порядок на самом деле не важен для сохранения, он важен только для других операций. Именно поэтому я и спросил про нормальный Словарь.

Afinainflowers 09.08.2024 17:12
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
57
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Этот ответ должен частично ответить на ваш вопрос.

В чистом Swift вы можете кодировать/декодировать словарь, если его тип Key соответствует CodingKeyRepresentable.

В качестве примера давайте рассмотрим следующие два типа Swift:

struct ObjcType: Codable, Hashable {
  let foo, bar: String, baz: Bool
}

struct SwiftType: Codable {
  let rom, ram: String, rem: Int
}

Вы можете соответствовать ObjcTypeCodingKeyRepresentable следующим образом:

extension ObjcType: CodingKeyRepresentable {
  struct StringCodingKey: CodingKey {
    var intValue: Int? { nil }
    let stringValue: String

    init?(intValue: Int) { return nil }
    init(stringValue value: String) { stringValue = value }
  }

  var codingKey: any CodingKey {
    let encoder = JSONEncoder()
    let data = try! encoder.encode(self)
    let string = String(data: data, encoding: .utf8)!
    return StringCodingKey(stringValue: string)
  }

  init?(codingKey: some CodingKey) {
    let decoder = JSONDecoder()
    guard
      let data = codingKey.stringValue.data(using: .utf8),
      let value = try? decoder.decode(Self.self, from: data)
    else { return nil }

    self = value
  }
}

Здесь codingKey возвращает CodingKey, stringValue которого является строковым представлением JSON экземпляра ObjcType, а init(codingKey:) инициализирует экземпляр путем декодирования строкового значения ключа кодирования.
Со временем они смогут использовать определенные вами функции GetJSON и initWithObject.

Имея это соответствие, вы можете кодировать:

let encodingValue: [ObjcType: SwiftType] = [
  .init(foo: "one", bar: "two", baz: true): .init(rom: "uno", ram: "due", rem: 3),
  .init(foo: "one", bar: "two", baz: false): .init(rom: "uno", ram: "due", rem: 0),
  .init(foo: "un", bar: "dos", baz: true): .init(rom: "one", ram: "two", rem: 6),
]

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
try print(String(data: encoder.encode(encodingValue), encoding: .utf8)!)

// {
//   "{\"foo\":\"un\",\"bar\":\"dos\",\"baz\":true}" : {
//     "rom" : "one",
//     "ram" : "two",
//     "rem" : 6
//   },
//   "{\"foo\":\"one\",\"bar\":\"two\",\"baz\":true}" : {
//     "rom" : "uno",
//     "ram" : "due",
//     "rem" : 3
//   },
//   "{\"foo\":\"one\",\"bar\":\"two\",\"baz\":false}" : {
//     "rom" : "uno",
//     "ram" : "due",
//     "rem" : 0
//   }
// }

и декодируем:

let encodedValue = #"""
  {
    "{\"foo\":\"uno\",\"bar\":\"due\",\"baz\":true}" : {
      "rom": "one",
      "ram": "two",
      "rem": 3
    },
  }
  """#

try dump(JSONDecoder().decode(
  [ObjcType: SwiftType].self, from: encodedValue.data(using: .utf8)!
))

// ▿ 1 key/value pair
//   ▿ (2 elements)
//     ▿ key: ObjcType
//       - foo: "uno"
//       - bar: "due"
//       - baz: true
//     ▿ value: SwiftType
//       - rom: "one"
//       - ram: "two"
//       - rem: 3

Проблема в том, что init, требуемый CodingKeyRepresentable, должен быть required init, поскольку ObjcType — это класс. Однако required init нельзя объявлять в расширении.

Sweeper 09.08.2024 01:40
Ответ принят как подходящий

Вы можете написать обертку следующим образом:

@propertyWrapper
struct Wrapper: Codable {
    struct StringCodingKey: CodingKey {
        var intValue: Int? { nil }
        let stringValue: String
        
        init?(intValue: Int) { return nil }
        init(stringValue value: String) { stringValue = value }
    }
    
    var wrappedValue: [ObjcType: SwiftType]
    
    func encode(to encoder: any Encoder) throws {
        var container = encoder.container(keyedBy: StringCodingKey.self)
        for (key, value) in wrappedValue {
            // assuming there is property called 'jsonString'
            let keyString = key.jsonString
            try container.encode(value, forKey: StringCodingKey(stringValue: keyString))
        }
    }
    
    init(wrappedValue: [ObjcType : SwiftType]) {
        self.wrappedValue = wrappedValue
    }
    
    init(from decoder: any Decoder) throws {
        var wrapped = [ObjcType: SwiftType]()
        let container = try decoder.container(keyedBy: StringCodingKey.self)
        for key in container.allKeys {
            // assuming such an initialiser exists
            let objcKey = ObjcType(json: key.stringValue)
            wrapped[objcKey] = try container.decode(SwiftType.self, forKey: key)
        }
        self.wrappedValue = wrapped
    }
}

Использование:

// decoding
JSONDecoder().decode(Wrapper.self, from: someJson).wrappedValue

// encoding
JSONEncoder().encode(Wrapper(wrapped: yourActualDictionary))

Вы также можете использовать его как обертку свойств в других типах Codable:

struct SomeOtherCodableType: Codable {
    @Wrapper var dict: [ObjcType: SwiftType]
}

Другие вопросы по теме