swiftswiftuifoundation

Swift use enum with associated properties in view


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 ?


Solution

  • 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)
            }
        }
    }