Swift Codable重用跨模型共享的部分键 [英] Swift Codable reusing a subset of keys shared across different models Struct/Class

查看:59
本文介绍了Swift Codable重用跨模型共享的部分键的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想知道是否存在通过使用自定义解码器初始化程序和/或多个Containers和Codingkeys在多个模型之间共享通用属性的可行方法.这是我要映射到相应Codable模型的JSON对象:

I am wondering if there is a feasible way to share common properties across multiple models by using a custom decoder initializer and/or multiple Containers and Codingkeys. Here is the JSON Object i want to map to the corresponding Codable Model:

JSON对象

我要映射到 Codable 的属性是"子画面".

The property i want to map to Codable is 'sprites'.

如您所见, back_default,back_female等.等许多属性可在其他模型之间共享,例如 sprites-> other-> dream_world,sprites-> other->官方艺术品,并在 sprites-> versions-> generation-i-> red-blue

As you can see, a lot of properties like back_default, back_female etc.. are sharable across other models like for example sprites->other->dream_world, sprites->other->official-artwork and in sprites->versions->generation-i->red-blue

我的目标是能够使用一个单一的共享模型并自定义他的解码器(也许通过使用多个容器),因此我可以说例如:如果当前要解码的密钥是" official-artwork "仅解码此模型对象公开的所有C​​odingKeys的一个子集(也许我需要使用另一个子容器?),因此对于 officialartwork 键,我只有1个解码字段,而2个解码字段关键梦想世界等.

My goal is to be able to use one single shared model and customize his decoder (maybe by using multiple Containers) thus i can say for example: if the current key to be decode is "official-artwork" decode only a subset of all the CodingKeys exposed by this model object (maybe i need to use another sub-container?) so i will have only 1 decoded field for the key official-artwork, 2 decoded field for the key dream-world and so on.

[ EDIT ]

目前,我想出了 other 键的解决方案是这个,但是一些属性是共享的,并且我认为有更好的解决方案(如果我理解我可以做到的话,我可以将其应用于模型的版本键):

At the moment the solution i came up with the other key is this, but some properties are shared and i think there are a better solution (if i understand i to do it properly i can than apply it to the versions key of the model):

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和中型博客文章,但它们并没有帮助我.

I can use the model before and extend it with all the other json object to represent but it will be a lot of repetitive code to write and i think there is a better approach than what i am currently doing. I have read a lot of SO and medium blog post but they didn't helped me.

我不确定是否可以在 Swift 中完成我要问的事情.

I am not sure if what am i asking is even possible to do in Swift.

[ EDIT 2 ]

我认为最好从头开始,让其他人了解我要完成的工作.目前,我用于该JSON结构的Codable是这样的:

I think it's better to start from scratch to give other people an idea of what i am trying to accomplish. Currently my Codable for that JSON struct is this:

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?
      
   }

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

I omitted the other generations struct because the concept will be the same.

使用这种方法,所有方法都有效,但问题是我始终使用结构'PokemonSpritesModelVersionsGenerationsColors'中的完整键集(我也应该使用过'sprites'键集,但我不知道如何使用推断Set并使sprites结构仍然有效,另外我还应该在'PokemonSpritesModelVersionsGenerationsColors'中添加带有*标记的新灰色.

With this approach everything works but the problem is that i am using always the complete set of keys from the struct 'PokemonSpritesModelVersionsGenerationsColors' (also i should have used the set of keys of 'sprites' but i don't know how to extrapolate that Set and make the sprites struct still working, plus i should also add the new gray color marked with * in 'PokemonSpritesModelVersionsGenerationsColors').

正如您所说,重复多次键入的键是 sprites 结构的子集(应排除 other versions json对象,但正如我之前说过的,应添加带有*的精灵.)

As you said the keys that get repeated several type are a subset of the sprites struct (other and versions json object should be excluded, but as i said earlier the sprites marked with * should be added).

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

From the research i have made there are 2 possible solutions to accomplish what i want to do:

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

  1. Create a separate DTO layer (models that represent the data returned by the Server API) and then have another Domain Layer (Models of the App). Each Domain Model will filtrate the Set of keys of the corresponding DTO model so the Domain layers models will match the Server response (the DTO models doesn't match the server response because it will not use only the required keys of the necessary subset)

使用自定义解码器初始化程序 + 多个容器的某种组合,通过对键子类型枚举进行编码(可能借助枚举关联值)+可能其他事情.

Use some sort of combination of custom decoder initializer + multiple container through coding key subtype enum (maybe with the help of enum associated value) + maybe other things.

我想避免第一种方法,主要是因为有两层几乎都是相同的,除了域模型中包含的字段和业务逻辑可以过滤必要的键.我当时想继续使用2方法.

I want to avoid the first approach mainly because of having two layers which will be mostly identical except for the fields and business logic contained in the Domain models to filtrate the necessary keys. I was thinking to proceed with the 2 approach.

我认为我对如何执行操作有一个想法,但我无法将所有内容都付诸实践.主要是我在阅读以下博客文章:

I think i have an idea in my mind of how to do it but i can't put all the pieces togheter. Mainly i was reading these blog posts:

  1. https://matteomanferdini.com/codable/
  2. https://lostmoa.com/blog/CodableConformanceForSwiftEnumsWithMultipleAssociatedValuesOfDiffer>
  1. https://matteomanferdini.com/codable/
  2. https://lostmoa.com/blog/CodableConformanceForSwiftEnumsWithMultipleAssociatedValuesOfDifferentTypes/

  • 其他资源
    1. 使用关联值仅解码键的子集吗?( https://forums.swift.org/t/codable-synthesis-for-enums-with-associated-values/41493 )
    2. 多个容器用作鉴别器,仅对密钥子集进行解码?( https://stackoverflow.com/a/53270057/2685716 )
    3. 多个容器?( https://stackoverflow.com/a/57788293/2685716 )

    我认为第一篇和第二篇博客文章是我可以借鉴一些想法来构建这种Sprites Json Codable stuct的文章.在第一篇博客文章中,这是代码中可能更有趣的部分:

    The 1 and the 2 blog post i think are the one from which i can take some ideas to build this sprites Json Codable stuct. From the 1 blog post this is the part of the code that maybe is more interesting:

    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对象的底部解码.

    From the 2 blog post this is the code that maybe is what i am looking for that start from a top/root key (our sprites json object?) and it goes down until the bottom of the Json objects that we need to decode.

    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."
                )
            )
        }
    }
    

    因此,我将尝试以最简单的方式总结我想做的事情:我的想法是创建多个属于全局变量的键子集的容器(其中一些是嵌套容器,也可能是无键容器).在不同模型之间共享的密钥集.比在相同的结构中,或者在分离的结构中可能更好(也许我还需要将与其关联的枚举键的子集移入其中),我将拥有一个自定义的解码器初始化程序,该初始化程序将仅创建在该枚举子集中定义的键键).

    So i will try to summarize in the most easy way what i want to do: My idea is to create multiple containers (which some of these will be nested containers and maybe unkeyed containers) of subsets of keys which belongs to a global sets of keys that are shared across different models. Than in the same struct or maybe better in separated struct (maybe i will need to move also the subset of enum keys associated with it inside of it) i will have a custom decoder initializer that will create only the keys defined in this enum subset of keys).

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

    @RobNapier Yes i want to use the same syntax. I still don't know what is the simplest or right approach. Could you provide a complete example with your approach of [String:URL] that enable me for example to decode the following json with the reuse of the shared key?

    {
      "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 以获得 front_shiny 的默认URL,并且可以使用诸如 sprites [.official] .frontDefault .我希望您可以稍微调整一下此实现以使其与您的用法相匹配,但这应该可以帮助您入门.

    There are a lot of ways to do this depending on what kind of interface you want this object to have, but here's one approach. When it's done, you'll be able to reference sprites.frontShiny to get the default URL for front_shiny, and you'll be able to get a variant using subscripts like sprites[.official].frontDefault. I expect that you'll want to tailor this implementation a bit to match your usage, but it should get you started.

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

    The structure we're decoding looks like this:

    {
      ...
      "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"之类的路径下的各个嵌套点.和版本/i代/红色-蓝色"(请参见 PokeAPI/sprites ).

    The important thing to note is that there is a Sprite structure that gets repeated several times, first at the root level, and then at various nested points under paths like "other/dream_world" and "versions/generation-i/red-blue" (see PokeAPI/sprites).

    我的假设是您不太在乎其他"内容.和版本/第i代"层,实际上只想讨论诸如 .dreamWorld .redBlue 之类的特定变体.(这可以适合于公开其他那些层,但是当前的实现有意将其隐藏.)

    My assumption is that you don't care much about the "other" and "versions/generation-i" layers, and really just want to talk about specific variants like .dreamWorld and .redBlue. (This can be adapted to exposing about those other layers, but the current implementation intentionally hides them.)

    第一个工具是来自带有未知动态的可解码的Swift 4的工具键,用于解码的任意字符串键:

    The first tool is the one from Swift 4 decodable with unknown dynamic keys, an arbitrary String key for decoding:

    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/....

    The other key tool is going to be a [String: URL] for storage, and variants will be stored like Unix paths: 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
                    }
                }
            }
        }
    }
    

    这样,您可以通过按键访问数据:

    With this, you can access the data by key:

    // 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"]!)
    

    可以,但是使用起来有点不方便.我们可以通过一些计算属性来对其进行改进.

    That's ok, but kind of inconvenient to use. We can improve it with some computed properties.

    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 字典访问较低级别的内容:

    Lower levels are also accessible from the urls dictionary:

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

    但是同样,这很不方便.我们可以使用Variant下标来做得更好.

    But again, this is pretty inconvenient. We can do better with a Variant subscript.

    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/")}
    }
    

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

    And a subscript that rewrites the dictionary, removing the variant prefix from the keys, and removing anything missing that prefix:

    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
                }
            })
        }
    }
    

    这使您可以从现有的精灵中提取新的精灵:

    And that allows you to extract new Sprites from an existing Sprites:

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

    完整要旨

    这篇关于Swift Codable重用跨模型共享的部分键的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆