Skip to content

Instantly share code, notes, and snippets.

@Amzd
Last active August 6, 2021 14:48
Show Gist options
  • Save Amzd/d7d0c7de8eae8a771cb0ae3b99eab73d to your computer and use it in GitHub Desktop.
Save Amzd/d7d0c7de8eae8a771cb0ae3b99eab73d to your computer and use it in GitHub Desktop.
SwiftUI TextField has a very small tappable area and there is no simple way to make that area bigger. This is a solution using Introspect (https://github.com/siteline/SwiftUI-Introspect/) Stack Overflow question: https://stackoverflow.com/questions/56795712/swiftui-textfield-touchable-area/62089790#62089790
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
}
}
struct ExampleView: View {
@State var text: String = ""
var body: some View {
TextField("User ID", text: $text)
.padding(20)
.textFieldFocusableArea() // You can now tap in the 20 padding around the TextField and it will focus
}
}
@tyirvine
Copy link

tyirvine commented Oct 28, 2020

This is silly but I'd like to add that import Introspect should be added to the .swift file this gist is being used in.

I'm new to Swift and I thought it just auto-imported everything like it does with regular files but alas lol. Thank you for writing this!
@Amzd

Edit: Just tested this and it works great! I think I will change a few things like removing the button animation but it works well otherwise.

@tyirvine
Copy link

I noticed that this could be used as a ViewModifier instead so I trimmed it down a bit so it's a little bit easier to understand ⤵

struct TextFieldButtonMod: ViewModifier {
    private var textField = Weak<UITextField>(nil)
    
    func body(content: Content) -> some View {
        Button(
            action: {
                self.textField.value?.becomeFirstResponder()
            },
            label: {
                content.introspectTextField { textfield in
                    self.textField.value = textfield
                }
            }).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
    }
}

extension View {
    func textFieldFocusableArea() -> some View {
        modifier(TextFieldButtonMod())
    }
}

content replaces the label() type you made

@Amzd
Copy link
Author

Amzd commented Dec 31, 2020

Your solution is nicer than the original gist @tyirvine

But since the Button adds its own styling and animations I now advise using ResponderChain for this.

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

@Amzd
Copy link
Author

Amzd commented Dec 31, 2020

Don't forget to change the cursor if you use this on MacOS.

I use this: https://gist.github.com/Amzd/cb8ba40625aeb6a015101d357acaad88

Not sure how to do cursor stuff on iPad.

@desmondkokkas
Copy link

desmondkokkas commented Aug 6, 2021

Your solution is nicer than the original gist @tyirvine

But since the Button adds its own styling and animations I now advise using ResponderChain for this.

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

Have you checked CPU usage and Memory using this solution. I am having CPU working at 100% and Memory constantly increasing.

@Amzd
Copy link
Author

Amzd commented Aug 6, 2021

Are you using ResponderChain with the SwiftUI only call or are you initializing your own ResponderChain object? The SwiftUI only version is not very optimized and I should really update it. I don’t have access to a Mac right now to test it.

@desmondkokkas
Copy link

Are you using ResponderChain with the SwiftUI only call or are you initializing your own ResponderChain object? The SwiftUI only version is not very optimized and I should really update it. I don’t have access to a Mac right now to test it.

Just copy/pasted the above solution. I am testing it on iOS.

@Amzd
Copy link
Author

Amzd commented Aug 6, 2021

Okay but you have to attach a ResponderChain which is not in the above solution. Otherwise the environmentobject is empty and it crashes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment