I'm having some difficulty implementing a custom textfield in SwiftUI. I'm trying to create a password field, however I'm having to dip into UIKit because I need to react when the field is focused, and (unlike with the standard TextField in SwiftUI) there is no onEditingChanged closure with SecureField.
So I have the following :
struct PasswordField: UIViewRepresentable {
#ObservedObject var viewModel: TextFieldFloatingWithBorderViewModel
func makeUIView(context: UIViewRepresentableContext<PasswordField>) -> UITextField {
let tf = UITextField(frame: .zero)
tf.isUserInteractionEnabled = true
tf.delegate = context.coordinator
return tf
}
func makeCoordinator() -> PasswordField.Coordinator {
return Coordinator(viewModel: viewModel)
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = viewModel.text
uiView.isSecureTextEntry = !viewModel.isRevealed
}
class Coordinator: NSObject, UITextFieldDelegate {
#ObservedObject var viewModel: TextFieldFloatingWithBorderViewModel
init(viewModel: TextFieldFloatingWithBorderViewModel) {
self.viewModel = viewModel
}
func textFieldDidChangeSelection(_ textField: UITextField) {
DispatchQueue.main.async {
self.viewModel.text = textField.text ?? ""
}
}
func textFieldDidBeginEditing(_ textField: UITextField) {
DispatchQueue.main.async {
self.viewModel.isFocused = true
}
}
func textFieldDidEndEditing(_ textField: UITextField) {
DispatchQueue.main.async {
self.viewModel.isFocused = false
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return false
}
}
}
We then declare this field as follows within a SwiftUI View:
HStack {
PasswordField(viewModel: viewModel)
Button(action: {
viewModel.toggleReveal()
}) {
viewModel.revealIcon
.foregroundColor(viewModel.isFocused ? .blue : viewModel.hasWarning ? .red: .gray)
}
}
In the TextFieldFloatingWithBorderViewModel viewModel we have a Published var isRevealed and then the following method called when the button is tapped:
func toggleReveal() {
isRevealed.toggle()
}
Because in the PasswordField we have:
uiView.isSecureTextEntry = !viewModel.isRevealed
This toggles the password field between secure view (i.e. input masked with dots) and standard view. This is working well, except that when the user toggles back to hidden and continues to type the password is being wiped:
I cannot work out why the password is being wiped here, and only when it goes from non secure to secure (not the other way around)
You already have the #FocusState for this. You can use it like any state variable. Here is some code where I put a yellow background on a TextField and conditionally controlled it with the #FocusState variable:
struct HighlightWhenFocused: View {
#State private var password: String = ""
#FocusState var passwordFocused: Bool
var body: some View {
PasswordView(password: $password)
.focused($passwordFocused)
.background(
Color.yellow
.opacity(passwordFocused ? 1 : 0)
)
}
}
struct PasswordView: View {
#Binding var password: String
#State private var secured = true
var body: some View {
HStack{
Text("Password:")
if secured{
SecureField("",text:$password)
}
else{
TextField("",text: $password)
}
Button {
secured.toggle()
} label: {
Image(systemName: secured ? "eye.slash" : "eye")
}
.buttonStyle(BorderlessButtonStyle())
}
}
}
Edited it to make it a secure password solution. Please note that when you switch between viewable/non-viewable, the fields lose focus because you tapped outside of them.
I am using a boolean (searchVM.showSearchView: Bool) to simultaneously turn opacity of a SearchView to 1.0 AND show/hide a UIRepresentableKeyboard.
This gives me the same functionality as Google Maps when you tap on the search bar and (simultaneously) the white search view appears, textfield focused, and keyboard showing.
However I am getting the below message print twice every time I set the showSearchView to true.
=== AttributeGraph: cycle detected through attribute 320536 ===
=== AttributeGraph: cycle detected through attribute 318224 ===
Why am I getting this message? Is it the passing around of my searchVM?
SearchViewModel
class SearchViewModel: ObservableObject {
var text: String = ""
#Published var showSearchView: Bool = false
}
Textfield & Cancel button
struct SearchBar: View {
#ObservedObject var searchVM: SearchViewModel
#State var text: String = ""
var body: some View {
HStack {
FirstResponderTextfield(searchVM: searchVM, text: $text, placeholder: "Search")
Text("Cancel")
.onTapGesture {
searchVM.showSearchView.toggle()
}
}
}
}
FirstResponderTextField
struct FirstResponderTextfield: UIViewRepresentable {
#ObservedObject var searchVM : SearchViewModel
#Binding var text: String
let placeholder: String
// COORDINATOR
class TextFieldCoordinator: NSObject, UITextFieldDelegate {
var control: FirstResponderTextfield
#Binding var text: String // unused but required
func textFieldDidChangeSelection(_ textField: UITextField) {
control.searchVM.text = textField.text ?? ""
}
func textFieldDidEndEditing(_ textField: UITextField) {
}
init(control: FirstResponderTextfield, text: Binding<String>) {
self.control = control
self._text = text
}
}
func makeCoordinator() -> TextFieldCoordinator {
return TextFieldCoordinator(control: self, text: $text)
}
func makeUIView(context: Context) -> some UIView {
let textField = UITextField()
textField.delegate = context.coordinator
textField.placeholder = placeholder
return textField
}
func updateUIView(_ uiView: UIViewType, context: Context) {
if searchVM.showSearchView {
uiView.becomeFirstResponder() // show keyboard
} else {
uiView.resignFirstResponder() // dismiss keyboard
}
}
}
I fixed this by adding DispatchQueue.asyncAfter( .now() + 0.05). Probably a mickey-mouse way of doing things but it works!
func updateUIView(_ uiView: UIViewType, context: Context) {
// here we check to see if our textfield has become first responder
if searchVM.showSearchView {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
uiView.becomeFirstResponder() // show keyboard
}
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
uiView.resignFirstResponder() // dismiss keyboard
}
}
}
How do i select all text when clicking inside the textfield? Just like how a web browser like chrome would when you click inside the address bar.
import SwiftUI
import AppKit
struct ContentView: View {
var body: some View {
TextField("Enter a URL", text: $site)
}
}
SwiftUI Solution:
struct ContentView: View {
var body: some View {
TextField("Placeholder", text: .constant("This is text data"))
.onReceive(NotificationCenter.default.publisher(for: UITextField.textDidBeginEditingNotification)) { obj in
if let textField = obj.object as? UITextField {
textField.selectedTextRange = textField.textRange(from: textField.beginningOfDocument, to: textField.endOfDocument)
}
}
}
}
Note : import Combine
Use UIViewRepresentable and wrap UITextField and use textField.selectedTextRange property with delegate.
Here is the sample demo
struct HighlightTextField: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ textField: UITextField, context: Context) {
textField.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: HighlightTextField
init(parent: HighlightTextField) {
self.parent = parent
}
func textFieldDidBeginEditing(_ textField: UITextField) {
textField.selectedTextRange = textField.textRange(from: textField.beginningOfDocument, to: textField.endOfDocument)
}
}
}
For macOS
struct HighlightTextField: NSViewRepresentable {
#Binding var text: String
func makeNSView(context: Context) -> CustomTextField {
CustomTextField()
}
func updateNSView(_ textField: CustomTextField, context: Context) {
textField.stringValue = text
}
}
class CustomTextField: NSTextField {
override func mouseDown(with event: NSEvent) {
if let textEditor = currentEditor() {
textEditor.selectAll(self)
}
}
}
Here is my solution
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
#State private var renameTmpText: String = ""
#FocusState var isFocused: Bool
#State private var textSelected = false
var body: some View {
TextEditor(text: $renameTmpText)
.padding(3)
.border(Color.accentColor, width: 1)
.frame(width: 120, height: 40)
.onExitCommand(perform: {
renameTmpText = ""
})
.onAppear {
renameTmpText = "Test"
isFocused = true
}
.focused($isFocused)
.onReceive(NotificationCenter.default.publisher(for: NSTextView.didChangeSelectionNotification)) { obj in
if let textView = obj.object as? NSTextView {
guard !textSelected else { return }
let range = NSRange(location: 0, length: textView.string.count)
textView.setSelectedRange(range)
textSelected = true
}
}
.onDisappear { textSelected = false }
}
}
let view = ContentView()
PlaygroundPage.current.setLiveView(view)
I've created a ViewModifier to select all the text in a TextField.
Only downside is, it won't work with multiple TextFields.
public struct SelectTextOnEditingModifier: ViewModifier {
public func body(content: Content) -> some View {
content
.onReceive(NotificationCenter.default.publisher(for: UITextField.textDidBeginEditingNotification)) { obj in
if let textField = obj.object as? UITextField {
textField.selectedTextRange = textField.textRange(from: textField.beginningOfDocument, to: textField.endOfDocument)
}
}
}
}
extension View {
/// Select all the text in a TextField when starting to edit.
/// This will not work with multiple TextField's in a single view due to not able to match the selected TextField with underlying UITextField
public func selectAllTextOnEditing() -> some View {
modifier(SelectTextOnEditingModifier())
}
}
usage:
TextField("Placeholder", text: .constant("This is text data"))
.selectAllTextOnEditing()
I'm looking to clear my FirstResponder TextField with an independent button that will save the field data and clear the input content so that we can write again. My code is based on the work of :
Matteo Pacini -> https://stackoverflow.com/a/56508132/15763454
The Clear variable should when it goes to True clear the field, it works when I click the button but I have to additionally enter a character in the text field for it to clear. This is logical since the "textFieldDidChangeSelection" function only runs when the textfield is changed
How can I make sure that as soon as my Clear variable changes to True the textfield is automatically deleted?
struct ContentView : View {
#State var text: String = ""
#State var Clear = false
var body: some View {
HStack{
Spacer()
Button(action: set,label: {
Text("Clic")
})
Spacer()
CustomTextField(text: $text,clear: $Clear,isFirstResponder: true)
.frame(width: 300, height: 50)
.background(Color.red)
}
}
func set(){
self.Clear=true
}
}
struct CustomTextField: UIViewRepresentable {
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
#Binding var clear: Bool
var didBecomeFirstResponder = false
init(text: Binding<String>,clear: Binding<Bool>) {
_text = text
_clear = clear
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
if clear == true{
textField.text?.removeAll()
clear = false
}
}
}
#Binding var text: String
#Binding var clear: Bool
var isFirstResponder = false
func makeUIView(context: UIViewRepresentableContext<CustomTextField>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.delegate = context.coordinator
return textField
}
func makeCoordinator() -> CustomTextField.Coordinator {
return Coordinator(text: $text,clear: $clear)
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTextField>) {
uiView.text = text
if isFirstResponder && !context.coordinator.didBecomeFirstResponder {
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
}
Just replace
func set() {
self.Clear=true
}
with
func set() {
self.text = ""
}
This immediately clears the text.
I'm using a modal to add names to a list. When the modal is shown, I want to focus the TextField automatically, like this:
I've not found any suitable solutions yet.
Is there anything implemented into SwiftUI already in order to do this?
Thanks for your help.
var modal: some View {
NavigationView{
VStack{
HStack{
Spacer()
TextField("Name", text: $inputText) // autofocus this!
.textFieldStyle(DefaultTextFieldStyle())
.padding()
.font(.system(size: 25))
// something like .focus() ??
Spacer()
}
Button(action: {
if self.inputText != ""{
self.players.append(Player(name: self.inputText))
self.inputText = ""
self.isModal = false
}
}, label: {
HStack{
Text("Add \(inputText)")
Image(systemName: "plus")
}
.font(.system(size: 20))
})
.padding()
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(10)
Spacer()
}
.navigationBarTitle("New Player")
.navigationBarItems(trailing: Button(action: {self.isModal=false}, label: {Text("Cancel").font(.system(size: 20))}))
.padding()
}
}
iOS 15
There is a new wrapper called #FocusState that controls the state of the keyboard and the focused keyboard ('aka' firstResponder).
Become First Responder ( Focused )
If you use a focused modifier on the text fields, you can make them become focused, for example, you can set the focusedField property in the code to make the binded textField become active:
Resign first responder ( Dismiss keyboard )
or dismiss the keyboard by setting the variable to nil:
Don't forget to watch the Direct and reflect focus in SwiftUI session from WWDC2021
iOS 13 and 14 (and 15)
Old but working:
Simple wrapper struct - Works like a native:
Note that Text binding support added as requested in the comments
struct LegacyTextField: UIViewRepresentable {
#Binding public var isFirstResponder: Bool
#Binding public var text: String
public var configuration = { (view: UITextField) in }
public init(text: Binding<String>, isFirstResponder: Binding<Bool>, configuration: #escaping (UITextField) -> () = { _ in }) {
self.configuration = configuration
self._text = text
self._isFirstResponder = isFirstResponder
}
public func makeUIView(context: Context) -> UITextField {
let view = UITextField()
view.addTarget(context.coordinator, action: #selector(Coordinator.textViewDidChange), for: .editingChanged)
view.delegate = context.coordinator
return view
}
public func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
switch isFirstResponder {
case true: uiView.becomeFirstResponder()
case false: uiView.resignFirstResponder()
}
}
public func makeCoordinator() -> Coordinator {
Coordinator($text, isFirstResponder: $isFirstResponder)
}
public class Coordinator: NSObject, UITextFieldDelegate {
var text: Binding<String>
var isFirstResponder: Binding<Bool>
init(_ text: Binding<String>, isFirstResponder: Binding<Bool>) {
self.text = text
self.isFirstResponder = isFirstResponder
}
#objc public func textViewDidChange(_ textField: UITextField) {
self.text.wrappedValue = textField.text ?? ""
}
public func textFieldDidBeginEditing(_ textField: UITextField) {
self.isFirstResponder.wrappedValue = true
}
public func textFieldDidEndEditing(_ textField: UITextField) {
self.isFirstResponder.wrappedValue = false
}
}
}
Usage:
struct ContentView: View {
#State var text = ""
#State var isFirstResponder = false
var body: some View {
LegacyTextField(text: $text, isFirstResponder: $isFirstResponder)
}
}
🎁 Bonus: Completely customizable
LegacyTextField(text: $text, isFirstResponder: $isFirstResponder) {
$0.textColor = .red
$0.tintColor = .blue
}
Since Responder Chain is not presented to be consumed via SwiftUI, so we have to consume it using UIViewRepresentable.
I have made a workaround that can work similarly to the way we use to do using UIKit.
struct CustomTextField: UIViewRepresentable {
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
#Binding var nextResponder : Bool?
#Binding var isResponder : Bool?
init(text: Binding<String>,nextResponder : Binding<Bool?> , isResponder : Binding<Bool?>) {
_text = text
_isResponder = isResponder
_nextResponder = nextResponder
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
func textFieldDidBeginEditing(_ textField: UITextField) {
DispatchQueue.main.async {
self.isResponder = true
}
}
func textFieldDidEndEditing(_ textField: UITextField) {
DispatchQueue.main.async {
self.isResponder = false
if self.nextResponder != nil {
self.nextResponder = true
}
}
}
}
#Binding var text: String
#Binding var nextResponder : Bool?
#Binding var isResponder : Bool?
var isSecured : Bool = false
var keyboard : UIKeyboardType
func makeUIView(context: UIViewRepresentableContext<CustomTextField>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.isSecureTextEntry = isSecured
textField.autocapitalizationType = .none
textField.autocorrectionType = .no
textField.keyboardType = keyboard
textField.delegate = context.coordinator
return textField
}
func makeCoordinator() -> CustomTextField.Coordinator {
return Coordinator(text: $text, nextResponder: $nextResponder, isResponder: $isResponder)
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTextField>) {
uiView.text = text
if isResponder ?? false {
uiView.becomeFirstResponder()
}
}
}
You can use this component like this...
struct ContentView : View {
#State private var username = ""
#State private var password = ""
// set true , if you want to focus it initially, and set false if you want to focus it by tapping on it.
#State private var isUsernameFirstResponder : Bool? = true
#State private var isPasswordFirstResponder : Bool? = false
var body : some View {
VStack(alignment: .center) {
CustomTextField(text: $username,
nextResponder: $isPasswordFirstResponder,
isResponder: $isUsernameFirstResponder,
isSecured: false,
keyboard: .default)
// assigning the next responder to nil , as this will be last textfield on the view.
CustomTextField(text: $password,
nextResponder: .constant(nil),
isResponder: $isPasswordFirstResponder,
isSecured: true,
keyboard: .default)
}
.padding(.horizontal, 50)
}
}
Here isResponder is to assigning responder to the current textfield, and nextResponder is to make the first response , as the current textfield resigns it.
SwiftUIX Solution
It's super easy with SwiftUIX and I am surprised more people are not aware about this.
Install SwiftUIX through Swift Package Manager.
In your code, import SwiftUIX.
Now you can use CocoaTextField instead of TextField to use the function .isFirstResponder(true).
CocoaTextField("Confirmation Code", text: $confirmationCode)
.isFirstResponder(true)
I think SwiftUIX has many handy stuff, but that is still the code outside of your control area and who knows what happens to that sugar magic when SwiftUI 3.0 comes out.
Allow me to present the boring UIKit solution slightly upgraded with reasonable checks and upgraded timing DispatchQueue.main.asyncAfter(deadline: .now() + 0.5)
// AutoFocusTextField.swift
struct AutoFocusTextField: UIViewRepresentable {
private let placeholder: String
#Binding private var text: String
private let onEditingChanged: ((_ focused: Bool) -> Void)?
private let onCommit: (() -> Void)?
init(_ placeholder: String, text: Binding<String>, onEditingChanged: ((_ focused: Bool) -> Void)? = nil, onCommit: (() -> Void)? = nil) {
self.placeholder = placeholder
_text = text
self.onEditingChanged = onEditingChanged
self.onCommit = onCommit
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<AutoFocusTextField>) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
textField.placeholder = placeholder
return textField
}
func updateUIView(_ uiView: UITextField, context:
UIViewRepresentableContext<AutoFocusTextField>) {
uiView.text = text
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // needed for modal view to show completely before aufo-focus to avoid crashes
if uiView.window != nil, !uiView.isFirstResponder {
uiView.becomeFirstResponder()
}
}
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: AutoFocusTextField
init(_ autoFocusTextField: AutoFocusTextField) {
self.parent = autoFocusTextField
}
func textFieldDidChangeSelection(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
func textFieldDidEndEditing(_ textField: UITextField) {
parent.onEditingChanged?(false)
}
func textFieldDidBeginEditing(_ textField: UITextField) {
parent.onEditingChanged?(true)
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
parent.onCommit?()
return true
}
}
}
// SearchBarView.swift
struct SearchBarView: View {
#Binding private var searchText: String
#State private var showCancelButton = false
private var shouldShowOwnCancelButton = true
private let onEditingChanged: ((Bool) -> Void)?
private let onCommit: (() -> Void)?
#Binding private var shouldAutoFocus: Bool
init(searchText: Binding<String>,
shouldShowOwnCancelButton: Bool = true,
shouldAutofocus: Binding<Bool> = .constant(false),
onEditingChanged: ((Bool) -> Void)? = nil,
onCommit: (() -> Void)? = nil) {
_searchText = searchText
self.shouldShowOwnCancelButton = shouldShowOwnCancelButton
self.onEditingChanged = onEditingChanged
_shouldAutoFocus = shouldAutofocus
self.onCommit = onCommit
}
var body: some View {
HStack {
HStack(spacing: 6) {
Image(systemName: "magnifyingglass")
.foregroundColor(.gray500)
.font(Font.subHeadline)
.opacity(1)
if shouldAutoFocus {
AutoFocusTextField("Search", text: $searchText) { focused in
self.onEditingChanged?(focused)
self.showCancelButton.toggle()
}
.foregroundColor(.gray600)
.font(Font.body)
} else {
TextField("Search", text: $searchText, onEditingChanged: { focused in
self.onEditingChanged?(focused)
self.showCancelButton.toggle()
}, onCommit: {
print("onCommit")
}).foregroundColor(.gray600)
.font(Font.body)
}
Button(action: {
self.searchText = ""
}) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.gray500)
.opacity(searchText == "" ? 0 : 1)
}.padding(4)
}.padding([.leading, .trailing], 8)
.frame(height: 36)
.background(Color.gray300.opacity(0.6))
.cornerRadius(5)
if shouldShowOwnCancelButton && showCancelButton {
Button("Cancel") {
UIApplication.shared.endEditing(true) // this must be placed before the other commands here
self.searchText = ""
self.showCancelButton = false
}
.foregroundColor(Color(.systemBlue))
}
}
}
}
#if DEBUG
struct SearchBarView_Previews: PreviewProvider {
static var previews: some View {
Group {
SearchBarView(searchText: .constant("Art"))
.environment(\.colorScheme, .light)
SearchBarView(searchText: .constant("Test"))
.environment(\.colorScheme, .dark)
}
}
}
#endif
// MARK: Helpers
extension UIApplication {
func endEditing(_ force: Bool) {
self.windows
.filter{$0.isKeyWindow}
.first?
.endEditing(force)
}
}
// ContentView.swift
class SearchVM: ObservableObject {
#Published var searchQuery: String = ""
...
}
struct ContentView: View {
#State private var shouldAutofocus = true
#StateObject private var viewModel = SearchVM()
var body: some View {
VStack {
SearchBarView(searchText: $query, shouldShowOwnCancelButton: false, shouldAutofocus: $shouldAutofocus)
}
}
}
For macOS 13, there is a new modifier that does not require a delay. Currently, does not work on iOS 16.
VStack {
TextField(...)
.focused($focusedField, equals: .firstField)
TextField(...)
.focused($focusedField, equals: .secondField)
}.defaultFocus($focusedField, .secondField) // <== Here
Apple Documentation: defaultFocus()