Norman
Norman

Reputation: 3205

Create a list from enum

I'd like to be able to create a List from an enum that conforms to CaseIterable and CustomStringConvertible e.g.

public enum HairColor: Int, Codable, CaseIterable, CustomStringConvertible {
    public var description: String {
        switch self {
        case .black:
            return "Black"
        case .blond:
            return "Blond"
        case .brown:
            return "Brown"
        case .red:
            return "Red"
        case .grey:
            return "Gray"
        case .bald:
            return "Bald"
        }
    }
    case blond, brown, black, red, grey, bald
}
struct ContentView: View {
    var body: some View {
        SwiftUIHelpers.enumToList(HairColor)
    }
}

This is the approach I've tried but I get the error: "Cannot convert value of type 'Text' to closure result type '_"


struct SwiftUIHelpers {
    static func enumToList<T: CaseIterable, RandomAccessCollection>(_ a: T) -> some View {
        List {
            ForEach(a, id: \.rawValue) { (o: CustomStringConvertible) in
                Text(o.description)
            }
        }
    }
}

What is the error on my ways?!?

Upvotes: 0

Views: 649

Answers (2)

Asperi
Asperi

Reputation: 257563

Here is working solution. Tested with Xcode 11.4 / iOS 13.4.

struct SwiftUIHelpers {
    static func enumToList<T: CaseIterable>(_ t: T.Type) -> some View 
           where T.AllCases: RandomAccessCollection, T: Hashable & CustomStringConvertible {
        List {
            ForEach(t.self.allCases, id: \.self) { o in
                Text(o.description)
            }
        }
    }
}

struct ContentView: View {
    var body: some View {
        SwiftUIHelpers.enumToList(HairColor.self)
    }
}

public enum HairColor: Int, Codable, Hashable, CaseIterable, CustomStringConvertible {
    public var description: String {
        switch self {
        case .black:
            return "Black"
        case .blond:
            return "Blond"
        case .brown:
            return "Brown"
        case .red:
            return "Red"
        case .grey:
            return "Gray"
        case .bald:
            return "Bald"
        }
    }
    case blond, brown, black, red, grey, bald
}

Update: Having played with this more I've found that the following extension can be much more helpful

extension CaseIterable where Self.AllCases: RandomAccessCollection, Self: Hashable & CustomStringConvertible {
    static func toForEach() -> some View  {
        ForEach(Self.allCases, id: \.self) { o in
            Text(o.description).tag(o)
        }
    }
}

because gives wider reuse possibility, like

List { HairColor.toForEach() }

and this

Form { HairColor.toForEach() }

and

demo

struct DemoHairColorPicker: View {
    @State private var hairColor: HairColor = .red
    var body: some View {
        VStack {
            Text("Selected: \(hairColor.description)")
            Picker(selection: $hairColor, label: Text("Hair")) { HairColor.toForEach() }
        }
    }
}

and of course in any stack VStack { HairColor.toForEach() }

Upvotes: 1

andrewbuilder
andrewbuilder

Reputation: 3791

Not really an answer to all parts of your question - only the first part - but offered here as an alternative...

Might be worth considering the use of @EnvironmentObject for a @Published property? I used this to populate a sidebar style menu for a macOS target.

Step 1:

Use your enum. My enum is written a little differently to yours but I thought to leave it that way because it provides an alternate construction... but with the same outcome.

(Conforming to CaseIterable here allows us to use the .allCases method in Step 2.)

enum HairColor: Int, CaseIterable {

    case blond = 0, brown, black, red, grey, none

    var description: String {

        switch self {

        case .blond: return "Blond"
        case .brown: return "Brown"
        case .black: return "Black"
        case .red:   return "Red"
        case .grey:  return "Grey"
        case .none:  return "Bald"
        }
    }
}

Step 2:

Create a struct for your model and include a static property that maps all cases of your HairColor enum.

(Conforming to Identifiable here allows us to use the cleaner ForEach syntax in Step 4 - that is - use ForEach(appData.hairColors) in lieu of ForEach(appData.hairColors, id: \.id)).

import SwiftUI

struct Hair: Codable, Hashable, Identifiable {

    var id: Int
    var name: String

    init(id: Int, name: String) {
        self.id = id
        self.name = name
    }

    static var colors: [Hair] {
        return HairColor.allCases.map({ Hair(id: $0.rawValue, name: $0.description ) })
    }
}

Step 3:

Create a class that conforms to ObservableObject and that contains a @Published wrapped property to allow you to broadcast your HairColor via @EnvironmentObject.

import Combine // <- don't forget this framework import!
import SwiftUI

final class AppData: ObservableObject {

    @Published var hairColors = Hair.colors
}

Step 4:

Use in a View.

struct HairList: View {

    @EnvironmentObject var appData: AppData

    @State var selectedHair: Hair?

    var body: some View {

        VStack(alignment: .leading) {

            Text("Select...")
                .font(.headline)

            List(selection: $selectedHair) {

                ForEach(appData.hairColors) { hairColor in

                    Text(hairColor.name).tag(hairColor)
                }
            }
            .listStyle(SidebarListStyle())
        }
        .frame(minWidth: 100, maxWidth: 150)
        .padding()
    }
}

Step 5:

Remember to inject the environment object into the preview to make the preview usable.

struct HairList_Previews: PreviewProvider {

    static var previews: some View {

        HairList(selectedHair: .constant(AppData().hairColors[1]))
            .environmentObject(AppData())
    }
}

Upvotes: 0

Related Questions