简体   繁体   中英

SwiftUI TextField touchable Area

SwiftUI layout is very different from what we are used to. Currently I'm fighting against TextField<\/code> s. Specifically their touchable Area.

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

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.

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

1) If you want to shrink only input area.

在此处输入图片说明

  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

在此处输入图片说明

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

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

    }

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)  

Has any new solution found out yet since almost 2 years (other than using third party plugin)?

Most workarounds such as using EnhancedTextField: UIViewRepresentable or embedding the TextField inside a button does not work for me.

iOS 15 Solution with TextFieldStyle and additional header (it can be removed if need)<\/h2>
 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 } } }<\/code><\/pre>"

This solution only requires a @FocusState<\/code> and an onTapGesture<\/code> , 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
            }
    }
}

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

SwiftUI layout is very different from what we are used to. Currently I'm fighting against TextField s. 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)

在此处输入图片说明

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?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM