Search code examples
swiftswift3

Dynamic protocol conformance in Swift


Hi I am struggle to solve the problem dynamic protocol conformance in swift language. Please see code.

Protocol:

protocol Object {

    init(by object: [String: Any])
}

Custom structs with protocol object conformance:

struct Tree: Object {

    let treeName: String

    init(by object: [String: Any]) {

        self.treeName = object["tree"] as? String ?? "Notree"
    }
}

struct Plant: Object {

    let plantName: String

    init(by object: [String : Any]) {

        self.plantName = object["tree"] as? String ?? ""
    }
}

The above code just fine until the object is [String: Any]. I can't use [[String: Any]] like below.

let coconut = ["tree":"Coconut"] // => This fine
let allTrees = [["tree":"Apple"],["tree":"Orange"],["tree":"Jakfruit"]] //=> Here is the problem

 let aTree = Tree(by: coconut)
 let bTree = Tree(by: ["data":allTrees])
 let cTree = Plant(by: ["data":allTrees])

I can't use array of objects. So, I used to store objects in to key "data". Now I used extension: Array confirm protocol object.

extension Array: Object where Element == Object{

    init(by object: [String : Any]) {

        if let data = object["data"] as? [[String: Any]]{

            self  = data.map({ (object) -> Object in

//                return Plant.init(by: object) // => Works, But I need dynamic confirmance
//                return Tree.init(by: object) // => Works, But I need dynamic confirmance

                return Object.init(by: object) //=> How can I do?
            })
        }else{

            self = []
        }
    }
}

The return Object shows error Protocol type 'Object' cannot be instantiated. I tried lot to solve but not able.

Can someone suggest better idea or solution for this problem? Thank you in advance...


Solution

  • First, you should not use the constraint == Object. You want to say that not only [Object] is an Object, but also [Plant] and [Tree] are Objects too, right? For that, you should use the : Object constraint. Second, you can use Element.init to initialise a new Element of the array. Because of the constraint Element : Object, we know that a init(by:) initialiser exists:

    extension Array: Object where Element: Object{
    
        init(by object: [String : Any]) {
    
            if let data = object["data"] as? [[String: Any]]{
    
                self  = data.map({ (object) in
    
                    return Element.init(by: object)
                })
            }else{
    
                self = []
            }
        }
    }
    

    Usage:

    let trees = [Tree](by: ["data": allTrees])
    

    Here's what I think a more Swifty version of your code, making use of failable initialisers - initialisers that return nil when they fail to initialise the object:

    protocol Object {
    
        init?(by object: [String: Any])
    }
    
    
    struct Tree: Object {
    
        let treeName: String
    
        init?(by object: [String: Any]) {
    
            if let treeName = object["tree"] as? String {
                self.treeName = treeName
            } else {
                return nil
            }
        }
    }
    
    struct Plant: Object {
    
        let plantName: String
    
        init?(by object: [String : Any]) {
    
            if let plantName = object["tree"] as? String {
                self.plantName = plantName
            } else {
                return nil
            }
        }
    }
    
    extension Array: Object where Element: Object{
    
        init?(by object: [String : Any]) {
    
            if let data = object["data"] as? [[String: Any]]{
                self  = data.compactMap(Element.init)
            }else{
                return nil
            }
        }
    }