【问题标题】:Swift Codable with Generics, holding common data from responsesSwift Codable with Generics,保存来自响应的公共数据
【发布时间】:2019-11-08 15:00:58
【问题描述】:

我会尽量以最好的方式解释我想要做什么,就像我在过去几天谷歌搜索时所做的那样。

我的应用程序正在与几个不同的 API 进行通信,但让我们首先考虑来自一个 API 的响应。每个端点的响应包含一些“公共参数”,例如状态或错误消息,以及我们最感兴趣的单个对象或对象数组,它带来重要数据,我们可能想要对其进行编码、存储、放置它到 Realm、CoreData 等。

例如,单个对象的响应:

{
    "status": "success",
    "response_code": 200,
    "messages": [
        "message1",
        "message2"
    ]
    "data": {
        OBJECT we're interested in.
    }
}

或者,使用对象数组响应:

{
    "status": "success",
    "response_code": 200,
    "messages": [
        "message1",
        "message2"
    ]
    "data": [
        {
            OBJECT we're interested in.
        },
        {
            OBJECT we're interested in.
        }
    ]
}

好的。这很简单,很容易理解。

现在,我想编写一个“根”对象,它将包含“通用参数”或 status、response_code 和 messages,并为特定对象(或对象数组)提供另一个属性我们感兴趣的。


继承
第一种方法是创建一个根对象,如下所示:

class Root: Codable {
    let status: String
    let response_code: Int
    let messages: [String]?

    private enum CodingKeys: String, CodingKey {
        case status, response_code, messages
    }

    required public init(from decoder: Decoder) throws {
        let container = try? decoder.container(keyedBy: CodingKeys.self)
        status = try container?.decodeIfPresent(String.self, forKey: .code) ?? ""
        response_code = try container?.decodeIfPresent(Int.self, forKey: .error) ?? 0
        messages = try container?.decodeIfPresent([String].self, forKey: .key)
    }

    public func encode(to encoder: Encoder) throws {}
}

一旦我有了这个根对象,我就可以创建从这个根对象继承的特定对象,并在 JSONDecoder 中传递我的特定对象,在那里,我有一个很好的解决方案。但是,此解决方案不适用于数组。 也许对于某些人来说不是,但我不能强调我不想制作额外的“复数”对象,该对象仅存在以容纳对象数组,例如:

class Objects: Root {
    let objects: [Object]
    // Code that decodes array of "Object" from "data" key
}

struct Object: Codable {
    let property1
    let property2
    let property3
    // Code that decodes all properties of Object
}

它看起来不干净,它需要单独的对象来简单地保存一个数组,它在某些情况下由于继承而产生存储到 Realm 的问题,它最重要的是产生了可读性较差的代码。


泛型
我的第二个想法是尝试使用泛型,所以我创建了a little something like this:

struct Root<T: Codable>: Codable  {
    let status: String
    let response_code: Int
    let messages: [String]?
    let data: T?

    private enum CodingKeys: String, CodingKey {
        case status, response_code, messages, data
    }

    required public init(from decoder: Decoder) throws {
        let container = try? decoder.container(keyedBy: CodingKeys.self)
        status = try container?.decodeIfPresent(String.self, forKey: .code) ?? ""
        response_code = try container?.decodeIfPresent(Int.self, forKey: .error) ?? 0
        messages = try container?.decodeIfPresent([String].self, forKey: .key)
        data = try container.decodeIfPresent(T.self, forKey: .data)
    }

    public func encode(to encoder: Encoder) throws {}
}

有了这个,我可以像这样将单个对象和对象数组传递给 JSONDecoder:

let decodedValue = try JSONDecoder().decode(Root<Object>.self, from: data)
// or
let decodedValue = try JSONDecoder().decode(Root<[Object]>.self, from: data)

这很不错。我可以在 Root 结构的 .data 属性中获取我需要的结构,并按照我喜欢的方式使用它,作为单个对象或作为对象数组。我可以很容易地存储它,但是我想要不受限制地进行操作,继承在上面的例子中带来。
这个想法在我的案例中失败的地方是当我想在某个不确定 T 设置为什么的地方访问“公共属性”时。
这是对我的应用程序中实际发生的事情的简化解释,我将对其进行一些扩展以解释此通用解决方案对我不起作用的地方,最后问我的问题。


问题和疑问
如顶部所述,应用程序使用 3 个 API,所有 3 个 API 都有不同的 Root 结构,当然还有很多不同的“子结构” - 命名它们。我在应用程序中有一个单独的位置,单个 APIResponse 对象,该对象可以返回到应用程序的 UI 部分,在其中我从 decoded value 提取 1 个可读错误,decoded value 是那个“子结构”,是我的任何一个“特定对象”,Car,Dog,House,Phone。
使用 继承 解决方案,我能够做这样的事情:

struct APIResponse <T> {
    var value: T? {
        didSet {
            extractErrorDescription()
        }
    }
    var errorDescription: String? = "Oops."

    func extractErrorDescription() {
        if let responseValue = value as? Root1, let error = responseValue.errors.first {
            self.errorDescription = error
        }
        else if let responseValue = value as? Root2 {
            self.errorDescription = responseValue.error
        }
        else if let responseValue = value as? Root3 {
            self.errorDescription = responseValue.message
        }
    }
}

但使用 Generics 解决方案,我无法做到这一点。如果我尝试使用 Root1 或 Root2 或 Root3 编写相同的代码,如 Generics 示例所示:

func extractErrorDescription() {
    if let responseValue = value as? Root1, let error = responseValue.errors.first {
        self.errorDescription = error
    }
}

我会收到错误消息Generic parameter 'T' could not be inferred in cast to 'Root1',在这里,我试图提取错误,我不知道哪个子结构被传递给了 Root1。是Root1&lt;Dog&gt; 或Root1&lt;Phone&gt; 还是Root1&lt;Car&gt; - 我不知道如何弄清楚,我显然需要知道值是Root1 还是Root2 或Root3。

我正在寻找的解决方案是一种可以让我区分 Root 对象和上面显示的泛型解决方案的解决方案,或者一种允许我以某种完全不同的方式进行架构解码的解决方案,记住一切我写的,尤其是避免“复数”对象的能力

*如果 JSON 没有通过 JSON 验证器,请忽略,它是为了这个问题而手写的
**如果编写的代码无法运行,请忽略,这更像是一个架构问题,而不是如何编译一些代码。

【问题讨论】:

  • 你不需要像if let responseValue = value as? Root1&lt;T&gt; 这样它知道要转换的类型吗?
  • 对于继承路由,您可以使每个路由都符合一个通用协议并在每个路由上都有一个计算属性以返回错误消息
  • @Scriptable 在您的第一条评论中是的,我需要if let responseValue = value as? Root1&lt;T&gt;,但我不知道T 在那个地方是什么,可以是与@ 一起使用的20 或30 个结构中的任何一个987654355@。如果我检查每一个,1 会起作用,我通常会访问我的“公共属性”,但这并不是真正的方法。
  • 你找到解决办法了吗?

标签: ios swift generics codable jsondecoder


【解决方案1】:

您在这里寻找的是协议。

protocol ErrorProviding {
    var error: String? { get }
}

我有意将errorDescription 更改为error,因为这似乎是您的根类型中的内容(但您绝对可以在此处重命名)。

那么 APIResponse 要求:

struct APIResponse<T: ErrorProviding> {
    var value: T?
    var error: String? { value?.error }
}

然后每个经过特殊处理的根类型实现协议:

extension Root1: ErrorProviding {
    var error: String? { errors.first }
}

但是已经具有正确形状的简单根类型可以只声明一致性,而不需要额外的实现。

extension Root2: ErrorProviding {}

假设您想要的不仅仅是 error,您可以使用 APIPayload 而不是 ErrorProviding,并添加任何其他常见要求。

附带说明,如果您只使用 Decodable 而不是将 Codable 与空的 encode 方法一起使用,您的代码会更简单。如果一个类型不能真正被编码,它就不应该符合 Encodable。

【讨论】:

  • 这很漂亮。谢谢罗布。我也了解您对 APIPayload 的想法 - 同意可解码。您能否让我知道您所说的“但您绝对可以在这里重命名”是什么意思,我的意思是,除了 'errorDescription' 是什么意思?我公开承认我需要回到协议并重新审视它们,当我更多地“管理”而不是“编码”时,它们正在上升。
  • 通过“重命名事物”我的意思是如果一层有error和一层有errorDescription就可以了;在这种情况下,您只需要编写更多的桥接代码。没有什么真正重要的。
  • 谢谢 Rob,我又想到了一个。使用这种 Generics 方法,我能否以某种方式(如果需要,在某些对象更新的情况下,API 仅返回 status、response_code 和 messages)解码“通用属性”?比如,我能以某种方式做类似try JSONDecoder().decode(Root&lt;Void&gt;.self, from: data) 的事情吗?从理论上讲,该类型是否可选?我可以通过创建一些没有属性的 VoidCodable 结构来想到“黑客”,但想知道是否有更好的方法?
  • 是的;我原本就是这么写的。我不会称它为VoidCodable。我会称它为struct Ignore: Decodable {},但是,这很好,这正是你的意思。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-11-15
  • 2023-04-07
  • 1970-01-01
  • 2017-09-16
  • 1970-01-01
  • 1970-01-01
  • 2020-01-14
相关资源
最近更新 更多