1

我一直Codable很高兴在我当前的项目中使用 s - 一切都很好,我开箱即用的大部分东西都是内置的 - 完美!不过,最近我偶然发现了第一个真正的问题,无法按照我想要的方式自动解决。


问题描述
我有一个来自后端的 JSON,它是一个嵌套的东西。看起来像这样

{
    "id": "fef08c8d-0b16-11e8-9e00-069b808d0ecc",
    "title": "Challenge_Chapter",
    "topics": [
        {
            "id": "5145ea2c-0b17-11e8-9e00-069b808d0ecc",
            "title": "Automation_Topic",
            "elements": [
                {
                    "id": "518dfb8c-0b18-11e8-9e00-069b808d0ecc",
                    "title": "Automated Line examle",
                    "type": "text_image",
                    "video": null,
                    "challenge": null,
                    "text_image": {
                        "background_url": "",
                        "element_render": ""
                    }
                },
                {
                    "id": "002a1776-0b18-11e8-9e00-069b808d0ecc",
                    "title": "Industry 3.0 vs. 4.0: A vision of the new manufacturing world",
                    "type": "video",
                    "video": {
                        "url": "https://www.youtube.com/watch?v=xxx",
                        "provider": "youtube"
                    },
                    "challenge": null,
                    "text_image": null
                },
                {
                    "id": "272fc2b4-0b18-11e8-9e00-069b808d0ecc",
                    "title": "Classmarker_element",
                    "type": "challenge",
                    "video": null,
                    "challenge": {
                        "url": "https://www.classmarker.com/online-test/start/",
                        "description": null,
                        "provider": "class_marker"
                    },
                    "text_image": null
                }
            ]
        }
    ]
}

Chapter是根对象,它包含一个Topics列表,每个主题都包含一个Elements列表。非常简单,但我被最低级别的Elements卡住了。每个Element都有一个来自后端的枚举,如下所示: [ video, challenge, text_image ],但 iOS 应用程序不支持挑战,因此我在 Swift 中的ElementType枚举如下所示:

public enum ElementType: String, Codable {
    case textImage = "text_image"
    case video = "video"
}

当然,它throws,因为发生的第一件事是它试图解码challenge这个枚举的值并且它不存在,所以我的整个解码失败了。

我想要
的 只是希望解码过程忽略 Element无法解码的 s 。我不需要任何Optionals。我只是希望它们不会出现在 Topic 的元素数组中。

我的推理和它的缺点
当然,我已经做了几次尝试来解决这个问题。第一个,最简单的只是标记ElementTypeOptional,但是稍后使用这种方法我将不得不打开所有内容并处理它 - 这是一项相当乏味的任务。我的第二个想法是.unsupported在我的枚举中有类似 case 的东西,但是稍后我想用它来生成单元格,我将不得不throw或返回Optional- 基本上,与以前的想法相同的问题。我的最后一个想法,但我还没有测试过,是编写一个init()可解码的自定义并以某种方式在那里处理它,但我不确定它是否Element应该Topic对此负责?如果我把它写进去Element,我不能返回任何东西,我将不得不throw,但如果我把它放进去,Topic我必须append成功地将元素解码为数组。问题是,如果在某个时候我将直接获取,将会发生什么- 再次,如果没有ing Elements,我将无法做到这一点。throw

TL;DR
我不想init(from decoder: Decoder) throwsthrow但要返回Optional

4

2 回答 2

2

我终于在SR-5953中找到了一些关于这个的东西,但我认为这是一个 hacky 的。

无论如何,对于允许这种有损解码的好奇者,您需要手动解码所有内容。你可以把它写在你里面init(from decoder: Decoder),但更好的方法是写一个新的助手,struct叫做FailableCodableArray. 实现看起来像:

struct FailableCodableArray<Element: Decodable>: Decodable {
    // https://github.com/phynet/Lossy-array-decode-swift4
    private struct DummyCodable: Codable {}

    private struct FailableDecodable<Base: Decodable>: Decodable {
        let base: Base?

        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            self.base = try? container.decode(Base.self)
        }
    }

    private(set) var elements: [Element]

    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()
        var elements = [Element]()

        if let count = container.count {
            elements.reserveCapacity(count)
        }

        while !container.isAtEnd {
            guard let element = try container.decode(FailableDecodable<Element>.self).base else {
                _ = try? container.decode(DummyCodable.self)
                continue
            }

            elements.append(element)
        }

        self.elements = elements
    }
}

而对于那些可能失败的元素的实际解码,您必须编写一个简单的init(from decoder: Decoder)实现,例如:

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    elements = try container.decode(FailableCodableArray<Element>.self, forKey: .elements).elements
}

正如我所说,这个解决方案工作正常,但感觉有点hacky。这是一个开放的错误,所以你可以投票并让 Swift 团队看到,像这样的内置内容将是一个不错的补充!

于 2018-02-09T12:03:35.540 回答
1

我建议为所有三种类型创建一个综合协议

protocol TypeItem {}

编辑:为了符合只能考虑两种类型的要求,您必须使用类来获取引用语义

然后创建类TextImageVideo一个Dummy采用该协议的类。的所有实例Dummy class将在解码过程后被删除。

class TextImage : TypeItem, Decodable {
    let backgroundURL : String
    let elementRender : String

    private enum CodingKeys : String, CodingKey {
        case backgroundURL = "background_url"
        case elementRender = "element_render"
    }
}

class Video : TypeItem, Decodable {
    let url : URL
    let provider : String
}

class Dummy : TypeItem {}

使用枚举type正确解码

enum Type : String, Decodable {
    case text_image, video, challenge
}

在结构Element中,您必须实现一个自定义初始化程序,该初始化程序根据类型将 JSON 解码为结构。不需要的challange类型被解码为一个Dummy实例。由于伞形协议,您只需要一个属性。

class Element : Decodable {
    let type : Type
    let id : String
    let title : String
    let item : TypeItem

    private enum CodingKeys : String, CodingKey {
        case id, title, type, video, text_image
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(String.self, forKey: .id)
        title = try container.decode(String.self, forKey: .title)
        type = try container.decode(Type.self, forKey: .type)
        switch type {
        case .text_image: item = try container.decode(TextImage.self, forKey: .text_image)
        case .video: item = try container.decode(Video.self, forKey: .video)
        default: item = Dummy()
        }
    }
}

最后为Root根元素和数组创建一个结构。添加过滤实例的方法。TopictopicsTopicDummy

class Root : Decodable {
    let id : String
    let title : String
    var topics : [Topic]
}

class Topic : Decodable {
    let id : String
    let title : String
    var elements : [Element]

    func filterDummy() {
        elements = elements.filter{!($0.item is Dummy)}
    }
}

filterDummy()在每个解码调用后Topic删除死项目。另一个缺点是您必须转换item为静态类型,例如

let result = try decoder.decode(Root.self, from: data)
result.topics.forEach({$0.filterDummy()})
if let videoElement = result.topics[0].elements.first(where: {$0.type == .video}) {
    let video = videoElement.item as! Video
    print(video.url)
}
于 2018-02-09T11:51:37.590 回答