swiftstructmetatype

How to change a Struct metatype stored in a local variable


I'm trying to make a clothing recommendation app and I ran into a problem. I have a property called wardrobe, in which I would like to store all clothing items that are relevant to a type of activity the user picked.

My method looks like this:

func recommendClothing(for conditions: WeatherForSelection, type: ActivityType){
        temperature = conditions.feelsLike ?? 0
        isWindy = conditions.windSpeed ?? 0 > 5
        
        let wardrobe = CyclingClothing.self
    
        
        clothing = [
            Clothing(title: "Head", description: wardrobe.Head.forTemperature(temperature).name, image: wardrobe.Head.forTemperature(temperature).image),
            Clothing(title: "Base Layer", description: wardrobe.BaseLayer.forTemperature(temperature).name, image: wardrobe.BaseLayer.forTemperature(temperature).image),
            Clothing(title: "Mid Layer", description: wardrobe.MidLayer.forTemperature(temperature).name, image: wardrobe.MidLayer.forTemperature(temperature).image),
            Clothing(title: "Legs", description: wardrobe.Legs.forTemperature(temperature).name, image: wardrobe.Legs.forTemperature(temperature).image),
            Clothing(title: "Hands", description: wardrobe.Hands.forTemperature(temperature).name, image: wardrobe.Hands.forTemperature(temperature).image),
            Clothing(title: "Feet", description: wardrobe.Feet.forTemperature(temperature).name, image: wardrobe.Feet.forTemperature(temperature).image)
            
        ]
        
        if wardrobe.Shell.forTemperature(temperature) != .none {
            clothing.append(Clothing(title: "Shell", description: wardrobe.Shell.forTemperature(temperature).name, image: wardrobe.Shell.forTemperature(temperature).image))
        }
        
        if isWindy {
            clothing.append(Clothing(title: "Wind Protection", description: "Windproof Layer", image: "windproof"))
        }
        if conditions.isRaining ?? false {
            clothing.append(Clothing(title: "Rain Protection", description: "Waterproof Layer", image: "raincoat"))
        }
        
    }

I thought, that I would just switch on the input type and pick the wardrobe based on that. But then I would have to define the wardrobe's type, which would be always different. So I tried to put the Clothing structs into one main struct called Wardrobe and choose just the substructs for the wardrobe, but that didn't work for me either... I have also tried some shenanigans with protocols and extensions, but since I'm fairly new to this, it didn't work aswell...

Might class with it's ability to inherit work?

Is there a way to make this work or do I have to change the logic all together?

Example of the clothing struct looks like this:

struct CyclingClothing {
    
    //MARK: - Head
    enum Head {
        case helmet, headBand, lightCap, winterCap
        
        static func forTemperature(_ temperature: Double) -> Head {
            switch temperature {
            case ..<5:
                    .winterCap
            case 5..<10:
                    .lightCap
            case 10..<15:
                    .headBand
            case 15..<30:
                    .helmet
            default:
                    .helmet
            }
        }
        
        var name: String {
            switch self {
            case .helmet:
                "Just a Helmet"
            case .headBand:
                "Headband"
            case .lightCap:
                "Skullcap"
            case .winterCap:
                "Winter Cap"
            }
        }
        
        var image: String {
            switch self {
                
            case .helmet:
                "helmet"
            case .headBand:
                "headband"
            case .lightCap:
                "light_cap"
            case .winterCap:
                "winter_cap"
            }
        }
    }
    
    //MARK: - Base Layer
    enum BaseLayer {
        case none, lightBase, thermalBase
        
        static func forTemperature(_ temperature: Double) -> BaseLayer {
            switch temperature {
            case ..<10:
                    .thermalBase
            case 10..<20:
                    .lightBase
            default:
                    .none
            }
        }
        
        var name: String {
            switch self {
            case .none:
                "No Base Layer"
            case .lightBase:
                "Light"
            case .thermalBase:
                "Thermal"
            }
        }
        
        var image: String {
            switch self {
            case .none:
                "body"
            case .lightBase:
                "light_base"
            case .thermalBase:
                "thermal_base"
            }
        }
    }
    
    //MARK: - Mid Layer
    enum MidLayer {
        case jersey, longJersey, thermalJersey
        
        static func forTemperature(_ temperature: Double) -> MidLayer {
            switch temperature {
            case ..<10:
                    .thermalJersey
            case 10..<20:
                    .longJersey
            default:
                    .jersey
            }
        }
        
        var name: String {
            switch self {
            case .jersey:
                "Jersey"
            case .longJersey:
                "Long Sleeved Jersey"
            case .thermalJersey:
                "Thermal Jersey"
            }
        }
        
        var image: String{
            switch self {
            case .jersey:
                "jersey"
            case .longJersey:
                "long_jersey"
            case .thermalJersey:
                "thermal_jersey"
            }
        }
    }
    
    //MARK: - Shell
    enum Shell {
        case none, lightJacket, insulatedJacket
        
        static func forTemperature(_ temperature: Double) -> Shell {
            switch temperature {
            case ..<5:
                    .insulatedJacket
            case 5..<15:
                    .lightJacket
            default:
                    .none
            }
        }
        
        var name: String {
            switch self {
            case .none:
                "No Shell"
            case .lightJacket:
                "Light Jacket"
            case .insulatedJacket:
                "Insulated Jacket"
            }
        }
        
        var image: String {
            switch self {
            case .none:
                "no_shell"
            case .lightJacket:
                "light_jacket"
            case .insulatedJacket:
                "insulated_jacket"
            }
        }
        
    }
    
    //MARK: - Legs
    enum Legs {
        case shorts, longBibs, thermalBibs
        
        static func forTemperature(_ temperature: Double) -> Legs {
            switch temperature {
            case ..<10:
                    .thermalBibs
            case 10..<18:
                    .longBibs
            default:
                    .shorts
            }
        }
        
        var name: String {
            switch self {
            case .shorts:
                "Bib Shorts"
            case .longBibs:
                "Long Bibs"
            case .thermalBibs:
                "Thermal Bibs"
            }
        }
        
        var image: String {
            switch self {
            case .shorts:
                "bib_shorts"
            case .longBibs:
                "long_bibs"
            case .thermalBibs:
                "thermal_bibs"
            }
        }
    }
    
    //MARK: - Hands
    enum Hands {
        case longGloves, insulatedGloves, summerGloves
        
        static func forTemperature(_ temperature: Double) -> Hands {
            switch temperature {
            case ..<10:
                    .insulatedGloves
            case 10..<15:
                    .longGloves
            default:
                    .summerGloves
            }
        }
        
        var name: String {
            switch self {
            case .summerGloves:
                "Summer Gloves"
            case .longGloves:
                "Long Gloves"
            case .insulatedGloves:
                "Insulated Gloves"
            }
        }
        var image: String {
            switch self {
            case .summerGloves:
                "gloves"
            case .longGloves:
                "long_gloves"
            case .insulatedGloves:
                "insulated_gloves"
            }
        }
    }
    
    //MARK: - Feet
    enum Feet {
        case summerSocks, warmSocks, shoeCovers, winterShoes
        
        static func forTemperature(_ temperature: Double) -> Feet {
            switch temperature {
            case ..<3:
                return .winterShoes
            case 3..<7:
                return .shoeCovers
            case 7..<15:
                return .warmSocks
            default:
                return .summerSocks
            }
        }
        
        var name: String {
            switch self {
            case .summerSocks:
                "Light Socks"
            case .warmSocks:
                "Warm Socks"
            case .shoeCovers:
                "Shoe Covers"
            case .winterShoes:
                "Winter Shoes"
            }
        }
        
        var image: String {
            switch self {
            case .summerSocks:
                "socks"
            case .warmSocks:
                "warm_socks"
            case .shoeCovers:
                "covers"
            case .winterShoes:
                "winter_shoes"
            }
        }
    }
}




Solution

  • Your code is using types as values, in a way that's going to be pretty tricky to work with.

    The first issue you're going to run into is that you can't have your wardrobe value be chosen conditionally to pick one of two types. E.g. this won't work:

    let wardrobe = cycling ? CyclingWardrobe.self : WalkingWardrobe.self
    

    Because CyclingWardrobe and WalkingWardrobe are two completely unrelated types. They might happen to be structurally similar, but that's irrelevant. It's similar to having let i = small ? Int8() : Int64().

    To make them interoperable like this, you need to introduce an abstraction (super class or protocol) that bring them together under a common type which expresses the operations they can both handle. But in this case, you're access the nested types by name, and there's no (easy) way to abstract over that. This all becomes a mess pretty quickly.

    Instead, you should just work with objects/values directly. There's no need to go "meta" at the type level.

    1. Drop all the Enums.
    2. Create a protocol that describes what is in common for every wardrobe.
      • In this case, it's the ability to provide something for the head, baseLayer, midLayer, and so on. Capture those as requirements (functions or variables) of a protocol.
    3. Create different types which conform to the Wardrobe protocol, which implement those requirements in their unique ways.
    4. Lastly, create a "picker" function, which given the activity, returns a new wardrobe of the correct type.

    Here's an example to get started:

    struct Clothing {
        let title: String
        let description: String
        let image: String
    }
    
    struct WeatherConditions {
        let temperature: Double
        let isWindy: Bool
        let isRaining: Bool
    }
    
    enum ActivityType {
        case cycling
        case walking
    }
    
    // A Wardrobe is a thing that can provide you with a particular kind of clothing
    protocol Wardrobe {
        // TODO: should these be optional?
        // E.g. imagine if there was `ActivityType.beachParty`
        var head: Clothing { get }
        var baseLayer: Clothing { get }
        var midLayer: Clothing { get }
        var legs: Clothing { get }
        var hands: Clothing { get }
        var feet: Clothing { get }
    
        // Add a way for wardrobes to add other stuff, if necessary.
        var other: [Clothing] { get }
    }
    
    extension Wardrobe {
        func recommendClothing() -> [Clothing] {
            return [head, baseLayer, midLayer, legs, hands, feet] + other
        }
    }
    
    func chooseWardrobe(activity: ActivityType, conditions: WeatherConditions) -> any Wardrobe {
            switch activity {
            case .cycling:
                return CyclingWardrobe(forConditions: conditions)
            case .walking:
                // Example TODO: Create a WalkingWardrobe that conforms to Wardrobe
                fatalError("Implement me!")
            }
        }
    
    struct CyclingWardrobe: Wardrobe {
        let weatherConditions: WeatherConditions
        
        public init(forConditions weatherConditions: WeatherConditions) {
            self.weatherConditions = weatherConditions
        }
    
        private var temperature: Double { return weatherConditions.temperature }
    
        var head: Clothing {
            switch temperature {
            case ..<5:    return Clothing(title: "Head", description: "Winter Cap",    image: "winter_cap")
            case 5..<10:  return Clothing(title: "Head", description: "Skullcap",      image: "light_cap" )
            case 10..<15: return Clothing(title: "Head", description: "Headband",      image: "headband"  )
            default:      return Clothing(title: "Head", description: "Just a Helmet", image: "helmet"    )
            }
        }
        var baseLayer: Clothing {
            switch temperature {
            case ..<10:   return Clothing(title: "Base Layer", description: "Thermal",       image: "thermal_base")
            case 10..<20: return Clothing(title: "Base Layer", description: "Light",         image: "light_base"  )
            default:      return Clothing(title: "Base Layer", description: "No Base Layer", image: "body"        )
            }
        }
    
        var midLayer: Clothing {
            switch temperature {
            case ..<10:   return Clothing(title: "Mid layer", description: "Thermal Jersey", image: "thermal_jersey")
            case 10..<20: return Clothing(title: "Mid layer", description: "Long Sleeved Jersey", image: "long_jersey")
            default:      return Clothing(title: "Mid layer", description: "Jersey", image: "jersey")
            }
        }
    
        var legs: Clothing {
            switch temperature {
            case ..<10:   return Clothing(title: "Legs", description: "Thermal Bibs", image: "thermal_bibs")
            case 10..<18: return Clothing(title: "Legs", description: "Long Bibs",    image: "long_bibs"   )
            default:      return Clothing(title: "Legs", description: "Bib Shorts",   image: "bib_shorts"  )
            }
        }
    
        var hands: Clothing {
            switch temperature {
            case ..<10:   return Clothing(title: "Hands", description: "Insulated Gloves", image: "insulated_gloves")
            case 10..<15: return Clothing(title: "Hands", description: "Long Gloves",      image: "long_gloves"     )
            default:      return Clothing(title: "Hands", description: "Summer Gloves",    image: "gloves"          )
            }
        }
        
        var feet: Clothing {
            switch temperature {
            case ..<3:   return Clothing(title: "Feet", description: "Winter Shoes",      image: "winter_shoes")
            case 3..<7:  return Clothing(title: "Feet", description: "Shoe Covers" ,      image: "covers"      )
            case 7..<15: return Clothing(title: "Feet", description: "Warm Socks Covers", image: "warm_socks"  )
            default:     return Clothing(title: "Feet", description: "Light Socks",       image: "socks"       )
            }
        }
    
        var shell: Clothing? {
            switch temperature {
            case ..<5:   return Clothing(title: "Mid Layer", description: "Insulated Jacket", image: "insulated_jacket")
            case 5..<15: return Clothing(title: "Mid Layer", description: "Light Jacket",     image: "light_jacket"    )
            default:     return nil
            }
        }
    
        var other: [Clothing] {
            var clothing = [Clothing]()
    
            if let shell = self.shell {
                clothing.append(shell)
            }
    
            if weatherConditions.isWindy {
                clothing.append(Clothing(title: "Wind Protection", description: "Windproof Layer", image: "windproof"))
            }
    
            if weatherConditions.isRaining {
                clothing.append(Clothing(title: "Rain Protection", description: "Waterproof Layer", image: "raincoat"))
            }
            
            return clothing
        }
    }
    

    Your caller becomes very simple. It consults the picker to select the correct wardrobe for the activity, and then asks the wardrobe for all the recommended clothing. Le fin.

    let weather = WeatherConditions(temperature: 10, isWindy: false, isRaining: false)
    
    let clothing = chooseWardrobe(activity: .cycling, conditions: weather).recommendClothing()
    
    for article in clothing {
        print("\(article.title): \(article.description)")
    }