Home > Enterprise >  SwiftUI Simplify .onChange Modifier for Many TextFields
SwiftUI Simplify .onChange Modifier for Many TextFields

Time:12-20

I'm searching for a way to simplify/refactor the addition of .onChange(of:) in a SwiftUI view that has MANY TextFields. If a solution were concise, I would also move the modifier closer to the appropriate field rather than at the end of, say, a ScrollView. In this case, all of the .onChange modifiers call the same function.

Example:

.onChange(of: patientDetailVM.pubFirstName) { x in
    changeBackButton()
}
.onChange(of: patientDetailVM.pubLastName) { x in
    changeBackButton()
}
// ten  more times for other fields

I tried "oring" the fields. This does not work:

.onChange(of:
            patientDetailVM.pubFirstName ||
            patientDetailVM.pubLastName
) { x in
    changeBackButton()
}

This is the simple function that I want to call:

func changeBackButton() {
    withAnimation {
        showBackButton = false
        isEditing = true
    }
}

Any guidance would be appreciated. Xcode 13.2.1 iOS 15

CodePudding user response:

Any time you are duplicating code you want to move it one level down so the same code can be reused.

Here is a solution, the parent view will hold a variable that will know if the "name" as a whole has changes.

import SwiftUI
class PatientDetailViewModel: ObservableObject{
    @Published var pubFirstName: String = "John"
    @Published var pubLastName: String = "Smith"
}
struct TrackingChangesView: View {
    @StateObject var vm: PatientDetailViewModel = PatientDetailViewModel()
    ///Variable to know if there is a change
    @State var nameHasChanges: Bool = false
    var body: some View {
        NavigationView{
            NavigationLink("EditView", destination: {
                VStack{
                    TrackingChangesTextFieldView(hasChanges: $nameHasChanges, text: $vm.pubFirstName, titleKey: "first name")
                    TrackingChangesTextFieldView(hasChanges: $nameHasChanges, text: $vm.pubLastName, titleKey: "last name")
                    Button("save", action: {
                        //Once you verify saving the object reset the variable
                        nameHasChanges = false
                    })//Enable button when there are changes
                        .disabled(!nameHasChanges)
                }
                //Or track the single variable here
                .onChange(of: nameHasChanges, perform: {val in
                    //Your method here
                })
                //trigger back button with variable
                .navigationBarBackButtonHidden(nameHasChanges)
            })
            
        }
    }
}
struct TrackingChangesTextFieldView: View {
    //Lets the parent view know that there has been a change
    @Binding var hasChanges: Bool
    @Binding var text: String
    let titleKey: String
    var body: some View {
        TextField(titleKey, text: $text)
            .onChange(of: text, perform: { _ in
                //To keep it from reloading view if already true
                if !hasChanges{
                    hasChanges = true
                }
            })
    }
}
struct TrackingChangesView_Previews: PreviewProvider {
    static var previews: some View {
        TrackingChangesView()
    }
}

CodePudding user response:

Another way you can do this is to make a combined publisher for both pubFirstName and pubLastName. Add following function to your viewModel

var nameChanged: AnyPublisher<Bool, Never> {
        $patientDetailVM.pubFirstName
            .combineLatest($patientDetailVM.pubLastName)
            .map { firstName, lastName in
                if firstName != patientDetailVM.pubFirstName ||
                    lastName != patientDetailVM.pubLastName
                {
                    return true
                } else {
                    return false
                }
            }
            .eraseToAnyPublisher()
    }

and listen to nameChanged publisher on onReceive of your view

.onReceive(of: patientDetailVM.nameChanged) { hasNameChanged in
    changeBackButton()
}

so you can listen to either of first or last name changed

  • Related