SwiftUser
SwiftUser

Reputation: 615

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!

Upvotes: 4

Views: 1410

Answers (1)

YodagamaHeshan
YodagamaHeshan

Reputation: 6548

  • @State is used for private changes, inside withing a view

  • to update changes back and forth from sub view you have to use @Binding

  • we can access/pass binding of @State using $ , ex :- $yourStateVariable

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

Upvotes: 3

Related Questions