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"
}
}
}
}
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.
head
, baseLayer
, midLayer
, and so on. Capture those as
requirements (functions or variables) of a protocol.Wardrobe
protocol, which implement those requirements in their unique ways.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)")
}