Search code examples
iosswiftenumsswift5

Swift enum associated value with different types


I have a Swift enum like this:

public enum AnimationType {

    case position(Float)
    case position([Keyframe<Float>])
    case scale(Float)
    case scale([Keyframe<Float>])
    case rect(CGRect)
    case rect([Keyframe<CGRect>])
    case transform(CGAffineTransform)
    case transform([Keyframe<CGAffineTransform>])
    ...
    ...
}

As we can see, for each type there are two values possible - fixed value of type T or an array of keyframes with value type T ([Keyframe]). I am wondering if there is anything I can do to avoid repetition of same name in the enum and merge the two enum case types? Or I am modelling it wrong way?


Solution

  • I would solve this with a Kind enum type, for each kind of variation.

    public enum AnimationType {
        public enum Kind<Value> {
            case scalar(Value)
            case keyframes([Keyframe<Value>])
        }
    
        case position(Kind<Float>)
        case scale(Kind<Float>)
        case rect(Kind<CGRect>)
        case transform(Kind<CGAffineTransform>)
    }
    

    Usage:

    let anim1 = AnimationType.position(.scalar(10))
    let anim2 = AnimationType.position(.keyframes([Keyframe(10)]))
    

    Getting values:

    switch anim1 {
    case .position(let kind):
        switch kind {
        case .scalar(let value):
            print("value: \(value)")
        case .keyframes(let keyframes):
            print("keyframes: \(keyframes)")
        }
    
    default: // You would implement the rest
        break
    }
    
    switch anim1 {
    case .position(.scalar(let value)):
        print("value: \(value)")
    
    case .position(.keyframes(let keyframes)):
        print("keyframes: \(keyframes)")
    
    default: // You would implement the rest
        break
    }
    
    if case .position(.scalar(let value)) = anim1 {
        print("value: \(value)")
    }
    

    You can also add Codable conformance:

    public struct Keyframe<Value: Codable> {
        let value: Value
    
        init(_ value: Value) {
            self.value = value
        }
    }
    
    extension Keyframe: Codable {
        public func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            try container.encode(value)
        }
    
        public init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            value = try container.decode(Value.self)
        }
    }
    
    public enum AnimationType {
        public enum Kind<Value: Codable> {
            case scalar(Value)
            case keyframes([Keyframe<Value>])
        }
    
        case position(Kind<Float>)
        case scale(Kind<Float>)
        case rect(Kind<CGRect>)
        case transform(Kind<CGAffineTransform>)
    }
    
    extension AnimationType.Kind: Codable {
        public func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
    
            switch self {
            case .scalar(let value): try container.encode(value)
            case .keyframes(let keyframes): try container.encode(keyframes)
            }
        }
    
        public init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
    
            if let scalar = try? container.decode(Value.self) {
                self = .scalar(scalar)
                return
            }
            if let keyframes = try? container.decode([Keyframe<Value>].self) {
                self = .keyframes(keyframes)
                return
            }
    
            // You should throw error here instead
            fatalError("Failed to decode")
        }
    }
    
    extension AnimationType: Codable {
        private enum CodingKeys: CodingKey {
            case position
            case scale
            case rect
            case transform
        }
    
        public func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
    
            switch self {
            case .position(let kind): try container.encode(kind, forKey: .position)
            case .scale(let kind): try container.encode(kind, forKey: .scale)
            case .rect(let kind): try container.encode(kind, forKey: .rect)
            case .transform(let kind): try container.encode(kind, forKey: .transform)
            }
        }
    
        public init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
    
            if let position = try? container.decode(Kind<Float>.self, forKey: .position) {
                self = .position(position)
                return
            }
            if let scale = try? container.decode(Kind<Float>.self, forKey: .scale) {
                self = .scale(scale)
                return
            }
            if let rect = try? container.decode(Kind<CGRect>.self, forKey: .rect) {
                self = .rect(rect)
                return
            }
            if let transform = try? container.decode(Kind<CGAffineTransform>.self, forKey: .transform) {
                self = .transform(transform)
                return
            }
    
            // You should throw error here instead
            fatalError("Failed to decode")
        }
    }
    

    Example encoding:

    do {
        let data = try JSONEncoder().encode(anim1)
        if let str = String(data: data, encoding: .utf8) {
            print(str)
            // Prints: {"position":10}
        }
    } catch {
        print(error)
    }
    

    The same sort of thing with anim2 returns {"position":[10]}.