Search code examples
swiftuiviewrefactoringcopy-paste

SwiftUI - common view with "binding" variable


I have main ImportDataView with List of few pickers. Pickers are similar but I need to use different "selection". Each Picker is in separate var (some View) of ImportDataView extension. Is it possible to use one picker view instead of 3 (or more) and pass/bind different "selection" variable (from ImportDataViewModel)?

Thanks in advance.

import SwiftUI

class ImportDataViewModel: ObservableObject {
   
    @Published var field1Index: Int?
    @Published var field2Index: Int?
    @Published var fieldNIndex: Int?
    
    func getFieldNamesFromCSV() -> [String] {
        return ["fieldFromFile1", "fieldFromFile2", "fieldFromFileN"]
    }
}

struct ImportDataView: View {
    
    @Environment(\.dismiss) var dismiss
    @EnvironmentObject var itVM: ImportDataViewModel
    
    var body: some View {
        NavigationView {
            List {
                HStack {
                    Text("Field 1  *")
                    Spacer()
                    chooseField1View
                }
                HStack {
                    Text("Field 2 *")
                    Spacer()
                    chooseField2View
                }
                HStack {
                    Text("Field N *")
                    Spacer()
                    chooseFieldNView
                }
            }
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Back", role: .cancel) {
                        dismiss()
                    }
                }
            }
        }
        
    }
}

extension ImportDataView {
    @ViewBuilder
    private var chooseField1View: some View {
        Picker(selection: $itVM.field1Index, label: EmptyView()) {
            ForEach(itVM.getFieldNamesFromCSV().indices, id: \.self) { index in
                Text(itVM.getFieldNamesFromCSV()[index]).tag(index as Int?)
            }
        }
    }
    
    @ViewBuilder
    private var chooseField2View: some View {
        Picker(selection: $itVM.field2Index, label: EmptyView()) {
            ForEach(itVM.getFieldNamesFromCSV().indices, id: \.self) { index in
                Text(itVM.getFieldNamesFromCSV()[index]).tag(index as Int?)
            }
        }
    }
    
    @ViewBuilder
    private var chooseFieldNView: some View {
        Picker(selection: $itVM.fieldNIndex, label: EmptyView()) {
            ForEach(itVM.getFieldNamesFromCSV().indices, id: \.self) { index in
                Text(itVM.getFieldNamesFromCSV()[index]).tag(index as Int?)
            }
        }
    }
}

struct ImportDataView_Previews: PreviewProvider {
    static var previews: some View {
        ImportDataView()
            .environmentObject(ImportDataViewModel())
            .preferredColorScheme(.light)
    }
}

I use separate "some View" variable for each Picker but understand that it is wrong way with copy/paste.

I expect to have only one "common" extension "some View" var and use it for all importing fields like this:

HStack {
   Text("Field 1  *")
   Spacer()
   chooseFieldCommonView(selectionFieldIndex: $itVM.field1Index)
}
HStack {
   Text("Field 2  *")
   Spacer()
   chooseFieldCommonView(selectionFieldIndex: $itVM.field2Index)
}
HStack {
   Text("Field N  *")
   Spacer()
   chooseFieldCommonView(selectionFieldIndex: $itVM.fieldNIndex)
}

Solution

  • Split the ChooseFieldView to a new file with input are index (which binds with view model outside) and string array as content

    struct ChooseFieldView: View {
        
        @Binding var index: Int
        var values: [String]
        
        var body: some View {
            Picker(selection: $index, label: EmptyView()) {
                ForEach(values.indices, id: \.self) { index in
                    Text(values[index]).tag(index)
                }
            }
        }
    }
    

    Then in ImportDataView edit like this:

    struct ImportDataView: View {
        
        @Environment(\.dismiss) var dismiss
        @EnvironmentObject var itVM: ImportDataViewModel
        
        var body: some View {
            NavigationView {
                List {
                    HStack {
                        Text("Field 1  *")
                        Spacer()
                        ChooseFieldView(index: $itVM.field1Index,
                                        values: itVM.getFieldNamesFromCSV())
                    }
                    HStack {
                        Text("Field 2 *")
                        Spacer()
                        ChooseFieldView(index: $itVM.field2Index,
                                        values: itVM.getFieldNamesFromCSV())
                    }
                    HStack {
                        Text("Field N *")
                        Spacer()
                        ChooseFieldView(index: $itVM.fieldNIndex,
                                        values: itVM.getFieldNamesFromCSV())
                    }
                }
                .toolbar {
                    ToolbarItem(placement: .cancellationAction) {
                        Button("Back", role: .cancel) {
                            dismiss()
                        }
                    }
                }
            }
            
        }
    }
    

    Result:

    enter image description here