3

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).

2
  • Are all enums the same or is it just a coincidence? Commented Oct 11, 2020 at 12:04
  • Coincidental (although even the two enums above do have different cases) because the two in the example are hair/eye color. Other enums include things like status, preferences, visibility, etc.... Commented Oct 11, 2020 at 12:23

1 Answer 1

4

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.

Sign up to request clarification or add additional context in comments.

4 Comments

Amazing, thanks so much. This has solved my problem. I would note that the only change I needed to make in order for the code to compile is inserting a "$" into compactMap {$0} instead of just compactMap {0}
Oops, that was a typo! You could also avoid typing .attribute for every attribute by writing it like this: ForEach([profileEyeColor as AttributeType, profileHairColor].compactMap { $0.attribute }). By marking only the first attribute as as AttributeType, Swift is able to figure out the type of the array and then .attribute can be applied in the compactMap.
Amazing thank you for that tip on avoiding .attribute every time. I may have quickly found an even easier way. by defining a an array first... eg: let attributeArray:[AttributeType] = [profileEyeColor, profileHairColor] you avoid needing to do "as AttributeType" on the first item in the ForEach loop. That makes my code easier to read and follow. Honestly, thanks so much! amazing help
Yes, defining the array ahead of time is a great way to do it. I was assuming you wanted to create the array on the fly in the ForEach and was showing how that could be done. Either way, applying .attribute in the compactMap keeps the extra code to a minimum.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.