从嵌套容器解码 JSON 并动态检查其类型以在 swift 中进行类型转换

Decode JSON from the nested container and check its type dynamically for typecasting in swift

这是我要解码的 JSON。它在屏幕中有不同类型的对象数组,所以我想根据它的 objectid 投射每个对象。例如,如果它的对象 ID 是 bd_label 那么它应该是 LabelConfig 类型。

{
    "objectid": "template",
    "version": {
        "major": "2",
        "minor": "1"
    },
    "screens":[
        {
            "id":"1",
            "objectid":"bd_label",
            "height": "100",
            "width" : "50",
            "label": "it's a label"
            
        },
        {
            "id":"2",
            "objectid":"bd_input",
            "height": "100",
            "width" : "50",
            "placeholder": "enter your input"
        },
        {
            "id":"3",
            "objectid":"bd_button",
            "height":"100",
            "width" : "50",
            "btn_label":
            [
                "click",
                " the",
                " button"
            ]
            
        }
    ]
}

我想解码它,为此我尝试了以下结构:

struct Version : Codable{
    var major : String
    var minor : String
}

protocol ComponentConfig: class, Codable{
    var id : String { get set }
    var objectid : String { get set }
}

class LabelConfig : ComponentConfig {
    var id: String
    var objectid : String
    var height : String?
    var width : String?
    var label : String?
}

class ButtonConfig : ComponentConfig {
    var id: String
    var objectid : String
    var height : String?
    var width : String?
    var btn_label : [String]
}

class InputConfig : ComponentConfig {
    var id: String
    var objectid : String
    var height : String?
    var width : String?
    var placeholder : String?
}

在这里,我想根据该对象的 objectid 属性 来决定要动态解码的 UIConfig 类型,即 LabelConfigButtonConfigInputConfig

struct ScreenData: Decodable {
    
    var objectid : String
    var version: Version
    var screens : [ComponentConfig]
    
    enum CodingKeys: CodingKey {
        case objectid, version, screens
    }

 init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        objectid = try container.decode(String.self, forKey: .objectid)
        version = try container.decode(Version.self, forKey: .version)
      }
} 

使用继承很简单

import SwiftUI
class CustomJSONViewModel: ObservableObject {
    var configObject: ConfigObject?{
        do {
            let decodedResponse = try JSONDecoder().decode(ConfigObject.self, from: jsonData!)
            return decodedResponse
        } catch {
            print(error)
            return nil
        }
    }
    
    let jsonData = """
    {
        "objectid": "template",
        "version": {
            "major": "2",
            "minor": "1"
        },
        "screens":[
            {
                "id":"1",
                "objectid":"bd_label",
                "height": "100",
                "width" : "50",
                "label": "it's a label"
                
            },
            {
                "id":"2",
                "objectid":"bd_input",
                "height": "100",
                "width" : "50",
                "placeholder": "enter your input"
            },
            {
                "id":"3",
                "objectid":"bd_button",
                "height":"100",
                "width" : "50",
                "btn_label":
                [
                    "click",
                    " the",
                    " button"
                ]
                
            }
        ]
    }
    """.data(using: .utf8)
}
struct CustomJSONView: View {
    @StateObject var vm: CustomJSONViewModel = CustomJSONViewModel()
    var body: some View {
        List{
            Text(vm.configObject?.objectid ?? "nil")
            Text(vm.configObject?.version.major ?? "nil")
            Text(vm.configObject?.version.minor ?? "nil")
            if vm.configObject?.screens != nil{
                ForEach(vm.configObject!.screens, id: \.id){ screen in
                    //To access the specific properties you have to detect the type
                    if screen is BDInput{
                        //Once you know what type it is you can force it into that type
                        Text((screen as! BDInput).placeholder ?? "nil placeholder").foregroundColor(.green)
                    }else if screen is BDLabel{
                        Text((screen as! BDLabel).label ?? "nil label").foregroundColor(.orange)
                    }else if screen is BDButton{
                        Text((screen as! BDButton).btnLabel?.first ?? "nil button").foregroundColor(.blue)
                    }else{
                        //You would default to Screen if the type in unknown 
                        Text(screen.objectid)
                    }
                }
            }
        }
    }
}
// MARK: - ConfigObject
class ConfigObject: Codable {
    let objectid: String
    let version: Version
    let screens: [Screen]
    init(objectid: String, version: Version, screens: [Screen]) {
        self.objectid = objectid
        self.version = version
        self.screens = screens
    }
    required public init(from decoder: Decoder) throws {
        do{
            let coder = try decoder.container(keyedBy: CodingKeys.self)
            self.objectid = try coder.decode(String.self, forKey: .objectid)
            self.version = try coder.decode(Version.self, forKey: .version)
            //
            let container = try decoder.container(keyedBy: CodingKeys.self)
            var objectsArray = try container.nestedUnkeyedContainer(forKey: CodingKeys.screens)
            var items = [Screen]()
            
            var array = objectsArray
            while !objectsArray.isAtEnd {
                let object = try objectsArray.nestedContainer(keyedBy: Screen.CodingKeys.self)
                let type = try object.decode(String.self, forKey: Screen.CodingKeys.objectid)
                //Here is where the decision is made to decode as the specific type/objectid
                switch type {
                case "bd_label":
                    items.append(try array.decode(BDLabel.self))
                case "bd_input":
                    items.append(try array.decode(BDInput.self))
                case "bd_button":
                    items.append(try array.decode(BDButton.self))
                default:
                    items.append(try array.decode(Screen.self))
                }
            }
            self.screens = items
        }catch{
            print(error)
            throw error
        }
    }
    enum CodingKeys: String, CodingKey {
        case objectid, version, screens
    }
}

// MARK: - Screen
///The different types will be a Screen
class Screen: Codable {
    let id, objectid, height, width: String
    enum CodingKeys: String, CodingKey {
        case id, objectid, height, width
    }
    
    init(id: String, objectid: String, height: String, width: String) {
        self.id = id
        self.objectid = objectid
        self.height = height
        self.width = width
    }
    //This superclass would do all the work for the common properties
    required public init(from decoder: Decoder) throws {
        do{
            let coder = try decoder.container(keyedBy: CodingKeys.self)
            self.id = try coder.decode(String.self, forKey: .id)
            self.objectid = try coder.decode(String.self, forKey: .objectid)
            self.height = try coder.decode(String.self, forKey: .height)
            self.width = try coder.decode(String.self, forKey: .width)
        }catch{
            print(error)
            throw error
        }
    }
    
}
// MARK: - BDLabel
class BDLabel: Screen {
    //Each subclass would handle their individual properties
    let label: String?
    init(id: String, objectid: String, height: String, width: String, label: String?) {
        self.label = label
        super.init(id: id, objectid: objectid, height: height, width: width)
    }
    //Each subclass would handle their individual properties
    required public init(from decoder: Decoder) throws {
        do{
            let coder = try decoder.container(keyedBy: CodingKeys.self)
            self.label = try coder.decode(String.self, forKey: .label)
            //Sending the super class work to be done by Screen
            try super.init(from: decoder)
        }catch{
            print(error)
            throw error
        }
    }
    enum CodingKeys: String, CodingKey {
        case  label
    }
}
// MARK: - BDInput
class BDInput: Screen {
    //Each subclass would handle their individual properties
    let placeholder: String?
    init(id: String, objectid: String, height: String, width: String, placeholder: String?) {
        self.placeholder = placeholder
        //Sending the super class work to be done by Screen
        super.init(id: id, objectid: objectid, height: height, width: width)
    }
    required public init(from decoder: Decoder) throws {
        do{
            let coder = try decoder.container(keyedBy: CodingKeys.self)
            self.placeholder = try coder.decode(String.self, forKey: .placeholder)
            try super.init(from: decoder)
        }catch{
            print(error)
            throw error
        }
    }
    enum CodingKeys: String, CodingKey {
        case placeholder
    }
}
// MARK: - BDButton
class BDButton: Screen {
    //Each subclass would handle their individual properties
    let btnLabel: [String]?
    init(id: String, objectid: String, height: String, width: String, btnLabel: [String]?) {
        self.btnLabel = btnLabel 
        //Sending the super class work to be done by Screen
        super.init(id: id, objectid: objectid, height: height, width: width)
    }
    required public init(from decoder: Decoder) throws {
        do{
            let coder = try decoder.container(keyedBy: CodingKeys.self)
            self.btnLabel = try coder.decode([String].self, forKey: .btnLabel)
            //Sending the super class work to be done by Screen
            try super.init(from: decoder)
        }catch{
            print(error)
            throw error
        }
    }
    enum CodingKeys: String, CodingKey {
        case btnLabel = "btn_label"
    }
}
// MARK: - Version
struct Version: Codable {
    let major, minor: String
}

struct CustomJSONView_Previews: PreviewProvider {
    static var previews: some View {
        CustomJSONView()
    }
}