Swift Codable 重用在不同模型结构/类之间共享的键子集

2023-12-11

我想知道是否有一种可行的方法可以通过使用自定义解码器初始化程序和/或多个容器和编码键来跨多个模型共享公共属性。这是我想要映射到相应的可编码模型的 JSON 对象:

JSON 对象

我想要映射到的属性Codable is 'sprites'.

正如你所看到的,很多属性都像back_default、back_female 等..可以在其他模型之间共享,例如精灵->其他->dream_world,精灵->其他->官方艺术作品 and in 精灵->版本->第一代->红蓝

我的目标是能够使用一个共享模型并自定义他的解码器(也许通过使用多个容器),因此我可以说,例如:如果当前要解码的密钥是“官方艺术作品“仅解码此模型对象公开的所有 CodingKey 的子集(也许我需要使用另一个子容器?),因此我将只有 1 个密钥的解码字段官方艺术作品, 2 个密钥的解码字段梦想世界等等。

[EDIT]

目前我想出的解决方案是other关键是这个,但有些属性是共享的,我认为有一个更好的解决方案(如果我理解我正确地做到了这一点,我可以将其应用到versions模型的关键):

struct PokemonSpritesModel: Codable {
   
   
   //MARK: Properties
   
   var back_default: String?
   var back_female: String?
   var back_shiny: String?
   var back_shiny_female: String?
   var front_default: String?
   var front_female: String?
   var front_shiny: String?
   var front_shiny_female: String?
   
   var other: PokemonSpritesModelOther?
   var versions: PokemonSpritesModelVersions?
   
   
}


struct PokemonSpritesModelOther: Codable {
   
   //MARK: Properties
   
   var dreamWorld: PokemonSpritesModelOtherDreamWorld?
   var officialArtwork: PokemonSpritesModelOtherOfficialArtwork?
   
   
   private enum CodingKeys: String, CodingKey {
      case officialArtwork = "official-artwork", dreamWorld = "dream_world"
   }

   
   
   struct PokemonSpritesModelOtherDreamWorld: Codable {
      
      //MARK: Properties
      
      var front_default: String?
      var front_female: String?
      
   }
   
   
   struct PokemonSpritesModelOtherOfficialArtwork: Codable {
      
      //MARK: Properties
      
      var front_default: String?
      
   }
   
}

我可以使用之前的模型并将其扩展为所有其他 json 对象来表示,但这将需要编写大量重复代码,我认为有比我目前正在做的更好的方法。我读过很多 SO 和 Medium 博客文章,但它们对我没有帮助。

我不确定我所要求的是否可以做Swift.

[EDIT 2]

我认为最好从头开始,让其他人了解我正在努力实现的目标。目前我对该 JSON 结构的可编码是这样的:

struct PokemonDetailsModel: Decodable {
   
   
   // MARK: Properties
   
   var name: String?
   var base_experience: Int?
   var order: Int?
   var sprites: PokemonSpritesModel?
   var stats: [PokemonStatsModel]?
   var types: [PokemonTypesModel]?
   
}


struct PokemonSpritesModel: Decodable {
   
   
   //MARK: Properties

   // ======== SET Of keys that are repeated

   var back_default: String?
   var back_female: String?
   var back_shiny: String?
   var back_shiny_female: String?
   var front_default: String?
   var front_female: String?
   var front_shiny: String?
   var front_shiny_female: String?

   // ======== SET Of keys that are repeated
   
   var other: PokemonSpritesModelOther?
   var versions: PokemonSpritesModelVersions?
   
   
}

struct PokemonSpritesModelOther: Decodable {
   
   //MARK: Properties
   var dreamWorld: PokemonSpritesModelOtherDreamWorld?
   var officialArtwork: PokemonSpritesModelOtherOfficialArtwork?
   
   
   
   private enum CodingKeys: String, CodingKey {
      case officialArtwork = "official-artwork", dreamWorld = "dream_world"
   }
   
   struct PokemonSpritesModelOtherDreamWorld: Decodable {
      
      //MARK: Properties
      var front_default: String?
      var front_female: String?
      
   }
   
   
   struct PokemonSpritesModelOtherOfficialArtwork: Decodable {
      
      //MARK: Properties
      var front_default: String?
      
   }
   
}

struct PokemonSpritesModelVersions: Decodable {
   
   
   //MARK: Properties
   var generation_i: PokemonSpritesModelVersionsGenerationsI?
   var generation_ii: PokemonSpritesModelVersionsGenerationsII?
   //   var generation_iii: PokemonSpritesModelVersionsGenerationsIII?
   //   var generation_iv: PokemonSpritesModelVersionsGenerationsIV?
   //   var generation_v: PokemonSpritesModelVersionsGenerationsV?
   //   var generation_vi: PokemonSpritesModelVersionsGenerationsVI?
   //   var generation_vii: PokemonSpritesModelVersionsGenerationsVII?
   //   var generation_viii: PokemonSpritesModelVersionsGenerationsVIII?
   
   
   
   private enum CodingKeys: String, CodingKey {
      case generation_i = "generation-i",
                  generation_ii = "generation-ii",
      //           generation_iii = "generation-iii",
      //           generation_iv = "generation-iv",
      //           generation_v = "generation-v",
      //           generation_vi = "generation-vi",
      //           generation_vii = "generation-vii",
      //           generation_viii = "generation-viii"
   }
   
   
   struct PokemonSpritesModelVersionsGenerationsI: Decodable {
      
      //MARK: Properties
      
      var red_blue: PokemonSpritesModelVersionsGenerationsColors?
      var yellow: PokemonSpritesModelVersionsGenerationsColors?
      
      private enum CodingKeys: String, CodingKey {
         case red_blue = "red-blue", yellow
      }
      
   }
   
   
   struct PokemonSpritesModelVersionsGenerationsII: Decodable {
      
      //MARK: Properties
      var crystal: PokemonSpritesModelVersionsGenerationsColors?
      var gold: PokemonSpritesModelVersionsGenerationsColors?
      var silver: PokemonSpritesModelVersionsGenerationsColors?
   }



   struct  PokemonSpritesModelVersionsGenerationsColors: Decodable {
      

      //****** Notes: Some of the keys here (i marked them with an asterisk) are not part of the Set of keys market in the 'sprites' json object that are shared across different models

      var back_default: String?
      var back_shiny: String?
      var back_gray: String? // *
      var back_female: String?
      var back_shiny_female: String?
      var front_default: String?
      var front_shiny: String?
      var front_gray: String? // *
      var front_female: String?
      var front_shiny_female: String?
      
   }

我省略了其他几代结构,因为概念是相同的。

通过这种方法,一切正常,但问题是我始终使用结构“PokemonSpritesModelVersionsGenerationsColors”中的完整键集(我也应该使用“sprites”键集,但我不知道如何推断该集并使精灵结构仍然工作,另外我还应该在“PokemonSpritesModelVersionsGenerationsColors”中添加标有*的新灰色。

正如您所说,重复几种类型的键是sprites结构体(other and versions应该排除 .json 对象,但正如我之前所说,应该添加标有 * 的精灵)。

根据我所做的研究,有两种可能的解决方案可以完成我想做的事情:

  1. 创建一个单独的DTO层(代表服务器 API 返回的数据的模型),然后有另一个领域层(应用程序的型号)。每个域模型将过滤相应 DTO 模型的键集,以便域层模型将与服务器响应匹配(DTO 模型与服务器响应不匹配,因为它不会仅使用必要子集所需的键)

  2. 使用某种组合自定义解码器初始化程序 + 多个容器通过编码键子类型枚举(也许借助枚举关联值)+也许其他东西。

我想避免第一种方法,主要是因为有两个层,除了域模型中包含的字段和业务逻辑来过滤必要的键之外,它们基本上是相同的。我正在考虑继续采用第二种方法。

我想我心里有一个关于如何做到这一点的想法,但我无法将所有部分放在一起。我主要阅读了这些博客文章:

  1. https://matteomanferdini.com/codable/
  2. https://lostmoa.com/blog/CodableConformanceForSwiftEnumsWithMultipleAssociatedValuesOfDifferentTypes/
  • 其他资源
  1. 使用关联值仅解码键的子集? (https://forums.swift.org/t/codable-synthesis-for-enums-with-linked-values/41493)
  2. 多个容器用作鉴别器来仅解码密钥的子集? (https://forums.swift.org/t/automatic-codable-conformance-for-enums-with-linked-values-that-themselves-conform-to-codable/11499)
  3. 检查解码器当前正在解码什么类型的密钥并仅创建该密钥?https://stackoverflow.com/a/53270057/2685716)
  4. 多个容器? (https://stackoverflow.com/a/57788293/2685716)

我认为第 1 篇和第 2 篇博客文章是我可以从中汲取一些想法来构建这个 sprites Json Codable 结构的文章。从第 1 篇博客文章来看,这是代码中可能更有趣的部分:

extension Launch: Decodable {
    enum CodingKeys: String, CodingKey {
        case timeline
        case links
        case rocket
        case flightNumber = "flight_number"
        case missionName = "mission_name"
        case date = "launch_date_utc"
        case succeeded = "launch_success"
        case launchSite = "launch_site"
        
        enum RocketKeys: String, CodingKey {
            case rocketName = "rocket_name"
        }
        
        enum SiteKeys: String, CodingKey {
            case siteName = "site_name_long"
        }
        
        enum LinksKeys: String, CodingKey {
            case patchURL = "mission_patch"
        }
    }
}

从第 2 篇博客文章中,这可能是我正在寻找的代码,它从顶部/根键(我们的 sprites json 对象?)开始,一直向下直到我们需要解码的 Json 对象的底部。

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    let key = container.allKeys.first
    
    switch key {
    case .empty:
        self = .empty
    case .editing:
        let subview = try container.decode(
            EditSubview.self,
            forKey: .editing
        )
        self = .editing(subview: subview)
    case .exchangeHistory:
        let connection = try container.decode(
            Connection?.self,
            forKey: .exchangeHistory
        )
        self = .exchangeHistory(connection: connection)
    case .list:
        var nestedContainer = try container.nestedUnkeyedContainer(forKey: .list)
        let selectedId = try nestedContainer.decode(UUID.self)
        let expandedItems = try nestedContainer.decode([Item].self)
        self = .list(
            selectedId: selectedId,
            expandedItems: expandedItems
        )
    default:
        throw DecodingError.dataCorrupted(
            DecodingError.Context(
                codingPath: container.codingPath,
                debugDescription: "Unabled to decode enum."
            )
        )
    }
}

所以我将尝试以最简单的方式总结我想做的事情:我的想法是创建属于全局键集的键子集的多个容器(其中一些将是嵌套容器,可能是无键容器)不同模型之间共享的。比在相同的结构中或者在单独的结构中更好(也许我还需要在其中移动与其关联的枚举键的子集),我将有一个自定义解码器初始值设定项,它将仅创建在此枚举子集中定义的键键)。

[EDIT 3]

@RobNapier 是的,我想使用相同的语法。我仍然不知道什么是最简单或正确的方法。您能否提供一个完整的示例,说明您的 [String:URL] 方法,使我能够通过重用共享密钥来解码以下 json?

{
  "sprites": 
  {
    "back_default": "some text",
    "back_female": "some text",
    "back_shiny": "some text",
    "back_shiny_female": "some text",
    "front_default": "some text",
    "front_female": "some text",
    "front_shiny": "some text",
    "front_shiny_female": "some text",
    "other": {
        "dream_world": {
          "front_default": "some text",
          "front_female": "some text"
        }, 
        "official-artwork": {
          "front_female": "some text"
        }
    }
  }

}

有很多方法可以做到这一点,具体取决于您希望该对象具有哪种接口,但这里是一种方法。完成后你就可以参考了sprites.frontShiny获取默认 URLfront_shiny,您将能够使用下标获得变体,例如sprites[.official].frontDefault。我希望您需要稍微调整一下这个实现以匹配您的使用情况,但它应该可以帮助您入门。

我们正在解码的结构如下所示:

{
  ...
  "sprites": {
    "back_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/21.png",
    "back_female": null,
    "back_shiny": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/21.png",
    "back_shiny_female": null,
    "front_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/21.png",
    "front_female": null,
    "front_shiny": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/21.png",
    "front_shiny_female": null,
    "other": {
      "dream_world": {
        "front_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/21.svg",
        "front_female": null
      },
      "official-artwork": {
        "front_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/21.png"
      }
    },
    "versions": {
      "generation-i": {
        "red-blue": {
          "back_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/back/21.png",
          "back_gray": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/back/gray/21.png",
          "front_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/21.png",
          "front_gray": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/versions/generation-i/red-blue/gray/21.png"
        },
...

需要注意的重要一点是,有一个Sprite结构会重复多次,首先在根级别,然后在“other/dream_world”和“versions/ Generation-i/red-blue”等路径下的各个嵌套点(请参阅PokeAPI/精灵).

我的假设是,您不太关心“其他”和“版本/i 代”层,实际上只想讨论特定变体,例如.dreamWorld and .redBlue。 (这可以适应暴露其他层,但当前的实现有意隐藏它们。)

第一个工具来自Swift 4 可通过未知动态键进行解码,用于解码的任意字符串键:

struct AnyStringKey: CodingKey, Hashable, CustomStringConvertible {
    var stringValue: String = ""
    var intValue: Int?
    init?(stringValue: String) { self.stringValue = stringValue }
    init?(intValue: Int) {}
}

另一个关键工具是[String: URL]用于存储,变体将像 Unix 路径一样存储:other/official-artwork/....

从解码开始:

struct Sprites {
   var urls: [String: URL] = [:]
}

extension Sprites: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: AnyStringKey.self)

        // Recursively decode, placing any URLs at this layer, and prepending
        // the current key to the path for child layers. Note that this
        // ignores all errors. If the data is malformed, then the dictionary
        // will just be empty.
        for key in container.allKeys {
            if let value = try? container.decode(URL.self, forKey: key) {
                urls[key.stringValue] = value
            } else if let variant = try? container.decode(Sprites.self, forKey: key) {
                for (childKey, value) in variant.sprites {
                    urls["\(key.stringValue)/\(childKey)"] = value
                }
            }
        }
    }
}

这样,您就可以通过密钥访问数据:

// https://pokeapi.co/docs/v2
struct Pokemon: Decodable {
    let sprites: Sprites
}

let sprites = try JSONDecoder().decode(Pokemon.self, from: json).sprites
print(sprites.urls["front_default"]!)

还可以,就是用起来有点不方便。我们可以通过一些计算属性来改进它。

extension Sprites {
    var frontDefault: URL? { urls["front_default"] }
    var frontShiny: URL? { urls["front_shiny"] }
    var frontFemale: URL? { urls["front_female"] }
    var frontShinyFemale: URL? { urls["front_shiny_female"] }
    var backDefault: URL? { urls["back_default"] }
    var backShiny: URL? { urls["back_shiny"] }
    var backFemale: URL? { urls["back_female"] }
    var backShinyFemale: URL? { urls["back_shiny_female"] }
}

也可从以下楼层进入较低楼层urls字典:

print(sprites.urls["other/official-artwork/front_default"]!)

但话又说回来,这非常不方便。我们可以使用 Variant 下标做得更好。

struct Variant: Hashable, CustomStringConvertible {
    let stringValue: String
    init(_ stringValue: String) { self.stringValue = stringValue }
    var description: String { stringValue }

    static var official: Variant { Variant("other/official-artwork/") }
    static var dreamWorld: Variant { Variant("other/dream_world/") }
    static var redBlue: Variant { Variant("versions/generation-i/red-blue/")}
}

还有一个重写字典的下标,从键中删除变体前缀,并删除任何缺少该前缀的内容:

extension Sprites {
    subscript(variant: Variant) -> Sprites {
        let prefix = variant.stringValue

        return Sprites(urls: urls.reduce(into: [:]) { (dict, kv) in
            let (key, value) = kv
            if key.hasPrefix(prefix) {
                let newKey = String(key.dropFirst(prefix.count))
                dict[newKey] = value
            }
        })
    }
}

这允许您从现有的 Sprite 中提取新的 Sprite:

let official = sprites[.official]
print(official.frontDefault!)

完整要点

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Swift Codable 重用在不同模型结构/类之间共享的键子集 的相关文章

随机推荐