Patrick Michiels
Patrick Michiels

Reputation: 509

Determine String changes in SwiftUI with Binding

This is my first question on stackoverflow and I started working with iOS, Swift and SwiftUI only a month ago. Please be understanding and tell me if I did something wrong.

I would like to determine if a String changed within a struct that has a bound String(@Binding) to it. I copy the original String to a second string on start (within body). Both Strings are always the same, as if the reference was copied, instead of the value. As far as I understood the documentation on String a String is always copied using its value. Please correct me if I am wrong. Here is some example code:

struct EditTextSheet: View {

var title: String
@Binding var showSheetView: Bool
@Binding var userConfirmedChange: Bool
@Binding var textToEdit: String

var body: some View
{
    // use to determine if user made any changes to text
    let originalText: String = textToEdit
    
    
    NavigationView
    {
        Form
        {
            Section(header: Text(title))
            {
                TextField(textToEdit, text: $textToEdit)
            }
            
        }
        
        .navigationBarTitle("", displayMode: .inline)
        // buttons within navigation Bar OK top right, cancel top left. OK also sets userConfirmedChange to true
        
        // Cancel BUTTON
        .navigationBarItems(leading: Button(action: {
            self.showSheetView = false
            
        }) {
            Text("Cancel")
        }, trailing: Button(action: {
            
            // OK BUTTON
            if textToEdit.isEmpty
            {
                // tell user that String cannot be empty
                ...
            }
            else
            {



                // ***BELOW IS ALWAYS THE SAME***
                // if "Test" was passed as textToEdit and user then
                // changes textToEdit via the TextField to "Test1234" 
                // below if clause results in "Test1234" == "Test1234"
                // what I want is "Test" == "Test1234"




                if textToEdit == originalText
                {
                    self.userConfirmedChange = false
                    self.showSheetView = false
                }
                else
                {
                    self.userConfirmedChange = true
                    self.showSheetView = false
                }
                
            }
            
        }) {
            Text("OK")
                .bold()
        })
        
    }
    
    
}

}

this struct is a sheet, that gets called like this:

    ...
    @State var showEditSheet = false
    @State var userConfirmedChange = false
    @State var editText: String = ""
    ...
   
    Button(action: {
            editText = "Test"
            userConfirmedChange = false
            showEditSheet.toggle()
        }) {
            Text ("Edit Test")
        }.sheet(isPresented: $showEditSheet, onDismiss: {
            if userConfirmedChange
            {
                print("\(editText)")
            }
            else
            {
                print("cancelled")
            }
        }) {
            EditTextSheet(title: "some title", showSheetView: $showEditSheet, userConfirmedChange: $userConfirmedChange, textToEdit: $editText)
        }

         ...

Upvotes: 2

Views: 2226

Answers (2)

davidev
davidev

Reputation: 8517

You are facing a problem with State updates, which override your old value. You are passing textToEdit as a Binding to your EditTextSheet View. Whenever this value changes, you Parent View with that State gets reloaded. Hence, it will reload your EditTextSheet and sets originalText to the current value. Hence, originalText and textToEdit will always be the same. That's why you compare them and they are always the same. Strings are value types, not reference types. But in your case, they always have the same value.

To fix this issue you will need a local State variable in your EditTextSheet, which takes the value of the Binding. This State is local in your view and will store the new value. When running your action, you will compare your unchanged Binding with that State (Which represents the TextField) and run your action. Here is a possible change to your code

struct EditTextSheet: View {

    var title: String
    @Binding var showSheetView: Bool
    @Binding var userConfirmedChange: Bool
    @Binding var textToEdit: String
    @State var actualText : String//<< this is the local State which will be used for the TextField

    init(title: String, showSheetView: Binding<Bool>, userConfirmedChange: Binding<Bool>, textToEdit: Binding<String>) {
        self.title = title
        self._showSheetView = showSheetView
        self._userConfirmedChange = userConfirmedChange
        self._textToEdit = textToEdit
        self._actualText = State(initialValue: textToEdit.wrappedValue) //<< create new State with the value of the Binding, now it will be initialized with "Test"
    }

Then your TextField looks like that

TextField(textToEdit, text: $actualText)

Now in your button action, we can use following code:

if actualText.isEmpty
{
    
}
else
{
    //This is our new value which is binded to the textField
    print(actualText)
    //This is our old value
    print(textToEdit)

    if actualText == textToEdit
    {
        self.textToEdit = actualText
        self.userConfirmedChange = false
        self.showSheetView = false
    }
    else
    {
        self.textToEdit = actualText
        self.userConfirmedChange = true
        self.showSheetView = false
    }

We check if the old value matches the new value. Here we compare the local State (which changes) with the old Binding (which has never changed so far). At the end we will use self.textToEdit = actualText to update the Binding with the acutal value, as we didn't have assigned that Binding to the TextField. TextToEdit will be passed back to our parent view, when we dismiss this view.

Upvotes: 1

nicksarno
nicksarno

Reputation: 4235

Your code is fine, except for the fact that you are initializing 'let originalText: String = textToEdit' within the body of the view. When the '@Binding var textToEdit: String' variable changes, the entire view is rerendered, which also initializes originalText again. The solution is to move originalText outside of the body.

The easy way is to add originalText as another variable to the initializer:

   @Binding var textToEdit: String
   let originalText: String

    var body: some View {

Upvotes: 1

Related Questions