Search code examples
swiftuiparent-childpickerswiftui-form

Swift UI how to retrieve state from child view


I have an app that has three views currently, a ParentView which is a simple List that the user can add entries to, a ContentView which is a modal form which collects information to add the list in the ParentView, and a ChildPickerView which refactored out some code in the ContentView that was getting the dreaded 'the compiler is unable to type-check this expression in reasonable time' message.

My problem is the ChildPickerView contains the text that needs to be saved. It is updated as the 3 pickers change. When Save is tapped in the ContentView I am trying to save the composite text returned by the pickers in the ChildPickerView.

I am a newbie to SwiftUI so may be missing something obvious, but here is my code. I have tried a @State object in the ContentView and @Binding in the ChildPickerView, but my problem is building the composite Text view that keeps track of the state of the pickers that I eventually want to save and display in the ParentView List.

Here is my code (for simplicity sake I have only included the code for the ContentView and the ChildPickerView:

struct ContentView: View {
    @State private var isCustomWidget: Bool = false
    @State private var customWidgetName: String = ""
    @State private var standardWidgetName: String = ""
    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("WIDGET DETAILS")) {
                    Toggle(isOn: self.$isCustomWidget) {
                        Text("Custom")
                    }
                    if !self.isCustomWidget {
                        ChildPickerView(standardWidgetName: $standardWidgetName)
                    }
                    else
                    {
                        TextField("enter custom widget name", text: $customWidgetName)
                    }
                }
                
                Button(action: {
                    // we want to save either the standard widget name or custom widget name to our store
                    let str = self.isCustomWidget ? customWidgetName : standardWidgetName
                     print("saving \(str)")
                    
                })
                {
                    Text("Save")
                }
                .navigationBarTitle("Add Widget")
            }
        }
    }
    
}

struct ChildPickerView: View {
    @State var selectedLengthIndex = 0
    @State var selectedUnitsIndex = 0
    @State var selectedItemIndex = 0
    @Binding var standardWidgetName: String

    var length: [String] =  ["1","5","10","20","40","50","100","200"]
    var units: [String] =  ["Inches", "Centimeters"]
    var items: [String] = ["Ribbon","Cord","Twine", "Rope"]
        
    var body: some View {
        List {
            HStack {
                Text("Widget Name")
                Spacer()
                let name =
                    "\(length[selectedLengthIndex])" + " " + " \(units[selectedUnitsIndex])" + " " + " \(items[selectedItemIndex])"
                standardWidgetName = name  //generates error on HStack -> Type '()' cannot conform to 'View'; only struct/enum/class types can conform to protocols
                Text(name)
            }
            
            Picker(selection: $selectedLengthIndex, label: Text("Length")) {
                ForEach(0 ..< length.count) {
                    Text(self.length[$0]).tag($0)
                }
            }
            
            Picker(selection: $selectedUnitsIndex, label: Text("Unit of Measure")) {
                ForEach(0 ..< units.count) {
                    Text(self.units[$0]).tag($0)
                }
            }
            
            Picker(selection: $selectedItemIndex, label: Text("Item Type")) {
                ForEach(0 ..< items.count) {
                    Text(self.items[$0]).tag($0)
                }
            }
        }
    }
}

Solution

  • In SwiftUI, you want a single source of truth for your data, and then all views read and write to that data.

    @State is a bit like private, in that it creates a new source of truth that can only be used by that view (and its subviews).

    What you want is to have the data in the Parent (ContentView in this case) and then pass the data that you want the children to update to them.

    You can do this using @Binding:

    public struct ChildPickerView : View {
    
        var selectedText: Binding<String>
    
        var body: some View { }
    }
    

    And then passing that binding from the parent using $:

    public struct ContentView : View {
        @State var selectedText = ""
    
        var body: some View {
          ChildViewItem($selectedText) 
        }
    }
    

    You can also use EnvironmentObject to avoid passing data from view to view.