how to detect when clear button tapped on SwiftUI Textfield with Introspect - swift

I'm using Introspect on a SwiftUI TextField to add a clear button to the text field. When the clear button is tapped, I want to toggle a boolean in my view model.
TextField("", text: $viewModel.title)
.textFieldStyle(.roundedBorder)
.introspectTextField(customize: { $0.clearButtonMode = .always })
.focused($isFocused)
.submitLabel(.done)
How can execute code when the clear button is tapped? Introspect makes UIKit methods available for the Swift UI Text Field. So the question might really be how do I execute some code on the tap of the clear button on a UIKIt UITextField.

Try this, make a view modifier:
struct ClearButton: ViewModifier {
#Binding var text: String
func body(content: Content) -> some View {
HStack {
content
if !text.isEmpty {
Button(
action: { self.text = "" },
label: {
Image(systemName: "delete.left")
.foregroundColor(Color(UIColor.opaqueSeparator))
}
)
}
} }
}
and the ContentView
struct ContentView: View {
#State var sampleText: String = ""
var body: some View {
NavigationView {
Form {
Section {
TextField("Type in here...", text: $sampleText)
.modifier(ClearButton(text: $sampleText))
.multilineTextAlignment(.leading)
}
}
.navigationTitle("Clear example")
}
}
}
You can execute what you want in the Button's action:

Related

SwiftUI keyboard-shortcut without modifier doesn't work in popover

In a View with a textfield I have a popover, activated by a button.
In that popover I want to listen to a keyboardShortcut without modifiers.
These keypresses arrive in the textfild of the parent view instead of the popover.
What can I do, to react on these in the popover?
struct ContentView: View {
#State var showPopOver = false
#State var text = ""
var body: some View {
VStack{
TextField("enter text here", text: $text)
Button("show popover"){ showPopOver = true }
.popover(isPresented: $showPopOver) {PopOverView(text: $text)} }
.keyboardShortcut("p")
.padding()
}
}
struct PopOverView: View {
#Environment(\.dismiss) var dismiss
#Binding var text: String
var body: some View {
VStack{
Text("my Popover")
Button("set Tom"){
text = "Tom"
dismiss()
}
.keyboardShortcut("t",modifiers: [])
//.keyboardShortcut("t") // like this it works
Button("set Frank"){
text = "Frank"
dismiss()
}
.keyboardShortcut("f",modifiers: [])
}
.padding()
}
}
KeyboardShortcuts with a modifier in the popover do work (commented out).

SwiftUI - Adding a keyboard toolbar button for only one TextField adds it for all TextFields

Background
I have two TextFields, one of which has a keyboard type of .decimalPad.
Given that there is no 'Done' button when using a decimal pad keyboard to close it, rather like the return key of the standard keyboard, I would like to add a 'Done' button within a toolbar above they keypad only for the decimal keyboard in SwiftUI.
Problem
Adding a .toolbar to any TextField for some reason adds it to all of the TextFields instead! I have tried conditional modifiers, using focussed states and checking for the Field value (but for some reason it is not set when checking, maybe an ordering thing?) and it still adds the toolbar above the keyboard for both TextFields.
How can I only have a .toolbar for my single TextField that accepts digits, and not for the other TextField that accepts a string?
Code
Please note that I've tried to make a minimal example that you can just copy and paste into Xcode and run it for yourself. With Xcode 13.2 there are some issues with displaying a keyboard for TextFields for me, especially within a sheet, so maybe simulator is required to run it properly and bring up the keyboard with cmd+K.
import SwiftUI
struct TestKeyboard: View {
#State var str: String = ""
#State var num: Float = 1.2
#FocusState private var focusedField: Field?
private enum Field: Int, CaseIterable {
case amount
case str
}
var body: some View {
VStack {
Spacer()
// I'm not adding .toolbar here...
TextField("A text field here", text: $str)
.focused($focusedField, equals: .str)
// I'm only adding .toolbar here, but it still shows for the one above..
TextField("", value: $num, formatter: FloatNumberFormatter())
.keyboardType(.decimalPad)
.focused($focusedField, equals: .amount)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Done") {
focusedField = nil
}
}
}
Spacer()
}
}
}
class FloatNumberFormatter: NumberFormatter {
override init() {
super.init()
self.numberStyle = .currency
self.currencySymbol = "€"
self.minimumFractionDigits = 2
self.maximumFractionDigits = 2
self.locale = Locale.current
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
// So you can preview it quickly
struct TestKeyboard_Previews: PreviewProvider {
static var previews: some View {
TestKeyboard()
}
}
Try to make toolbar content conditional and move toolbar outside, like below. (No possibility to test now - just idea)
Note: test on real device
var body: some View {
VStack {
Spacer()
TextField("A text field here", text: $str)
.focused($focusedField, equals: .str)
TextField("", value: $num, formatter: FloatNumberFormatter())
.focused($focusedField, equals: .amount)
.keyboardType(.decimalPad)
Spacer()
}
.toolbar { // << here !!
ToolbarItem(placement: .keyboard) {
if field == .amount { // << here !!
Button("Done") {
focusedField = nil
}
}
}
}
}
Using introspect you can do something like this in any part in your View:
.introspectTextField { textField in
textField.inputAccessoryView = UIView.getKeyboardToolbar {
textField.resignFirstResponder()
}
}
and for the getKeyboardToolbar:
extension UIView {
static func getKeyboardToolbar( _ callback: #escaping (()->()) ) -> UIToolbar {
let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 44))
let doneButton = CustomBarButtonItem(title: "Done".localized, style: .done) { _ in
callback()
}
let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
toolBar.items = [space, doneButton]
return toolBar
}
}
and for the CustomBarButtonItem this is a bar button item that takes a closure
import UIKit
class CustomBarButtonItem: UIBarButtonItem {
typealias ActionHandler = (UIBarButtonItem) -> Void
private var actionHandler: ActionHandler?
convenience init(image: UIImage?, style: UIBarButtonItem.Style, actionHandler: ActionHandler?) {
self.init(image: image, style: style, target: nil, action: #selector(barButtonItemPressed(sender:)))
target = self
self.actionHandler = actionHandler
}
convenience init(title: String?, style: UIBarButtonItem.Style, actionHandler: ActionHandler?) {
self.init(title: title, style: style, target: nil, action: #selector(barButtonItemPressed(sender:)))
target = self
self.actionHandler = actionHandler
}
convenience init(barButtonSystemItem systemItem: UIBarButtonItem.SystemItem, actionHandler: ActionHandler?) {
self.init(barButtonSystemItem: systemItem, target: nil, action: #selector(barButtonItemPressed(sender:)))
target = self
self.actionHandler = actionHandler
}
#objc func barButtonItemPressed(sender: UIBarButtonItem) {
actionHandler?(sender)
}
}
I tried it a lot but I ended up in the below one.
.focused($focusedField, equals: .zip)
.toolbar{
ToolbarItem(placement: .keyboard) {
switch focusedField{
case .zip:
HStack{
Spacer()
Button("Done"){
focusedField = nil
}
}
default:
Text("")
}
}
}
This is my solution:
func textFieldSection(title: String,
text: Binding<String>,
keyboardType: UIKeyboardType,
focused: FocusState<Bool>.Binding,
required: Bool) -> some View {
TextField(
vm.placeholderText(isRequired: required),
text: text
)
.focused(focused)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
if focused.wrappedValue {
Spacer()
Button {
focused.wrappedValue = false
} label: {
Text("Done")
}
}
}
}
}
For my project I have five TextField views on one View, so I created this method in the View's extension.
I pass the unique FocusState<Bool>.Binding value and use it in the ToolbarItemGroup closure to determine if we should display the content (Spacer, Button). If the particular TextField is focused, we display the toolbar content (all other unfocused TextFields won't).
Number Pad return solution in SwiftUI,
Tool bar button over keyboard,
Focused Field
struct NumberOfBagsView:View{
#FocusState var isInputActive: Bool
#State var phoneNumber:String = ""
TextField("Place holder",
text: $phoneNumber,
onEditingChanged: { _ in
//do actions while writing something in text field like text limit
})
.keyboardType(.numberPad)
.focused($isInputActive)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Done") {
print("done clicked")
isInputActive = false
}
}
}
}
I've found wrapping each TextField in its own NavigationView gives each its own context and thus a unique toolbar. It feels not right and I've seen constraint warnings in the console. Use something like this:
var body: some View {
VStack {
Spacer()
// I'm not adding .toolbar here...
NavigationView {
TextField("A text field here", text: $str)
.focused($focusedField, equals: .str)
}
// I'm only adding .toolbar here, but it still shows for the one above..
NavigationView {
TextField("", value: $num, formatter: FloatNumberFormatter())
.keyboardType(.decimalPad)
.focused($focusedField, equals: .amount)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Done") {
focusedField = nil
}
}
}
}
Spacer()
}
}
There is work. But the other TextField will still display toolbar.
--- update ---
Hi, I updated the code to use ViewModifier to make the code easier to use and this time the code does compile and run >_<
struct ToolbarItemWithShow<Toolbar>: ViewModifier where Toolbar: View {
var show: Bool
let toolbar: Toolbar
let placement: ToolbarItemPlacement
func body(content: Content) -> some View {
content.toolbar {
ToolbarItemGroup(placement: placement) {
ZStack(alignment: .leading) {
if show {
HStack { toolbar }
.frame(width: UIScreen.main.bounds.size.width - 12)
}
}
}
}
}
}
extension View {
func keyboardToolbar<ToolBar>(_ show: Bool, #ViewBuilder toolbar: () -> ToolBar) -> some View where ToolBar: View {
modifier(ToolbarItemWithShow(show: show, toolbar: toolbar(), placement: .keyboard))
}
}
struct ContentView: View {
private enum Field: Hashable {
case name
case age
case gender
}
#State var name = "Ye"
#State var age = "14"
#State var gender = "man"
#FocusState private var focused: Field?
var body: some View {
VStack {
TextField("Name", text: $name)
.focused($focused, equals: .name)
.keyboardToolbar(focused == .name) {
Text("Input Name")
}
TextField("Age", text: $age)
.focused($focused, equals: .age)
.keyboardToolbar(focused == .age) {
Text("Input Age")
}
TextField("Gender", text: $gender)
.focused($focused, equals: .gender)
.keyboardToolbar(focused == .gender) {
Text("Input Sex")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
--- old ---
struct TextFieldWithToolBar<Label, Toolbar>: View where Label: View, Toolbar: View {
#Binding public var text: String
public let toolbar: Toolbar?
#FocusState private var focus: Bool
var body: some View {
TextField(text: $text, label: { label })
.focused($focus)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
ZStack(alignment: .leading) {
if focus {
HStack {
toolbar
Spacer()
Button("Done") {
focus = false
}
}
.frame(width: UIScreen.main.bounds.size.width - 12)
}
}
}
}
}
}
TextFieldWithToolBar("Name", text: $name)
TextFieldWithToolBar("Name", text: $name){
Text("Only Brand")
}
TextField("Name", "Set The Name", text: $name)
with Done with Toolbar without

SwiftUI - onCommit equivalent for TextEditor

Is there an onCommit equivalent for TextEditor in SwiftUI? I want to dismiss the keyboard by using self.endEditing and this can easily be achieved with text fields by using onCommit.
I've tried using the onTapGesture method where i call UIApplication.self.endEditing() when a user taps anywhere outside the text field, however this causes some unwanted bugs with my existing segmented picker so I'd like to avoid that.
import SwiftUI
import PlaygroundSupport
struct TestView: View {
#State private var fullText: String = "This is some editable text..."
var body: some View {
VStack {
Spacer()
TextEditor(text: $fullText)
}.onTapGesture {
closeView()
}
}
func closeView() {
UIApplication.shared.sendAction(
#selector(UIResponder.resignFirstResponder),
to: nil,
from: nil,
for: nil
)
}
}
let view = TestView()
PlaygroundPage.current.setLiveView(view)
Give it a go with resignFirstResponder
SwiftUI - Dismiss the keyboard on TextEditor
When user press enter or creates a new line.
TextEditor(text: $text)
.focused($isFocused)
.onChange(of: text) { string in
if string.last == "\n"
{
text.removeLast()
isFocused = false
}
}
When user taps.
TextEditor(text: $text)
.focused($isFocused)
.onTapGesture {
isFocused ? (isFocused = false) : (isFocused = true)
}
Also you can create a function to hide keyboard and replace it where focus assignation is. But I think is not necessary here.

Setting TextField /TextEditor as the first responder upon appearing SwifUI

I've got a SwiftUI TextEditor that appears once a button is pressed. What I can't figure out is, how to make the TextEditor the first responder when it appears.
What I've tried is adding a .focused modifier to the TextEditor and then setting the focused Bool value to true inside .onAppear. But still the keyboard only shows up when its pressed.
import SwiftUI
struct ContentView: View {
#State var showTextField : Bool = false
#State private var currentEditText : String = "Some text"
#FocusState private var editTextFieldFocus: Bool
var body: some View {
VStack {
Button {
showTextField.toggle()
} label: {
Text("Show Text Field")
}
Text("Hello, world!")
.padding()
if showTextField{
TextEditor(text: $currentEditText)
.focused($editTextFieldFocus)
.onAppear {
editTextFieldFocus = true
}
}
}
}
}

TextField with animation crashes app and looses focus

Minimal reproducible example:
In SceneDelegate.swift:
let contentView = Container()
In ContentView.swift:
struct SwiftUIView: View {
#State var text: String = ""
var body: some View {
VStack {
CustomTextFieldView(text: $text)
}
}
}
struct Container: View {
#State var bool: Bool = false
#State var text: String = ""
var body: some View {
Button(action: {
self.bool.toggle()
}) {
Text("Sheet!")
}
.sheet(isPresented: $bool) {
SwiftUIView()
}
}
}
In CustomTextField.swift:
struct CustomTextFieldView: View {
#Binding var text: String
#State var editing: Bool = false
var body: some View {
Group {
if self.editing {
textField
.background(Color.red)
} else {
ZStack(alignment: .leading) {
textField
.background(Color.green)
Text("Placeholder")
}
}
}
.onTapGesture {
withAnimation {
self.editing = true
}
}
}
var textField: some View {
TextField("", text: $text)
}
Problem:
After running the above code and focusing the text field, the app crashes. Some things I noticed:
If I remove the withAnimation code, or the ZStack in CustomTextField file, the app doesn't crash, but the TextField looses focus.
If I remove the VStack in SwiftUIView, the app doesn't crash, but the TextField looses focus.
If I use a NavigationLink or present the TextField without a sheet, the app doesn't crash, but the TextField looses focus.
Questions:
Is this a problem in the current version of SwiftUI?
Is there a solution to this problem using SwiftUI? I want to stay out of
ViewRepresentables as much as possible.
How can I keep the focus of the TextField after the body is recalculated because of a change in state?
How can I keep the focus of the TextField after the body is
recalculated because of a change in state?
You have two of them. Two different TextField could not be in editing state at the same time.
The approach suggested by Asperi is the only possible.
The reason, why your code crash is not easy explain, but expected in current SwiftUI.
You have to understand, that Group is not a standard container, it just like a "block" on which you can apply some modifiers. Removing Group and using wraping body in ViewBuilder
struct CustomTextFieldView: View {
#Binding var text: String
#State var editing: Bool = false
#ViewBuilder
var body: some View {
if self.editing {
TextField("", text: $text)
.background(Color.red)
.onTapGesture {
self.editing.toggle()
}
} else {
ZStack(alignment: .leading) {
TextField("", text: $text)
.background(Color.green)
.onTapGesture {
self.editing.toggle()
}
}
}
}
}
the code will stop to crash, but there is other issue, the keyboard will dismiss immediately. That is due the tap gesture applied.
So, believe or not, you have to use ONE TextField ONLY.
struct CustomTextFieldView: View {
#Binding var text: String
#State var editing = false
var textField: some View {
TextField("", text: $text, onEditingChanged: { edit in
self.editing = edit
})
}
var body: some View {
textField.background(editing ? Color.green : Color.red)
}
}
Use this custom text field elsewhere in your code, as you want
Try the following for CustomTextFieldView (tested & works with Xcode 11.2 / iOS 13.2)
struct CustomTextFieldView: View {
#Binding var text: String
#State var editing: Bool = false
var body: some View {
TextField("", text: $text)
.background(self.editing ? Color.red : Color.green)
.onTapGesture {
withAnimation {
self.editing = true
}
}
}
}
How can I keep the focus of the TextField after the body is recalculated because of a change in state?
You don't loose focus, you just remove entire text field, so the solution is not replace text field, but modify its property, ie background. It's ok to put it into ZStack, but keep it one.