SwiftUI keyboard toolbar. Is it possible to use it with UITexField - swiftui

I have new SwiftUI .keyboard toolbar added. And it works great with Swiftui TextFields. But I consider if it is possible and how can it be done to use this toolbar also with UITextFields wrapped in UIViewRepresentable. I don’t know if I am doing something wrong or this isn’t supported.

I had the same problem and couldn't find an answer, so I tried to recreate this keyboard toolbar in SwiftUI. Here is the code:
struct SomeView: View {
#State var text = ""
#State var focusedUITextField = false
var body: some View {
NavigationStack {
ZStack {
VStack {
Button("Remove UITextField focus") {
focusedUITextField = false
}
TextField("SwiftUI", text: $text)
CustomTextField(hint: "UIKit", text: $text, focused: $focusedUITextField)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.horizontal)
if focusedUITextField {
VStack(spacing: 0) {
Spacer()
Divider()
keyboardToolbarContent
.frame(maxWidth: .infinity, maxHeight: 44)
.background(Color(UIColor.secondarySystemBackground))
}
}
}
/*
.toolbar {
ToolbarItem(placement: .keyboard) {
keyboardToolbarContent
}
}
*/
}
}
var keyboardToolbarContent: some View {
HStack {
Rectangle()
.foregroundColor(.red)
.frame(width: 50, height: 40)
Text("SwiftUI stuff")
}
}
}
And for the custom UITextField:
struct CustomTextField: UIViewRepresentable {
let hint: String
#Binding var text: String
#Binding var focused: Bool
func makeUIView(context: Context) -> UITextField {
let uiTextField = UITextField()
uiTextField.delegate = context.coordinator
uiTextField.placeholder = hint
return uiTextField
}
func updateUIView(_ uiTextField: UITextField, context: Context) {
uiTextField.text = text
uiTextField.placeholder = hint
if focused {
uiTextField.becomeFirstResponder()
} else {
uiTextField.resignFirstResponder()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
class Coordinator: NSObject, UITextFieldDelegate {
let parent: CustomTextField
init(parent: CustomTextField) {
self.parent = parent
}
func textFieldDidBeginEditing(_ textField: UITextField) {
parent.focused = true
}
func textFieldDidEndEditing(_ textField: UITextField) {
parent.focused = false
}
}
}
Unfortunately the animation of the custom toolbar and the keyboard don't match perfectly. I would challenge the guy in the comments from this post How to add a keyboard toolbar in SwiftUI that remains even when keyboard not visible but sadly I can't comment.
Also the background color doesn't match the native toolbar exactly, I don't know which color is used there.

Related

Transparent and Blurred fullScreenCover modal in SwiftUI?

I'm the fullScreenCover to show a modal in my SwiftUI project.
However, I need to show the modal with a transparent and blurred background.
But I can't seem to be able to achieve this at all.
This is my code:
.fullScreenCover(isPresented: $isPresented) {
VStack(spacing: 20) {
Spacer()
.frame(maxWidth: .infinity, minHeight: 100)
.background(Color.black)
.opacity(0.3)
//Text("modal")
}
.background(SecondView()) // << helper !!
}
And I have this on the same View:
struct BackgroundBlurView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let view = UIVisualEffectView(effect: UIBlurEffect(style: .light))
DispatchQueue.main.async {
view.superview?.superview?.backgroundColor = .clear
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
The above code will open the fullscreen modal but its not transparent or blurred at all.
This is what I get:
is there something else I need to do?
Reference to where I got the above code from:
SwiftUI: Translucent background for fullScreenCover
I removed some extraneous stuff and made sure that there was content in the background that you could see peeking through.
Without the alpha that is suggested in a different answer, the effect is very subtle, but there.
struct ContentView : View {
#State var isPresented = false
var body: some View {
VStack {
Text("Some text")
Spacer()
Text("More")
Spacer()
Button(action: {
isPresented.toggle()
}) {
Text("Show fullscreenview")
}
.fullScreenCover(isPresented: $isPresented) {
Text("modal")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(BackgroundBlurView())
}
Spacer()
Text("More text")
}
}
}
struct BackgroundBlurView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let view = UIVisualEffectView(effect: UIBlurEffect(style: .light))
DispatchQueue.main.async {
view.superview?.superview?.backgroundColor = .clear
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
Note the extremely subtle blue you can see from the Button shining through. More obvious in dark mode.
The effect may get more dramatic as you add more color and content to the background view. Adding alpha certainly makes things more obvious, though.
#Asperi answer from here is totally correct. Just need to add alpha to blur view.
struct BackgroundBlurView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let view = UIVisualEffectView(effect: UIBlurEffect(style: .light))
view.alpha = 0.5 //< --- here
DispatchQueue.main.async {
view.superview?.superview?.backgroundColor = .clear
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
Another approach is to use ViewControllerHolder reference link
struct ViewControllerHolder {
weak var value: UIViewController?
}
struct ViewControllerKey: EnvironmentKey {
static var defaultValue: ViewControllerHolder {
return ViewControllerHolder(value: UIApplication.shared.windows.first?.rootViewController)
}
}
extension EnvironmentValues {
var viewController: UIViewController? {
get { return self[ViewControllerKey.self].value }
set { self[ViewControllerKey.self].value = newValue }
}
}
extension UIViewController {
func present<Content: View>(backgroundColor: UIColor = UIColor.clear, #ViewBuilder builder: () -> Content) {
let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
toPresent.view.backgroundColor = backgroundColor
toPresent.modalPresentationStyle = .overCurrentContext
toPresent.modalTransitionStyle = .coverVertical
toPresent.rootView = AnyView(
builder()
.environment(\.viewController, toPresent)
)
//Add blur efect
let blurEfect = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
blurEfect.frame = UIScreen.main.bounds
blurEfect.alpha = 0.5
toPresent.view.insertSubview(blurEfect, at: 0)
self.present(toPresent, animated: true, completion: nil)
}
}
struct ContentViewNeo: View {
#Environment(\.viewController) private var viewControllerHolder: UIViewController?
var body: some View {
VStack {
Text("Background screen")
Button("Open") {
viewControllerHolder?.present(builder: {
SheetView()
})
}
}
}
}
struct SheetView: View {
var body: some View {
Text("Sheet view")
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}
}

SwiftUI Searchbar pushed off screen by keyboard

I have implemented a searchbar that filters a list. However when the keyboard appears it pushes the searchbar right off the screen. I have tried using .ignoresSafeArea(.keyboard) however it will not work (I have tried placing it in many different spots). I would like to make it so the view/list does not move at all when the keyboard appears.
I am displaying this view below after pressing a button
//MARK: - ActivitySelectorView
ActivitySelectorView(showActivitySelector: $showActivitySelector, activityToSave: activityToSave, allActivities: activities, categoryNames: categoryNames)
.environmentObject(activityToSave)
.frame(width: screen.width, height: screen.height)
.offset(x: showActivitySelector ? 0 : screen.width)
.offset(y: screen.minY)
.offset(x: viewState.width)
.animation(.easeInOut)
And inside ActivitySelectorView I have a title bar and the filtered list which includes a searchbar and list
var body: some View {
ZStack {
//backgroundColor
Color("\(activityToSave.category)Color")
VStack {
TitleBar(showingAlert: $showingAlert, showActivitySelector: $showActivitySelector, categoryName: categoryNames[(Int(String(activityToSave.category.last!)) ?? 1) - 1])
//MARK: - LIST
FilteredList(filter: activityToSave.category, passedActivityBinding: $activityToSave.activityName, showActivitySelector: $showActivitySelector)
.colorMultiply(Color("\(activityToSave.category)Color"))
}
}
}
Here we have FilteredList:
var body: some View {
List {
SearchBar(text: $searchText)
ForEach(fetchRequest.wrappedValue.filter({ searchText.isEmpty ? true : $0.name.contains(searchText) }), id: \.self) { activity in
Text(activity.name.capitalized)
.onTapGesture {
self.showActivitySelector = false
self.selectedActivity = activity.name.capitalized
}
}.onDelete(perform: deleteActivity)
}
.resignKeyboardOnDragGesture()
}
And last the code for the searchBar
struct SearchBar: UIViewRepresentable {
#Binding var text: String
class Coordinator: NSObject, UISearchBarDelegate {
#Binding var text: String
init(text: Binding<String>) {
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
text = searchText
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
UIApplication.shared.endEditing()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text)
}
func makeUIView(context: Context) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
searchBar.returnKeyType = .done
searchBar.enablesReturnKeyAutomatically = false
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: Context) {
uiView.text = text
}
}

SwiftUI on tvOS: How to know which item is selected in a list

In a list, i need to know which item is selected and this item have to be clickable.
This is what i try to do:
| item1 | info of the item3 (selected) |
| item2 | |
|*item3*| |
| item4 | |
I can make it with .focusable() but it's not clickable.
Button or NavigationLink works but i can't get the current item selected.
When you use Button or NavigationLink .focusable don't hit anymore.
So my question is:
How i can get the current item selected (so i can display more infos about this item) and make it clickable to display the next view ?
Sample code 1: Focusable works but .onTap doesn't exists on tvOS
import SwiftUI
struct TestList: Identifiable {
var id: Int
var name: String
}
let testData = [Int](0..<50).map { TestList(id: $0, name: "Row \($0)") }
struct SwiftUIView : View {
var testList: [TestList]
var body: some View {
List {
ForEach(testList) { txt in
TestRow(row: txt)
}
}
}
}
struct TestRow: View {
var row: TestList
#State private var backgroundColor = Color.clear
var body: some View {
Text(row.name)
.focusable(true) { isFocused in
self.backgroundColor = isFocused ? Color.green : Color.blue
if isFocused {
print(self.row.name)
}
}
.background(self.backgroundColor)
}
}
Sample code 2: items are clickable via NavigationLink but there is no way to get the selected item and .focusable is not called anymore.
import SwiftUI
struct TestList: Identifiable {
var id: Int
var name: String
}
let testData = [Int](0..<50).map { TestList(id: $0, name: "Row \($0)") }
struct SwiftUIView : View {
var testList: [TestList]
var body: some View {
NavigationView {
List {
ForEach(testList) { txt in
NavigationLink(destination: Text("Destination")) {
TestRow(row: txt)
}
}
}
}
}
}
struct TestRow: View {
var row: TestList
#State private var backgroundColor = Color.clear
var body: some View {
Text(row.name)
.focusable(true) { isFocused in
self.backgroundColor = isFocused ? Color.green : Color.blue
if isFocused {
print(self.row.name)
}
}
.background(self.backgroundColor)
}
}
It seems like a major oversite to me you can't attach a click event in swiftui for tvos. I've come up with a hack that allows you to make most swiftui components selectable and clickable. Hope it helps.
First I need to make a UIView that captures the events.
class ClickableHackView: UIView {
weak var delegate: ClickableHackDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
}
override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
if event?.allPresses.map({ $0.type }).contains(.select) ?? false {
delegate?.clicked()
} else {
superview?.pressesEnded(presses, with: event)
}
}
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
delegate?.focus(focused: isFocused)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var canBecomeFocused: Bool {
return true
}
}
The clickable delegate:
protocol ClickableHackDelegate: class {
func focus(focused: Bool)
func clicked()
}
Then make a swiftui extension for my view
struct ClickableHack: UIViewRepresentable {
#Binding var focused: Bool
let onClick: () -> Void
func makeUIView(context: UIViewRepresentableContext<ClickableHack>) -> UIView {
let clickableView = ClickableHackView()
clickableView.delegate = context.coordinator
return clickableView
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<ClickableHack>) {
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, ClickableHackDelegate {
private let control: ClickableHack
init(_ control: ClickableHack) {
self.control = control
super.init()
}
func focus(focused: Bool) {
control.focused = focused
}
func clicked() {
control.onClick()
}
}
}
Then I make a friendlier swiftui wrapper so I can pass in any kind of component I want to be focusable and clickable
struct Clickable<Content>: View where Content : View {
let focused: Binding<Bool>
let content: () -> Content
let onClick: () -> Void
#inlinable public init(focused: Binding<Bool>, onClick: #escaping () -> Void, #ViewBuilder content: #escaping () -> Content) {
self.content = content
self.focused = focused
self.onClick = onClick
}
var body: some View {
ZStack {
ClickableHack(focused: focused, onClick: onClick)
content()
}
}
}
Example usage:
struct ClickableTest: View {
#State var focused1: Bool = false
#State var focused2: Bool = false
var body: some View {
HStack {
Clickable(focused: self.$focused1, onClick: {
print("clicked 1")
}) {
Text("Clickable 1")
.foregroundColor(self.focused1 ? Color.red : Color.black)
}
Clickable(focused: self.$focused2, onClick: {
print("clicked 2")
}) {
Text("Clickable 2")
.foregroundColor(self.focused2 ? Color.red : Color.black)
}
}
}
}
mark a view as focusable true (stating you want it to be able to have a focus), and implement onFocusChange to save the focus state
.focusable(true, onFocusChange: { focused in
isFocused = focused
})
you need to save the isFocused as a #State var
#State var isFocused: Bool = false
then style your View based on the isFocused value
.scaleEffect(isFocused ? 1.2 : 1.0)
here is a fully working example:
struct MyCustomFocus: View {
#State var isFocused: Bool = false
var body: some View {
Text("Select Me")
.focusable(true, onFocusChange: { focused in
isFocused = focused
})
.shadow(color: Color.black, radius: isFocused ? 10 : 5, x: 5, y: isFocused ? 20 : 5)
.scaleEffect(isFocused ? 1.2 : 1.0)
.animation(.spring().speed(2))
.padding()
}
}
struct CustomFocusTest: View {
var body: some View {
VStack
{
HStack
{
MyCustomFocus()
MyCustomFocus()
MyCustomFocus()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.yellow)
.ignoresSafeArea(.all) // frame then backround then ignore for full screen background (order matters)
.edgesIgnoringSafeArea(.all)
}
}
I haven't had much luck with custom button styles on tvOS, unfortunately.
However, to create a focusable, selectable custom view in SwiftUI on tvOS you can set the button style to plain. This allows you to keep the nice system-provided focus and selection animations, while you provide the destination and custom layout. Just add the .buttonStyle(PlainButtonStyle()) modifier to your NavigationLink:
struct VideoCard: View {
var body: some View {
NavigationLink(
destination: Text("Video player")
) {
VStack(alignment: .leading, spacing: .zero) {
Image(systemName: "film")
.frame(width: 356, height: 200)
.background(Color.white)
Text("Video Title")
.foregroundColor(.white)
.padding(10)
}
.background(Color.primary)
.frame(maxWidth: 400)
}
.buttonStyle(PlainButtonStyle())
}
}
Here's a screenshot of what it looks like in the simulator.
Clicking the button on the Siri remote, or Enter or a keyboard, should work as you'd expect.

InputAccessoryView / View Pinned to Keyboard with SwiftUI

Is there an equivalent to InputAccessoryView in SwiftUI (or any indication one is coming?)
And if not, how would you emulate the behavior of an InputAccessoryView (i.e. a view pinned to the top of the keyboard)? Desired behavior is something like iMessage, where there is a view pinned to the bottom of the screen that animates up when the keyboard is opened and is positioned directly above the keyboard. For example:
Keyboard closed:
Keyboard open:
iOS 15.0+
macOS 12.0+,Mac Catalyst 15.0+
ToolbarItemPlacement has a new property in iOS 15.0+
keyboard
On iOS, keyboard items are above the software keyboard when present, or at the bottom of the screen when a hardware keyboard is attached.
On macOS, keyboard items will be placed inside the Touch Bar.
https://developer.apple.com
struct LoginForm: View {
#State private var username = ""
#State private var password = ""
var body: some View {
Form {
TextField("Username", text: $username)
SecureField("Password", text: $password)
}
.toolbar(content: {
ToolbarItemGroup(placement: .keyboard, content: {
Text("Left")
Spacer()
Text("Right")
})
})
}
}
iMessage like InputAccessoryView in iOS 15+.
struct KeyboardToolbar<ToolbarView: View>: ViewModifier {
private let height: CGFloat
private let toolbarView: ToolbarView
init(height: CGFloat, #ViewBuilder toolbar: () -> ToolbarView) {
self.height = height
self.toolbarView = toolbar()
}
func body(content: Content) -> some View {
ZStack(alignment: .bottom) {
GeometryReader { geometry in
VStack {
content
}
.frame(width: geometry.size.width, height: geometry.size.height - height)
}
toolbarView
.frame(height: self.height)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
extension View {
func keyboardToolbar<ToolbarView>(height: CGFloat, view: #escaping () -> ToolbarView) -> some View where ToolbarView: View {
modifier(KeyboardToolbar(height: height, toolbar: view))
}
}
And use .keyboardToolbar view modifier as you would normally do.
struct ContentView: View {
#State private var username = ""
var body: some View {
NavigationView{
Text("Keyboar toolbar")
.keyboardToolbar(height: 50) {
HStack {
TextField("Username", text: $username)
}
.border(.secondary, width: 1)
.padding()
}
}
}
}
I got something working which is quite near the wanted result. So at first, it's not possible to do this with SwiftUI only. You still have to use UIKit for creating the UITextField with the wanted "inputAccessoryView". The textfield in SwiftUI doesn't have the certain method.
First I created a new struct:
import UIKit
import SwiftUI
struct InputAccessory: UIViewRepresentable {
func makeUIView(context: Context) -> UITextField {
let customView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 44))
customView.backgroundColor = UIColor.red
let sampleTextField = UITextField(frame: CGRect(x: 20, y: 100, width: 300, height: 40))
sampleTextField.inputAccessoryView = customView
sampleTextField.placeholder = "placeholder"
return sampleTextField
}
func updateUIView(_ uiView: UITextField, context: Context) {
}
}
With that I could finally create a new textfield in the body of my view:
import SwiftUI
struct Test: View {
#State private var showInput: Bool = false
var body: some View {
HStack{
Spacer()
if showInput{
InputAccessory()
}else{
InputAccessory().hidden()
}
}
}
}
Now you can hide and show the textfield with the "showInput" state. The next problem is, that you have to open your keyboard at a certain event and show the textfield. That's again not possible with SwiftUI and you have to go back to UiKit and making it first responder. If you try my code, you should see a red background above the keyboard. Now you only have to move the field up and you got a working version.
Overall, at the current state it's not possible to work with the keyboard or with the certain textfield method.
I've solved this problem using 99% pure SwiftUI on iOS 14.
In the toolbar you can show any View you like.
That's my implementation:
import SwiftUI
struct ContentView: View {
#State private var showtextFieldToolbar = false
#State private var text = ""
var body: some View {
ZStack {
VStack {
TextField("Write here", text: $text) { isChanged in
if isChanged {
showtextFieldToolbar = true
}
} onCommit: {
showtextFieldToolbar = false
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
VStack {
Spacer()
if showtextFieldToolbar {
HStack {
Spacer()
Button("Close") {
showtextFieldToolbar = false
UIApplication.shared
.sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
.foregroundColor(Color.black)
.padding(.trailing, 12)
}
.frame(idealWidth: .infinity, maxWidth: .infinity,
idealHeight: 44, maxHeight: 44,
alignment: .center)
.background(Color.gray)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I managed to create a nicely working solution with some help from this post by Swift Student, with quite a lot of modification & addition of functionality you take for granted in UIKit. It is a wrapper around UITextField, but that's completely hidden from the user and it's very SwiftUI in its implementation. You can take a look at it in my GitHub repo - and you can bring it into your project as a Swift Package.
(There's too much code to put it in this answer, hence the link to the repo)
I have a implementation that can custom your toolbar
public struct InputTextField<Content: View>: View {
private let placeholder: LocalizedStringKey
#Binding
private var text: String
private let onEditingChanged: (Bool) -> Void
private let onCommit: () -> Void
private let content: () -> Content
#State
private var isShowingToolbar: Bool = false
public init(placeholder: LocalizedStringKey = "",
text: Binding<String>,
onEditingChanged: #escaping (Bool) -> Void = { _ in },
onCommit: #escaping () -> Void = { },
#ViewBuilder content: #escaping () -> Content) {
self.placeholder = placeholder
self._text = text
self.onEditingChanged = onEditingChanged
self.onCommit = onCommit
self.content = content
}
public var body: some View {
ZStack {
TextField(placeholder, text: $text) { isChanged in
if isChanged {
isShowingToolbar = true
}
onEditingChanged(isChanged)
} onCommit: {
isShowingToolbar = false
onCommit()
}
.textFieldStyle(RoundedBorderTextFieldStyle())
VStack {
Spacer()
if isShowingToolbar {
content()
}
}
}
}
}
You can do it this way without using a UIViewRepresentable.
Its based on https://stackoverflow.com/a/67502495/5718200
.onReceive(NotificationCenter.default.publisher(for: UITextField.textDidBeginEditingNotification)) { notification in
if let textField = notification.object as? UITextField {
let yourAccessoryView = UIToolbar()
// set your frame, buttons here
textField.inputAccessoryView = yourAccessoryView
}
}
}

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)
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?
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())
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.
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)
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)
}
}
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)
}
}
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
}
}
}
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!
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).