Search code examples
iosswiftcodabledecodable

init(from decoder: Decoder) is causing a "Cannot infer contextual base" error in separate convenience init


I'm trying to subclass a Codable class and it's working fine until I add a init(from decoder: Decoder) function. Then, the compiler is giving me 2 errors on my convenience init:

Cannot infer contextual base in reference to member 'geometry'

Extra arguments at positions #2, #3, #4, #5 in call

If I remove the decode function, I get no errors and the encode function works as expected. How can I have both a convenience init and a decoder function? Is there some unwritten rule prohibiting this?

class GeometryNode: Node {
    var values = GeometryNode.Values(shape: .triangle)
    
    enum CodingKeys: String, CodingKey {
        case values
        
        case id
        case type
        case indexPath
    }
    
    // NOTE: GeometryNode.Values is a codable struct
    // NOTE: GeometryNode.Values.Shape a codable enum
    // NOTE: both are defined elsewhere
    convenience init(id: String? = nil, shape: GeometryNode.Values.Shape = GeometryNode.Values.Shape.triangle, indexPath: IndexPath) {
        let icon = shape.icon
        let color = shape.color
        let title = shape.rawValue

        // Error shows for below call
        self.init(id: id, title: title, type: .geometry, icon: icon, color: color, indexPath: indexPath)
    }
    
    override func encode(to encoder: Encoder) throws {
        try super.encode(to: encoder)
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.values, forKey: .values)
    }
    
    // Remove this function and error above goes away
    required init(from decoder: Decoder) throws {
        try super.init(from: decoder)
        let container = try decoder.container(keyedBy: CodingKeys.self)
        values = try container.decode(GeometryNode.Values.self, forKey: .values)
    }
    
}
extension GeometryNode {
    struct Values: Codable {
        
        var shape: Shape
        
        enum Shape: String, CaseIterable, Codable {
            case triangle
            case rectangle
            case oval
            
            var defaultColor: UIColor {
                return NodeType.geometry.defaultColor
            }
            
            var fontSize: CGFloat {
                return 24
            }
            
            var icon: UIImage {
                return miscValues.icon
            }

            var color: UIColor {
                return miscValues.color
            }

            private var miscValues: (icon: UIImage, color: UIColor) {
                switch self {
                    case .triangle: return ("📐".textToImage(fontSize: fontSize)!, color: defaultColor)
                    case .rectangle: return ("◾️".textToImage(fontSize: fontSize)!, color: defaultColor)
                    case .oval: return ("⚫️".textToImage(fontSize: fontSize)!, color: defaultColor)
                }
            }
        }
        
        enum CodingKeys: String, CodingKey {
            case shape
        }
    }
}
public class Node: Codable {
    
    
    var id: String?
    var title: String?
    var type: NodeType = .geometry
    var icon: UIImage?
    var color: UIColor?
    var indexPath: IndexPath
    
    var defaultColorForType: UIColor {
        return type.defaultColor
    }
    var absoluteCoordinates: CGPoint? {
        return CGPoint(x: indexPath.item * kCellWidth, y: indexPath.section * kCellHeight)
    }
    func dictionary() -> [String: Any] {
        let data = (try? JSONEncoder().encode(self)) ?? Data()
        return (try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any]) ?? [:]
    }
    
    enum CodingKeys: String, CodingKey {
        case id
        case type
        case indexPath
    }
    
    internal init(id: String? = nil, title: String, type: NodeType = .geometry, icon: UIImage? = nil, color: UIColor? = nil, indexPath: IndexPath) {
        self.id = id
        self.title = title
        self.type = type
        self.icon = icon
        self.color = color
        self.indexPath = indexPath
    }
}

extension IndexPath {
    enum CodingKeys: String, CodingKey {
        case item
        case section
    }
}

Solution

  • If you don't really need a convenience initializer then you can call super.init inside that initializer, like this:

    init(id: String? = nil, shape: GeometryNode.Values.Shape = GeometryNode.Values.Shape.triangle, indexPath: IndexPath) {
        let icon = shape.icon
        let color = shape.color
        let title = shape.rawValue
    
        super.init(id: id, title: title, type: .geometry, icon: icon, color: color, indexPath: indexPath)
    }
    

    If you still need the initializer to be convenience you may need to override the super's initializer because Swift only allows convenience initializers to call designated initializers from the same class.

    convenience init(id: String? = nil, shape: GeometryNode.Values.Shape = GeometryNode.Values.Shape.triangle, indexPath: IndexPath) {
        let icon = shape.icon
        let color = shape.color
        let title = shape.rawValue
    
        self.init(id: id, title: title, type: .geometry, icon: icon, color: color, indexPath: indexPath)
    }
    
    override init(id: String? = nil, title: String, type: NodeType = .geometry, icon: UIImage? = nil, color: UIColor? = nil, indexPath: IndexPath) {
        super.init(id: id, title: title, type: type, icon: icon, color: color, indexPath: indexPath)
    }