Search code examples
swiftipadswiftui

Only have one button active in dynamic list | SwiftUI


I have a list generated from a ForEach loop:

struct TrainingList: View {
    
    @EnvironmentObject var trainingVM: TrainingViewModel
    
    var body: some View {
        VStack(alignment: .leading) {
            Text("Your training sessions")
                .font(.system(size: 35, weight: .semibold, design: .default))
                .padding(.all, 10)
                .foregroundColor(.white)
            Divider()
            ScrollView{
                if(trainingVM.loading){
                    ProgressView("Loading training session").progressViewStyle(CircularProgressViewStyle(tint: .blue))
                }
                LazyVStack {
                    ForEach(trainingVM.trainingList) { training in
                        TrainingCell(training: training)
                    }
                }
            }
            Spacer()
        }
        .background {
            Rectangle()
                .fill(Color(.sRGB, red: 41/255, green: 41/255, blue: 41/255))
                .cornerRadius(10, corners: [.topRight, .bottomRight])
                .shadow(color: .black.opacity(1), radius: 8, x: 6, y: 0)
        }
        .frame(width: 650)
        .zIndex(.infinity)
    }
}

Each TrainingCell has a button that opens an extra panel on the side of it. To indicate which row has the panel opened the button changes its styling:

struct TrainingCell: View {
    
    @EnvironmentObject var trainingVM: TrainingViewModel
    @State var showEvents = false
    
    let training: Training

    var body: some View {
        HStack(spacing: 0) {
            ZStack(alignment: .top) {
                RoundedRectangle(cornerRadius: 10, style: .continuous)
                    .fill(Color(.sRGB, red: 41/255, green: 41/255, blue: 41/255))
                VStack {
                    HStack(spacing: 10) {
                        VStack(alignment: .leading, spacing: 5) {
                            HStack{
                                Text(training.readableDate, style: .date)
                                    .font(.system(size: 25, weight: .semibold, design: .default))
                                    .foregroundColor(.white)
                                Text(" | ")
                                Text(training.readableDate, style: .time)
                                    .font(.system(size: 25, weight: .semibold, design: .default))
                                    .foregroundColor(.white)
                            }
                            VStack(alignment: .leading,spacing: 5){
                                HStack {
                                    HStack(spacing: 5) {
                                        Image(systemName: "person.text.rectangle.fill")
                                            .foregroundColor(Color(.sRGB, red: 10/255, green: 90/255, blue: 254/255))
                                        Text(training.instructor.fullName)
                                            .foregroundColor(.white)
                                    }
                                }
                                HStack{
                                    ForEach(training.students){ student in
                                        HStack(spacing: 5) {
                                            Image(systemName: "person")
                                                .imageScale(.medium)
                                                .foregroundColor(Color(.sRGB, red: 10/255, green: 90/255, blue: 254/255))
                                            Text(student.fullName_shortenedFirstName)
                                                .foregroundColor(.white)
                                        }
                                    }
                                }
                            }
                            .font(.system(size: 20, weight: .regular, design: .default))
                            .foregroundColor(.primary)
                        }
                        .frame(maxHeight: .infinity, alignment: .center)
                        .clipped()
                        Spacer()
                        View_Close_Button(showEvents: $showEvents)
                        
                    }
                    .frame(maxWidth: .infinity, maxHeight: 80, alignment: .top)
                    .padding(.all)
                    .background {
                        RoundedRectangle(cornerRadius: 0, style: .continuous)
                            .fill(Color(.sRGB, red: 41/255, green: 44/255, blue: 49/255))
                            .shadow(color: .black.opacity(1), radius: 5, x: 0, y: 5)
                    }
                }
            }
        }
    }
}

The button code:

struct View_Close_Button: View {
    
    @EnvironmentObject var trainingVM: TrainingViewModel
    @Binding var showEvents: Bool
    
    var body: some View {
        HStack
        {
            Image(systemName: showEvents ? "xmark" : "eye")
                .imageScale(.large)
                .padding(.horizontal, 5)
                .font(.system(size: 17, weight: .regular, design: .default))
            Text(showEvents ? "Close" : "View")
                .padding(.all, 10)
                .font(.system(size: 25, weight: .regular, design: .default))
                .multilineTextAlignment(.leading)
        }
        .onTapGesture {
            withAnimation(.easeIn(duration: 0.3)) {
                showEvents.toggle()
                if(showEvents) {
                    trainingVM.showingEvents = true
                }else{
                    trainingVM.showingEvents = false
                }
            }
        }
        .foregroundColor(.white)
        .background {
            Capsule(style: .continuous)
                .foregroundColor(showEvents ? Color(.sRGB, red: 253/255, green: 77/255, blue: 77/255) : Color(.sRGB, red: 10/255, green: 90/255, blue: 254/255))
                .clipped()
                .frame(maxWidth: 180)
        }
    }
}

Which should result in this: result of code

The only problem I have is that all button can be activated at the same time. How would I go about disabling the rest of the button when one is tapped?

I need the user to only be able to have on of the button displayed as "X Close"

I tought about looping trough other buttons to deactivate them programatically before activating the one that was tapped but I have no clue how

problem with code


Solution

  • You could keep track of the activated button in the parent view.

    If you have some kind of unique identifier per button you could make a variable in the parent view that contains the active identifier.

    You can pass that variable as a binding into the button views and depending on that you can change the views appearance.

    This way there is always just one active button. When a button is clicked you can set the value of the binding variable in the button view with the unique identifier of this button and the other views change automatically.

    On the TrainingList you can define a variable with the active tab:

    @State var activeTab: Int = 0
    

    On TrainingCell you can add this variable as a binding.

    @Binding var activeTab: Int
    

    And you pass it like:

    TrainingCell(training: training, activeTab: $activeTab)
    

    Then on View_Close_Button you can add two variables:

    @Binding var activeTab: Int
    @State var training: Training
    

    And pass it like this on the TrainingCell:

    View_Close_Button(showEvents: $showEvents, activeTab: $activeTab, training: training)
    

    In the View_Close_Button you can use this to get the value and set the styles accordingly:

    Image(systemName: activeTab == training.id ? "xmark" : "eye")
    Text(activeTab == training.id ? "Close" : "View")
    

    And you can set it when the button it tapped:

    .onTapGesture {
        withAnimation(.easeIn(duration: 0.3) {
            activeTab = training.id
        }
    }