4

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!

1 Answer 1

3
  • @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)
            }
        }
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

Thank you so much! So it was simply the fact I was using a Bool and setting the value within the ForEach as opposed to within the view it's self?
Sorry I didnt get you, somehow I add the reasons as well as the fixed answer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.