swiftuitextfield

Custom TextField Placeholder Animation


Still learning...

Am creating a form that has quite a few entry fields most Int but some String. For Int, since they are defaulted to 0, one never sees the placeholder text unless you backspace all digits out. When these Int fields are filled in, makes it hard for user to remember what each field represents. Trying not to have a text identifier next to each input field because takes up lots of space. Would rather use Placeholder text.

I found cool custom struct for String input fields that shows Placeholder text in the normal position when field is empty and moves Placeholder just above and reduced in size inside the field. If the field goes empty, the placeholder text returns to the normal position. This is all animated nicely.

Here is the code...

struct StringInputField: View {
    var fieldTitle: String
    @Binding var fieldText: String
    
    init(_ fieldTitle: String, fieldText: Binding<String>) {
        self.fieldTitle = fieldTitle
        self._fieldText = fieldText
    }
    
    var body: some View {
        ZStack(alignment: .leading) {
            Text(fieldTitle)
                .foregroundColor(fieldText.isEmpty ? Color(.placeholderText) : .accentColor)
                .offset(y: fieldText.isEmpty ? 0 : -25)
                .scaleEffect(fieldText.isEmpty ? 1 : 0.8, anchor: .leading)
            TextField("", text: $fieldText)
        }
        .padding(.top, 15)
        .animation(.easeInOut)
    }
}

In my editing view, I then call this custom input field and it all looks good.

StringInputField("League Name", fieldText: $league.name)

Field Unfilled

Field Filled

I want to have the same movement and animation of the Placeholder text for an Int textfield. I created the following but the Placeholder text doesn't move nor change color like the string field does. Since there is no fieldText.isEmpty for an Int, I am assuming that has to have a different logical value to check. I tried using fieldText.description.isEmpty but that doesn't work. I am fine with either a blank field showing when no Int typed in or a zero 0. Either is probably fine. I will be doing some validation checking on the Int entries like min, max bounds and no decimals.

This is what I tried...

struct IntInputField: View {
    var fieldTitle: String
    @Binding var fieldText: Int
    
    // Removed - Other testing...
    // @State private var lookAtState: Bool
    
    init(_ fieldTitle: String, fieldText: Binding<Int>) {
        self.fieldTitle = fieldTitle
        self._fieldText = fieldText
        //self.lookAtState = false
    }
    
    var body: some View {
        ZStack(alignment: .leading) {
            Text(fieldTitle)
                .foregroundColor(fieldText.description.isEmpty ? Color(.placeholderText) : .accentColor)
                .offset(y: fieldText.description.isEmpty ? 0 : -25)
                .scaleEffect(fieldText.description.isEmpty ? 1 : 0.8, anchor: .leading)
            
            TextField(fieldTitle, value: $fieldText, formatter: NumberFormatter())
        }
        .padding(.top, 15)
        .animation(.easeInOut)
    }
}

Can you help with a "trigger" for Int fields to produce the same effect as I have for String fields? I need to keep the stored var's as Ints because all these inputs are used in math calculations..


Solution

  • From the docs TextField, when you use text: in the TextField, the text is continously updated. But "For non-string types, it updates the value when the user commits their edits, such as by pressing the Return key".

    So you could try something like this approach to trigger the same animation when using Int.

    struct IntInputField: View {
        var fieldTitle: String
        @Binding var fieldInt: Int
        @State private var txt = ""
    
        var body: some View {
            ZStack(alignment: .leading) {
                Text(fieldTitle)
                    .foregroundColor(txt.isEmpty ? Color(.placeholderText) : .accentColor)
                    .offset(y: txt.isEmpty ? 0 : -25)
                    .scaleEffect(txt.isEmpty ? 1 : 0.8, anchor: .leading)
     
                TextField(fieldTitle, text: Binding(
                                get: { txt },
                                set: {
                                    if let n = Int($0) {
                                        fieldInt = n
                                        txt = $0
                                    } else {
                                        if $0.isEmpty {txt = ""}
                                    }
                                })
                            )
            }
            .padding(.top, 15)
            .animation(.easeInOut, value: fieldInt)
            .onAppear {
                txt = String(fieldInt)
            }
        }
    }
    

    Call it like:

     IntInputField(fieldTitle: "League Number", fieldInt: $league.number)
    

    where league.number is an Int.