Search code examples
swiftuiswiftui-environment

SwiftUI: How to pass an argument from one view to the next with dynamically generated buttons?


Problem:

  1. I am unable to force my alpha, beta, or gamma buttons to turn ON when an input parameter is passed from Landing.swift.
  2. I do not understand why when onAppear fires in the stack, the output becomes:
gamma is the title
beta is the title
alpha is the title
gamma is the title
beta is the title
alpha is the title

Confused -> Why is this outputting 2x when the ForEach loop has only 3 elements inside?

Background: I am trying to pass a parameter from one view (Landing.swift) to another (ContentView.swift) and then based on that parameter force the correct button (in ContentView) to trigger an ON state so it's selected. I have logic shown below in ButtonOnOff.swift that keeps track of what's selected and not.

For instance, there are 3 buttons in ContentView (alpha, beta, and gamma) and based on the selected input button choice from Landing, the respective alpha, beta, or gamma button (in ContentView) should turn ON.

I am dynamically generating these 3 buttons in ContentView and want the flexibility to extend to possibly 10 or more in the future. Hence why I'm using the ForEach in ContentView. I need some help please understanding if I'm incorrectly using EnvironmentObject/ObservedObject or something else.

Maintaining the ON/OFF logic works correctly with the code. That is, if you manually press alpha, it'll turn ON but the other two will turn OFF and so forth.

Thanks for your help in advance! :)

Testing.swift

import SwiftUI

@main

struct Testing: App {

    @StateObject var buttonsEnvironmentObject = ButtonOnOff()

    var body: some Scene {
        WindowGroup {
            Landing().environmentObject(buttonsEnvironmentObject)
        }
    }
}

Landing.swift

import SwiftUI

struct Landing: View {
    
    @State private var tag:String? = nil
    
    var body: some View {
        NavigationView {
            ZStack{
                HStack{
                    NavigationLink(destination: ContentView(landingChoice:tag ?? ""), tag: tag ?? "", selection: $tag) {
                        EmptyView()
                    }
                    Button(action: {
                        self.tag = "alpha"
                    }) {
                        HStack {
                            Text("alpha")
                        }
                    }
                    Button(action: {
                        self.tag = "beta"
                    }) {
                        HStack {
                            Text("beta")
                        }
                    }
                    Button(action: {
                        self.tag = "gamma"
                    }) {
                        HStack {
                            Text("gamma")
                        }
                    }
                }
                .navigationBarHidden(true)
            }
            .navigationViewStyle(StackNavigationViewStyle())
        }
    }
}

ContentView.swift

import SwiftUI

struct ContentView: View {

    var btnName:String
    @EnvironmentObject var buttonEnvObj:ButtonOnOff
    
    init(landingChoice:String){
        self.btnName = landingChoice
        print("\(self.btnName) is the input string")
    }

    var body: some View {
        VStack{
            Form{
                Section{
                    ScrollView(.horizontal, showsIndicators: false) {
                        HStack(spacing:10) {
                            ForEach(0..<buttonEnvObj.buttonNames.count) { index in
                                BubbleButton(label: "\(buttonEnvObj.buttonNames[index])")
                                .padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 0))
                                 .onAppear {
                                     print("\(buttonEnvObj.buttonNames[index]) is the title")
                                }
                            }
                        }
                    }.frame(height: 50)
                }
            }
        }
    }
}

struct BubbleButton: View{

    @EnvironmentObject var buttonBrandButtons:ButtonOnOff
    var label: String
    
    var body: some View{
        HStack{
            Button(action: {
                print("Button action")
                buttonBrandButtons.changeState(buttonName: self.label)
                
            }) {
                ZStack {
                    VStack{
                        HStack {
                            Spacer()
                            Text(label)
                                .font(.system(size: 12,weight:.regular, design: .default))
                                .foregroundColor(buttonBrandButtons.buttonBrand[self.label]! ? Color.white : Color.gray)
                            Spacer()
                        }
                    }
                    .frame(height:30)
                    .fixedSize()
                }
            }
            .background(buttonBrandButtons.buttonBrand[self.label]! ? Color.blue : .clear)
            .cornerRadius(15)
            .overlay(buttonBrandButtons.buttonBrand[self.label]! ?
                     RoundedRectangle(cornerRadius: 15).stroke(Color.blue,lineWidth:1) : RoundedRectangle(cornerRadius: 15).stroke(Color.gray,lineWidth:1))
            .animation(.linear, value: 0.15)
        }
    }
}

ButtonOnOff.swift

import Foundation

class ButtonOnOff:ObservableObject{
    
    var buttonNames = ["alpha","beta","gamma"]
    
    @Published var buttonBrand:[String:Bool] = [
        "alpha":false,
        "beta":false,
        "gamma":false
    ]
    
    func changeState(buttonName:String) -> Void {
        for (key,_) in buttonBrand{
            if key == buttonName && buttonBrand[buttonName] == true{
                buttonBrand[buttonName] = false
            } else{
                buttonBrand[key] = (key == buttonName) ? true : false
            }
        }
        print(buttonBrand)
    }
}

Solution

  • For a short answer just add

        .onAppear(){
            buttonEnvObj.changeState(buttonName: self.btnName)
        }
    

    to ContentView that will highlight the button that was selected.

    As for a solution that can be expanded at will. I would suggest a single source of truth for everything and a little simplifying.

    struct Landing: View {
        @EnvironmentObject var buttonEnvObj:ButtonOnOff
        
        @State private var tag:String? = nil
        
        var body: some View {
            NavigationView {
                ZStack{
                    HStack{
                        NavigationLink(destination: ContentView(), tag: tag ?? "", selection: $tag) {
                            EmptyView()
                        }
                        //Put your buttons here
                        HStack{
                            //Use the keys of the dictionary to create the buttons
                            ForEach(buttonEnvObj.buttonBrand.keys.sorted(by: <),  id: \.self){ key in
                                //Have the button set the value when pressed
                                Button(action: {
                                    self.tag = key
                                    buttonEnvObj.changeState(buttonName: key)
                                }) {
                                    Text(key)
                                }
                            }
                        }
                    }
                    .navigationBarHidden(true)
                }
                .navigationViewStyle(StackNavigationViewStyle())
            }
        }
    }
    
    struct ContentView: View {
        
        @EnvironmentObject var buttonEnvObj:ButtonOnOff
        
        var body: some View {
            VStack{
                Form{
                    Section{
                        ScrollView(.horizontal, showsIndicators: false) {
                            HStack(spacing:10) {
                                //Change this to use the dictionary
                                ForEach(buttonEnvObj.buttonBrand.sorted(by: {$0.key < $1.key }), id:\.key) { key, value in
                                    BubbleButton(key: key, value: value)
                                        .padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 0))
                                        .onAppear {
                                            print("\(value) is the title")
                                        }
                                }
                            }
                        }.frame(height: 50)
                    }
                }
            }
        }
    }
    
    struct BubbleButton: View{
        
        @EnvironmentObject var buttonBrandButtons:ButtonOnOff
        var key: String
        var value: Bool
        
        var body: some View{
            HStack{
                Button(action: {
                    print("Button action")
                    buttonBrandButtons.changeState(buttonName: key)
                    
                }) {
                    ZStack {
                        VStack{
                            HStack {
                                Spacer()
                                Text(key)
                                    .font(.system(size: 12,weight:.regular, design: .default))
                                    .foregroundColor(value ? Color.white : Color.gray)
                                Spacer()
                            }
                        }
                        .frame(height:30)
                        .fixedSize()
                    }
                }
                .background(value ? Color.blue : .clear)
                .cornerRadius(15)
                .overlay(value ?
                         RoundedRectangle(cornerRadius: 15).stroke(Color.blue,lineWidth:1) : RoundedRectangle(cornerRadius: 15).stroke(Color.gray,lineWidth:1))
                .animation(.linear, value: 0.15)
            }
        }
    }
    
    class ButtonOnOff:ObservableObject{
        //Get rid of this so you can keep the single source
        //var buttonNames = ["alpha","beta","gamma"]
        
        //When you want to add buttons just add them here it will all adjust
        @Published var buttonBrand:[String:Bool] = [
            "alpha":false,
            "beta":false,
            "gamma":false
        ]
        
        func changeState(buttonName:String) -> Void {
            for (key,_) in buttonBrand{
                if key == buttonName && buttonBrand[buttonName] == true{
                    buttonBrand[buttonName] = false
                } else{
                    buttonBrand[key] = (key == buttonName) ? true : false
                }
            }
            print(buttonBrand)
        }
    }