38

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?

Giuseppe Lanza
  • 3,519
  • 1
  • 18
  • 40

8 Answers8

23

This solution only requires a @FocusState and an onTapGesture, 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()
            .focused($textFieldFocused)
            .onTapGesture {
                textFieldFocused = true
            }
    }
}

Then apply it to your text fields with:

TextField("", text: $text)
    .textFieldStyle(TappableTextFieldStyle())
Dylan Hand
  • 1,190
  • 2
  • 14
  • 21
  • This was my solution also. Unfortunately this only works on iOS15 because of the @FocusState. – lopes710 Apr 02 '22 at 14:55
  • 1
    This solution just works perfect! – wildcard Apr 23 '22 at 03:28
  • 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 work – yspreen Dec 13 '22 at 23:15
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.

Casper Zandbergen
  • 3,419
  • 2
  • 25
  • 49
  • 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 this – tyirvine Oct 28 '20 at 08:55
4

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://stackoverflow.com/a/27066764/2217750

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)  
Darkisa
  • 1,899
  • 3
  • 20
  • 40
1

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) 
    
      }

    }
Ahmad
  • 348
  • 3
  • 14
  • 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. – Nelu Dec 03 '21 at 14:25
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)
    }
 }
Masanao Imai
  • 125
  • 2
  • 2
  • 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 small – Giuseppe Lanza Oct 16 '19 at 14:19
0

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
        }
    }
}
Evgeniy Vaganov
  • 345
  • 5
  • 6
0

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!

-2

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).

  • 1
    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. – Giuseppe Lanza Oct 30 '19 at 12:36
  • 2
    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. – Thomas Bahmandeji Oct 31 '19 at 00:08
  • 1
    @ThomasBahmandeji How did you make this? I really can't figure it out. – George Jan 11 '20 at 20:41
  • 3
    Didn't work for me. The button would tap, but the TextField wouldn't become first responder. – Ben Patch Jan 20 '20 at 20:44
  • Based off of this answer: https://stackoverflow.com/questions/58618470/expande-clickable-area-on-textfield-in-swiftui-xcode-11-2?rq=1 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 @ThomasBahmandeji – Mikael Weiss May 27 '20 at 22:56
  • 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. – Gene Loparco Jan 08 '21 at 05:51