swiftenumsswiftui

SwiftUI - View using Enum not updating


Firstly, I want to say the value updates, as I print the value(s) in the console and sure, tapping each option prints as expected. However, for UI purposes, I have added a few visual components/styles to help with indicating the current selection.

My enum:

enum Gender : Int, CaseIterable {
    case men = 0
    case women = 1
  

    private var cases: [String]  {
        ["Men", "Women"]
    }
   
    func toString() -> String {
        cases[self.rawValue]
    }
}

This is the view that helps with the logic for displaying the data and indexing the data

struct GenderTabMenuIndicator: View {
    var category: Gender
    var body: some View {
        HStack {
            ForEach(0..<Gender.allCases.count) { cat in
                GenderTabMenuIndicatorItem(category: Gender.allCases[cat], isActive: Gender.allCases[cat] == category)
                
            }
        }.frame(width: UIScreen.main.bounds.width * 0.75)
    }
}

And this is simply the view. However, the isActive does not seem to switch from the initial selection/value.

struct GenderTabMenuIndicatorItem: View {
    @State var category: Gender
    @State var isActive: Bool
    var body: some View {
        VStack(spacing: 0) {
            Text(category.toString().uppercased())
                .onTapGesture {
                    print("tapped")
                    print(category.toString())
                }
                .font(.system(size: 18, weight: isActive ? .bold : .light))
                .frame(maxWidth: .infinity)
                .layoutPriority(1)
            if isActive {
                Rectangle()
                    .frame(width: 50, height: 2, alignment: .center)

            }
        }.foregroundColor(Color(SYSTEM_FONT_COLOUR))
    }
}

This is how I'm declaring/using all these components in my actual view:

@State private var selected_tab: Gender = .men

VStack {
GenderTabMenuIndicator(category: selected_tab)
}

I don't know if it's the ForEach loop, but that at the same time does print the corresponding case that's passed. I have used @State where I can to update the view, but to no luck.

Any help would be appreciated!


Solution

  • Here is the Fixed answer:

    // If you change the type of this `enum` to `String`, you can use 
    // `.rawValue.capitalized` instead of manually mapping all cases 
    // to create a `toString` method/computed property. But assuming 
    // you absolutely need to have `Int` as the `RawValue` type, you 
    // should instead utilize a switch statement because it gives you 
    // compile-time checking/safety if the order of these values ever 
    // changes or if new cases are ever added, as I have done here.
    enum Gender : Int, CaseIterable {
        case men = 0
        case women = 1
        
        func toString() -> String {
            switch self {
                case .men:   "Men"
                case .women: "Women"
            }
        }
    }
    
    struct ContentView: View {
        @State private var selectedGender: Gender = .men
        
        var body: some View {
            VStack {
                Text("Selected: \(selectedGender.toString())")
                GenderTabMenuIndicator(
                    selectedGender: $selectedGender
                )
            }
        }
    }
    
    // If you were to change the `Gender` `enum` `RawValue` type to `String`, 
    // you could then make this view type reusable by making it take a generic type 
    // and then it would work for any `enum` with a `String` as its `RawValue` type.
    struct GenderTabMenuIndicator: View {
        @Binding var selectedGender: Gender
        
        var body: some View {
            HStack {
                ForEach(Gender.allCases) { gender in
                    GenderTabMenuIndicatorItem(
                        gender: gender, 
                        selection: $selectedGender
                    )
                }
            }   // NOTE: Apple advises not to use UIScreen for SwiftUI
                .frame(width: UIScreen.main.bounds.width * 0.75)
        }
    }
    
    // Same here:
    // If you were to change the `Gender` `enum` `RawValue` type to `String`, 
    // you could then make this view type reusable by making it take a generic type 
    // and then it would work for any `enum` with a `String` as its `RawValue` type.
    struct GenderTabMenuIndicatorItem: View {
        var category: Gender
        @Binding var selection: Gender
        
        var isSelected: Bool { selection == gender }
        
        var body: some View {
            VStack(spacing: 0) {
                Text(gender.toString().uppercased())
                    .onTapGesture {
                        selection = category
                    }
                    .font(.system(
                        size: 18, 
                        weight: isSelected ? .bold : .light
                    ))
                    .frame(maxWidth: .infinity)
                    .layoutPriority(1)
    
                if isSelected {
                    Rectangle()
                        .frame(width: 50, height: 2, alignment: .center)
                }
            }
        }
    }
    

    Example solution using generics:

    enum Gender: String, CaseIterable {
        case man
        case woman
    }
    
    enum MaritalStatus: String, CaseIterable {
        case single
        case married
        case separated
        case divorced
    }
    
    struct ContentView: View {
        @State private var gender = Gender.man
        @State private var maritalStatus = MaritalStatus.single
        
        var body: some View {
            VStack {
                Text("Gender: \(gender.rawValue.capitalized)")
                TabMenuIndicator(selectedItem: $gender)
    
                Text("Marital Status: \(maritalStatus.rawValue.capitalized)")
                TabMenuIndicator(selectedItem: $maritalStatus)
            }
        }
    }
    
    struct TabMenuIndicator<ItemType: RawRepresentable, CaseIterable, Equatable>: View {
        @Binding var selectedItem: ItemType
        
        var body: some View {
            HStack {
                ForEach(ItemType.allCases) { anItem in
                    TabMenuItem(
                        item: anItem, 
                        selectedItem: $selectedItem
                    )
                }
            }   // NOTE: Apple advises not to use UIScreen for SwiftUI
                .frame(width: UIScreen.main.bounds.width * 0.75)
        }
    }
    
    struct TabMenuItem<ItemType: RawRepresentable, CaseIterable, Equatable>: View {
        var item: ItemType
        @Binding var selectedItem: ItemType
        
        var isSelected: Bool { selectedItem == item }
        
        var body: some View {
            VStack(spacing: 0) {
                Text(item.rawValue.capitalized)
                    .onTapGesture {
                        selectedItem = item
                    }
                    .font(.system(
                        size: 18, 
                        weight: isSelected ? .bold : .light
                    ))
                    .frame(maxWidth: .infinity)
                    .layoutPriority(1)
    
                if isSelected {
                    Rectangle()
                        .frame(width: 50, height: 2, alignment: .center)
                }
            }
        }
    }