Home > Blockchain >  Swift use enum with associated properties in view
Swift use enum with associated properties in view

Time:12-31

I have Enum CategoryType with associated properties and I would like to use it in my view just to list all the cases from enum.

I can's use CaseIterable and Identifiable for my enum with associated properties, that's why it's a bit tricky.

I tried to use computed property allCases to list all the cases but it's still not compiling.

I'm getting these errors:

Generic struct 'Picker' requires that 'CategoryType' conform to 'Hashable'
Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'CategoryType' conform to 'Identifiable'
enum CategoryType: Decodable, Equatable {
    case psDaily, psWeekly, psMonthly
    case unknown(value: String)

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let status = try? container.decode(String.self)
        switch status {
        case "Daily": self = .psDaily
        case "Weekly": self = .psWeekly
        case "Monthly": self = .psMonthly
        default: self = .unknown(value: status ?? "unknown")
        }
    }
    
    var allCases: [CategoryType] {
        return [.psDaily, .psWeekly, .psMonthly]
    }
    
    var rawValue: String {
        switch self {
        case .psDaily: return "Daily"
        case .psWeekly: return "Weekly"
        case .psMonthly: return "Monthly"
        case .unknown: return "Unknown"
        }
    }
}

here is my view:

import SwiftUI

struct CategoryPicker: View {
    @Binding var selection: CategoryType

    var body: some View {
        NavigationStack {
            Form {
                Section {
                    Picker("Category", selection: $selection) {
                        ForEach(CategoryType().allCases) { category in
                            CategoryView(category: category)
                                .tag(category)
                        }
                    }
                }
            }
        }
    }
}

struct CategoryPicker_Previews: PreviewProvider {
    static var previews: some View {
        CategoryPicker(selection: .constant(.psDaily))
    }
}

How to fix these issues or is there another way how to implement it ?

CodePudding user response:

First of all do as the compiler says and have the enum conform to Hashable and Identifiable

extension CategoryType: Hashable, Identifiable {
    var id: String { self.rawValue }
}

Then the allCases property has nothing to do with self so change it to static

static let allCases: [CategoryType] = {
    return [.psDaily, .psWeekly, .psMonthly]
}()

And then change the ForEach loop to use this property

ForEach(CategoryType.allCases) { category in

CodePudding user response:

enums are great but sometimes you need a struct. When there might be unknowns is a perfect example because you can dynamically create objects.

struct CategoryType: Codable, Equatable, CaseIterable, RawRepresentable, Identifiable, Hashable {
    //Conform to Identifiable using the rawValue
    var id: String{
        rawValue
    }
    //Keep the "String" for decoding and encoding
    var rawValue: String
    
    static let psDaily: CategoryType = .init(rawValue: "Daily")
    static let psWeekly: CategoryType = .init(rawValue: "Weekly")
    static let psMonthly: CategoryType =  .init(rawValue: "Monthly")
    
    init(rawValue: String) {
        self.rawValue = rawValue
    }
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        //Dont ignore errors
        let status = try container.decode(String.self)
        switch status {
        case "Daily": self = .psDaily
        case "Weekly": self = .psWeekly
        case "Monthly": self = .psMonthly
            //Create a custom `CategoryType` when the case is unknown.<---
        default:
            self.rawValue = status
        }            
    }
    //CaseIterable conformance
    static var allCases: [CategoryType] = [.psDaily, .psWeekly, .psMonthly]    
}

Then you can use it in a View very similarly to the enum but with the advantage of supporting the unknown.

struct CategoryPicker: View {
    @Binding var selection: CategoryType
    @State var string: String = ""
    @State var cases: [CategoryType] = CategoryType.allCases
    var body: some View {
        NavigationStack {
            Form {
                Text(string)
                
                    .onAppear(){
                        encode()
                    }
                Section {
                    Picker("Category", selection: $selection) {
                        //The loop now works as expected and has individual objects for the "unknown"
                        ForEach(cases) { category in
                            Text(category.rawValue)
                                .tag(category)
                        }
                    }
                }.onAppear(){
                    decode()
                }
            }
        }
    }
    //Sample encoding to demonstrate
    func encode() {
        let encoder: JSONEncoder = .init()
        encoder.outputFormatting = .prettyPrinted
        do{
            let data = try encoder.encode(CategoryType.allCases)
            string = String(data: data, encoding: .utf8) ?? "failed"
        }catch{
            print(error)
        }
    }
    //Sample decoding that inclues "unknown" cases and creates object
    func decode() {
        let json = """
        ["Daily", "Weekly", "Monthly", "Unknown", "Something Unique"]
""".data(using: .utf8)!
        let decoder: JSONDecoder = .init()
        do{
            cases = (try decoder.decode([CategoryType].self, from: json))
        }catch{
            print(error)
        }
    }
}
  • Related