SwiftUI TextField touchable Area
Asked Answered
H

9

45

SwiftUI layout is very different from what we are used to. Currently I'm fighting against TextFields. Specifically their touchable Area.

TextField(
    .constant(""),
    placeholder: Text("My text field")
    )
        .padding([.leading, .trailing])
        .font(.body)

This results in a very small TextField (height wise)

enter image description here

Adding the frame modifier fixes the issue (visually)

TextField(
    .constant(""),
    placeholder: Text("My text field")
    ).frame(height: 60)
        .padding([.leading, .trailing])
        .font(.body)

but the touchable area remains the same.

I'm aware of the fact that the frame modifier does nothing else other than wrap the textField in another View with the specified height.

Is there any equivalent to resizable() for Image that will allow a taller TextField with wider touchable Area?

Herbivorous answered 27/6, 2019 at 17:1 Comment(6)
I'm facing same issueZeldazelde
Did you figure out a solution to this?Boldface
No i didn’t. I filed a radar to Apple. They said it’s fixed but it’s not... i eventually gave upHerbivorous
@GiuseppeLanza what do you mean by "they said it's fixed"? Should it suppose to work by applying the frame? Or padding?Pratt
Still no solution (besides UIViewRepresentable'ing UITextField) in Swift v2 (2020)Gladiator
Still broken in SwiftUI v3 (2021) :( Apple should fix this basic thing instead of having developers hack. File feedback reports: feedbackassistant.apple.com The more reports, the more likely Apple is to fix. Now is a good time since SwiftUI v3 is still in beta.Gladiator
B
35

This solution only requires a @FocusState, an onTapGesture and a .background with color, and allows the user to tap anywhere, including the padded area, to focus the field. Tested with iOS 15.

struct MyView: View {
    @Binding var text: String
    @FocusState private var isFocused: Bool
    var body: some View {
        TextField("", text: $text)
            .padding()
            .background(Color.gray)
            .focused($isFocused)
            .onTapGesture {
                isFocused = true
            }
    }
}

Bonus:

If you find yourself doing this on several text fields, making a custom TextFieldStyle will make things easier:

struct TappableTextFieldStyle: TextFieldStyle {
    @FocusState private var textFieldFocused: Bool
    func _body(configuration: TextField<Self._Label>) -> some View {
        configuration
            .padding()
            .background(Color.gray)
            .focused($textFieldFocused)
            .onTapGesture {
                textFieldFocused = true
            }
    }
}

Then apply it to your text fields with:

TextField("", text: $text)
    .textFieldStyle(TappableTextFieldStyle())
Briseno answered 2/2, 2022 at 13:38 Comment(6)
This was my solution also. Unfortunately this only works on iOS15 because of the @FocusState.Dionne
This solution just works perfect!Cribble
this solution is almost perfect. what's missing is moving the cursor to the beginning of the document when the user taps the leading padding. nothing I've done seems to work. I've even used the introspection library to set the selected range to beginning of document but for some reason it wouldn't workGorged
This works but only if you also add .background(.white) or whatever color you need. Please update the answerBettencourt
@Bettencourt I've added color to TappableTextFieldStyle.Briseno
I'm doing the first one and it's not working for meVocalist
G
7

Solution with Button

If you don't mind using Introspect you can do it by saving the UITextField and calling becomeFirstResponder() on button press.

extension View {
    public func textFieldFocusableArea() -> some View {
        TextFieldButton { self.contentShape(Rectangle()) }
    }
}

fileprivate struct TextFieldButton<Label: View>: View {
    init(label: @escaping () -> Label) {
        self.label = label
    }
    
    var label: () -> Label
    
    private var textField = Weak<UITextField>(nil)
    
    var body: some View {
        Button(action: {
            self.textField.value?.becomeFirstResponder()
        }, label: {
            label().introspectTextField {
                self.textField.value = $0
            }
        }).buttonStyle(PlainButtonStyle())
    }
}

/// Holds a weak reference to a value
public class Weak<T: AnyObject> {
    public weak var value: T?
    public init(_ value: T?) {
        self.value = value
    }
}

Example usage:

TextField(...)
    .padding(100)
    .textFieldFocusableArea()

Since I use this myself as well, I will keep it updated on github: https://gist.github.com/Amzd/d7d0c7de8eae8a771cb0ae3b99eab73d

New solution using ResponderChain

The Button solution will add styling and animation which might not be wanted therefore I now use a new method using my ResponderChain package

import ResponderChain

extension View {
    public func textFieldFocusableArea() -> some View {
        self.modifier(TextFieldFocusableAreaModifier())
    }
}

fileprivate struct TextFieldFocusableAreaModifier: ViewModifier {
    @EnvironmentObject private var chain: ResponderChain
    @State private var id = UUID()
    
    func body(content: Content) -> some View {
        content
            .contentShape(Rectangle())
            .responderTag(id)
            .onTapGesture {
                chain.firstResponder = id
            }
    }
}

You'll have to set the ResponderChain as environment object in the SceneDelegate, check the README of ResponderChain for more info.

Genteelism answered 29/5, 2020 at 15:46 Comment(1)
I can verify this is a good solution. It's not ideal having to use a third-party package but it will work well enough until Apple releases a native way to do thisStopover
T
3

Solution Without Any 3rd Parties

Increasing the tappable area can be done without third parties:

Step1: Create a modified TextField. This is done so we can define the padding of our new TextField:

Code used from - https://mcmap.net/q/127097/-create-space-at-the-beginning-of-a-uitextfield

class ModifiedTextField: UITextField {
  let padding = UIEdgeInsets(top: 20, left: 5, bottom: 0, right: 5)

  override open func textRect(forBounds bounds: CGRect) -> CGRect {
    bounds.inset(by: padding)
  }

  override open func placeholderRect(forBounds bounds: CGRect) -> CGRect {
    bounds.inset(by: padding)
  }

  override open func editingRect(forBounds bounds: CGRect) -> CGRect {
    bounds.inset(by: padding)
  }
}

Step 2: Make the new ModifiedTexField UIViewRepresentable so we can use it SwiftUI:

struct EnhancedTextField: UIViewRepresentable {
  @Binding var text: String
  
  
  init(text: Binding<String>) {
    self._text = text
  }

  func makeUIView(context: Context) -> ModifiedTextField {
    let textField = ModifiedTextField(frame: .zero)
    textField.delegate = context.coordinator
    
    return textField
  }
  

  func updateUIView(_ uiView: ModifiedTextField, context: Context) {
    uiView.text = text
  }


  func makeCoordinator() -> Coordinator {
    Coordinator(self)
  }
  
  
  class Coordinator: NSObject, UITextFieldDelegate {
    let parent: EnhancedTextField

    init(_ parent: EnhancedTextField) {
      self.parent = parent
    }
  

    func textFieldDidChangeSelection(_ textField: UITextField) {
      parent.text = textField.text ?? ""
    }
  }
} 

Step3: Use the new EnhancedTextField wherever needed:

EnhancedTextField(placeholder: placeholder, text: $binding)

Note: To increase or decrease the tappable area just change the padding in ModifiedTextField

let padding = UIEdgeInsets(top: 20, left: 5, bottom: 0, right: 5)  
Tracheotomy answered 21/8, 2021 at 17:37 Comment(0)
I
1

iOS 15 Solution with TextFieldStyle and additional header (it can be removed if need)

extension TextField {
    func customStyle(_ title: String) -> some View {
        self.textFieldStyle(CustomTextFieldStyle(title))
    }
}
extension SecureField {
    func customStyle(_ title: String, error) -> some View {
        self.textFieldStyle(CustomTextFieldStyle(title))
    }
}

struct CustomTextFieldStyle : TextFieldStyle {
    @FocusState var focused: Bool
    let title: String

    init(_ title: String) {
        self.title = title
    }
    
    public func _body(configuration: TextField<Self._Label>) -> some View {
        VStack(alignment: .leading) {
            Text(title)
                .padding(.horizontal, 12)
            configuration
                .focused($focused)
                .frame(height: 48)
                .padding(.horizontal, 12)
                .background(
                    RoundedRectangle(cornerRadius: 8, style: .continuous)
                        .foregroundColor(.gray)
                )
        }.onTapGesture {
            focused = true
        }
    }
}
Imidazole answered 25/1, 2022 at 19:46 Comment(1)
Most favorable solution and works like a charm with such customisation with the title on top of it.Sly
I
0

I don't know which is better for you. so, I post two solution.

1) If you want to shrink only input area.

enter image description here

  var body: some View {
     Form {
        HStack {
           Spacer().frame(width: 30)
           TextField("input text", text: $inputText)
           Spacer().frame(width: 30)
        }
     }
   }

2) shrink a whole form area

enter image description here

  var body: some View {
    HStack {
        Spacer().frame(width: 30)
        Form {
            TextField("input text", text: $restrictInput.text)
        }
        Spacer().frame(width: 30)
    }
 }
Indeed answered 12/10, 2019 at 23:39 Comment(1)
But I don't want my textfield in a form. What I want is a textfield on a view and I want it of an active size of my choice. Just like you can do with UIKit: create a textfield, give it frames and place it on screen. Now, while I can add the frame modifier, the textfield is wrapped inside a view and the touchable area of the textfield remains wrapped to the content of the field which Is very smallHerbivorous
S
0

A little work around but works.

  struct CustomTextField: View {
@State var name = ""
@State var isFocused = false
let textFieldsize : CGFloat = 20
var textFieldTouchAbleHeight : CGFloat = 200
var body: some View {
        ZStack {
            HStack{
                Text(name)
                    .font(.system(size: textFieldsize))
                    .lineLimit(1)
                    .foregroundColor(isFocused ? Color.clear : Color.black)
                    .disabled(true)
                Spacer()
            }
            .frame(alignment: .leading)
            TextField(name, text: $name , onEditingChanged: { editingChanged in
                isFocused = editingChanged
            })
            .font(.system(size: isFocused ? textFieldsize :  textFieldTouchAbleHeight ))
            .foregroundColor(isFocused ? Color.black  : Color.clear)
            .frame( height: isFocused ? 50 :  textFieldTouchAbleHeight , alignment: .leading)
             
        }.frame(width: 300, height: textFieldTouchAbleHeight + 10,alignment: .leading)
        .disableAutocorrection(true)
        .background(Color.white)
        .padding(.horizontal,10)
        .padding(.vertical,10)
        .border(Color.red, width: 2) 
    
      }

    }
Swanskin answered 22/3, 2021 at 16:39 Comment(1)
I upvoted but then later realized it does not work 100% - I have to tap the textfield twice for the cursor to appear, if text has previously been entered.Mestas
B
0

First, we use an empty TextField with its content aligned from right to left. We then overlay an HStack on top of it. This HStack contains a Text view with the content "$" and another Text view displaying the entered text.

HStack {
    Text("Amount")
    ZStack(alignment: .trailing) {
        TextField("", text: $cost)
            .multilineTextAlignment(.trailing)
            .keyboardType(.decimalPad)
            .overlay(
                HStack {
                    Text("$")
                    Text(cost).foregroundColor(.clear)
                },
                alignment: .trailing
            )
    }
}
Bordereau answered 13/9, 2023 at 14:7 Comment(0)
D
-1

Try using an overlay with a spacer to create a larger tapable/touchable area.

Create a myText variable:

    @State private var myText = ""

Then, create your TextField with the following example formatting with an overlay:

    TextField("Enter myText...", text: $myText)
        .padding()
        .frame(maxWidth: .infinity)
        .padding(.horizontal)
        .shadow(color: Color(.gray), radius: 3, x: 3, y: 3)
        .overlay(
            HStack {
                Spacer()
            })

Hope this works for you!

Dehart answered 16/2, 2022 at 6:21 Comment(0)
G
-3

quick workaround would be to just put TextField in a button, and it'll make keyboard open no matter where you tap (in button); I know it's not a solution but it gets the job done (sort of).

Goerke answered 30/10, 2019 at 4:32 Comment(6)
It's a good workaround... but quite bothering the idea that we need a button when we want a textField. This would work, but I still think that there must be another way. I don't believe that apple didn't think about the possibility of using textField outside of forms.Herbivorous
I totally agree; im honestly shocked there isnt just a property you can adjust to increase clickable area, or just having the clickable area automatically be whatever the size of the frame/border is.Goerke
@ThomasBahmandeji How did you make this? I really can't figure it out.Nanette
Didn't work for me. The button would tap, but the TextField wouldn't become first responder.Schermerhorn
Based off of this answer: #58618970 I think you're saying to put the text field inside a button, right? I tried this, and it didn't work for me. How exactly are you doing it @ThomasBahmandejiBadly
I downvoted this answer because you provide no details and no code, and no one who claims to have tried it, says that it works.Confucius

© 2022 - 2024 — McMap. All rights reserved.