ForEach loop over different variable types swift

Viewed 911

I am struggling with an issue I think (hopefully) has a simple fix that you may be able to help with.

I am trying to run a ForEach loop over a number of variables. To keep things simple here, I have only included two variables but there are many more, hence why I want to use a ForEach loop rather than have the same code repeated for each variable. The variables are each based on different enums.

Intended outcome

I want to run a ForEach loop in my code that loops through an array of variables and extracts the variable's description and associated rawValue.

Variables

var profileEyeColor: EyeColor = EyeColor()
var profileHairColor: HairColor = HairColor()

Enums

enum EyeColor: String, CaseIterable {
    case notSet
    case amber
    case blue
    case brown
    case gray
    case green
    case hazel
    case other
    case witheld
    
    var description: String {
        "Eye Color"
    }
    
    init() {
        self = .notSet
    }
}
enum HairColor: String, CaseIterable {
    case notSet
    case black
    case blond
    case brown
    case auburn
    case red
    case gray
    case white
    case other
    case witheld
    
    var description: String {
        "Hair Color"
    }
    
    init() {
        self = .notSet
    }
}

ForEach loop

ForEach([profileEyeColor, profileHairColor], id: \.self) { item in
   if item != .notSet && item != .witheld {
       print(item.description)
       print(item.rawValue)
   }
}

Actual result

Build fails and xCode errors include:

  • Cannot convert value of type 'profileHairColor' to expected element type 'EyeColor'

Alternative attempted

I have tried to run instead using a for loop rather than ForEach. I'm using SwiftUI, so can't implement a for loop within the body. I've also tried splitting it out into a separate function, but get an error on the function.

func profileDetailsLoop(data: [Any]) -> View {
   ForEach(data, id: \.self) { item in
      if item != .notSet && item != .witheld {
         HStack {
            Text(item.description)
            Text(item.rawValue)
       }
   }
}

Error: Value of protocol type 'Any' cannot conform to 'Hashable'; only struct/enum/class types can conform to protocols

If I replace [Any] with [enum] then I get the following error: Expected element type

And if I replace [Any] with any specific enum type, such as [EyeColor] that doesn't work (and presumably also won't won't work on different enums types).

1 Answers

ForEach has certain requirements and it likes to iterate over a collection of Identifiable items. With that in mind, let's provide a struct Attribute that each of the attributes can provide:

struct Attribute: Identifiable {
    let name: String
    let value: String
    let id = UUID()
}

You could manually add a computed var to every enum that provides this attribute, but that would be a lot of repeated code.

Instead, let's note that all of the attributes provide a rawValue and a description and that is all that's really needed to initialize an Attribute.

Define this protocol:

protocol AttributeType {
    var rawValue: String { get }
    var description: String { get }
}

And then create this extension to the protocol that returns an Attribute:

extension AttributeType {
    var attribute: Attribute? {
        guard self.isSet else { return nil }
        return Attribute(name: self.description, value: self.rawValue)
    }
    
    var isSet: Bool { return !["notSet", "witheld"].contains(self.rawValue) }
}

Finally, have all of your enums adopt AttributeType:

enum EyeColor: String, CaseIterable, AttributeType {
    case notSet
    case amber
    ...
    
    var description: String {
        "Eye Color"
    }
    
    init() {
        self = .notSet
    }
}


enum HairColor: String, CaseIterable, AttributeType {
    case notSet
    case black
    ...
    
    var description: String {
        "Hair Color"
    }
    
    init() {
        self = .notSet
    }
}

Then you can get an Attribute for every attribute by accessing the .attribute property.

ForEach([profileEyeColor.attribute, profileHairColor.attribute].compactMap { $0 }) { attribute in
    HStack {
        Text(attribute.name)
        Text(attribute.value)
    }
}

To avoid typing .attribute for every attribute in the array, you can write it like this:

ForEach([profileEyeColor as AttributeType, profileHairColor].compactMap { $0.attribute }) {

which will save a lot of typing for a long attribute list.

Notes:

  • .attribute returns an optional Attribute. It is nil if the attribute is .notSet or .witheld.
  • .compactMap { $0 } is used to remove the nil values and create an [Attribute] for ForEach to iterate over.
  • Since Attribute is Identifiable, it provides the id so it isn't necessary to explicitly state that.

Related