swiftswiftuitextfield

text in TextField is not updating and remains empty SwiftUI


below is the code snippet you can test, for me - print(text) always prints nothing and isFilled is false no matter how many times I type in something

import SwiftUI

struct TextInputField: View {

    var title: String
    var prompt: String
    let validate: (String)->Bool
    var placeholder: String

    @Binding var text: String

    @State var isEditing: Bool = false
    @State var error: Bool = false
    @State var isFilled: Bool = false

    var body: some View {
        ZStack(alignment: .leading) {
            Text(placeholder)
                .foregroundColor(text.isEmpty ? Color(.placeholderText) : .accentColor)
                .offset(x: 0, y: (!isEditing && !isFilled) ? 0 : -14)
                .padding()
                .font(UIConstraints.fontRegular(size: (isEditing && !isFilled) ? 12 : 16))

            TextField("", text: $text, onEditingChanged: { isEditing in
                withAnimation(.default) {
                    self.isEditing = isEditing
                }
                print(text)
                isFilled = !text.isEmpty
                print(isFilled)
            })
            .font(UIConstraints.fontRegular(size: 16))
            .offset(x: 0, y: (!isEditing && !isFilled) ? 0 : 6)
            .padding()
            .overlay(
                RoundedRectangle(cornerRadius: 5)
                    .stroke(isEditing ? Color.accentColor : Color(.secondarySystemBackground), lineWidth: 2)
            )
            .overlay(
                RoundedRectangle(cornerRadius: 5)
                    .stroke(error ? Color.red : Color.clear, lineWidth: 2)
            )

        }
        .frame(height: 56)
        .background {
            Color(.secondarySystemBackground)
                .cornerRadius(5.0)
                .shadow(radius: 5.0)
        }
        .animation(.default, value: error)
        if error {
            Text(prompt)
                .padding(.leading, 2)
                .font(.footnote)
                .foregroundColor(Color(.systemRed))
        }
    }
}

struct InputTextField_Previews: PreviewProvider {
    @State static var text: String = ""
    @State static var valid: Bool = true

    static var previews: some View {
        TextInputField(title: "", prompt: "promt", validate: {$0.isEmpty}, placeholder:  "placeholder", text: $text)
            .padding()
    }
}

please help T_T

**Adding more text to be allowed to post the question: this is a custom textField to add a floating placeholder with animations. It's not done yet and some parts are unused yet, but I don't think its crucial for this bug


Solution

  • There are several issues with the provided code.

    Detect change

    To start with, as mentioned in the comments, onEditingChanged does not work as you'd expect. Instead, use the onChange(of:perform:) function instead (or the newer iOS 17 variant if possible). You could for instance add it to your TextField:

    TextField("", text: $text, onEditingChanged: { isEditing in
        withAnimation(.default) {
            self.isEditing = isEditing
        }
    })
    .onChange(of: text, perform: { text in
        print(text)
        isFilled = !text.isEmpty
        print(isFilled)
    })
    

    Update isFilled property

    Secondly, the isFilled is currently a State variable, but this can easily be transformed into a computed property instead. You'll have to update the view slightly to re-enable the animation:

    Replace the @State var isFilled: Bool = false

    private var isFilled: Bool {
        !text.isEmpty
    }
    

    Next add the animation for the isFilled property. You already had one for the error, place this one below it:

    .animation(.default, value: isFilled)
    

    Since in your code the onEditingChanged callback is only used to set the isFilled property, I believe you could now even omit the onChange(of:perform:) function, as behaviour is working without it.

    Preview

    And finally the preview code. Now using static State properties has never really worked for me. If you need to have some state during preview, either see if you can mock it using the .constant(<value>) when possible, to generate static examples:

    struct InputTextField_Previews: PreviewProvider {
        static var previews: some View {
            TextInputField(
                title: "Title",
                prompt: "Prompt",
                validate: { $0.isEmpty }, 
                placeholder: "Placeholder",
                text: .constant("testvalue") // <-- This line here
            )
            .padding()
        }
    }
    

    This does not make your preview "interactive". So you won't be able to see the animations at work. Your best shot to get this to work, is to use an intermediate view instead. Often times, I create a "Wrapper" view to accomplish just that:

    struct InputTextField_Previews: PreviewProvider {
        struct Wrapper: View {
            @State private var text: String = ""
            @State private var valid: Bool = true
    
            var body: some View {
                TextInputField(
                    title: "",
                    prompt: "promt",
                    validate: {$0.isEmpty},
                    placeholder:  "placeholder",
                    text: $text
                )
                .padding()
            }
        }
    
        static var previews: some View {
            Wrapper()
        }
    }
    

    A combination of all these fixes will likely resolve your issue too!