Giuseppe Lanza
Giuseppe Lanza

Reputation: 3699

SwiftUI TextField touchable Area

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?

Upvotes: 46

Views: 11523

Answers (9)

Masanao Imai
Masanao Imai

Reputation: 125

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

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

Upvotes: 0

Dylan Hand
Dylan Hand

Reputation: 1400

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

Upvotes: 42

2o47
2o47

Reputation: 59

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

Upvotes: 0

Nika Magradze
Nika Magradze

Reputation: 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!

Upvotes: -1

Eugene Vaganov
Eugene Vaganov

Reputation: 643

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

Upvotes: 1

Darkisa
Darkisa

Reputation: 2047

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)  

Upvotes: 4

Ahmad
Ahmad

Reputation: 363

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

    }

Upvotes: 0

Casper Zandbergen
Casper Zandbergen

Reputation: 3697

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.

Upvotes: 7

Thomas Bahmandeji
Thomas Bahmandeji

Reputation: 191

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

Upvotes: -3

Related Questions