Among the announcements made in WWDC 2021 there was a quite interesting one; how to focus on SwiftUI text fields programmatically. Even though it sounds like a simple topic that could be considered as non important one, the truth is that the lack of a programming way to focus on text fields has been a headache for Mac developers in the first two releases of SwiftUI.
However, and despite the previous paragraph’s quick introduction, this post is not about those SwiftUI advancements on text fields. That’s something that I will talk about in a future article.
What I mentioned above is definitely good news, but it regards evolvements that are going to be working in macOS Monterey and above. That’s nice, but what about backward compatibility? What if we wanted to make apps that support macOS versions prior to Monterey, such as Big Sur or Catalina?
The answer to these questions is what this post is all about. In the following parts, I will take you step by step through the implementation of a custom text field capable of:
- Getting the focus automatically when a view appears; that means that the text field will be ready to start typing into without clicking on it first.
- Moving the focus on subsequent text fields by pressing the Tab key.
Note: I have already used the term “focus” a few times above, and I’ll use it next as well. What I mean with that term is that the text field becomes the first responder and can accept user input. You are definitely aware of the latter in case you have made apps using AppKit or UIKit on iOS.
The FocusableTextField type
Unfortunately, the tools we need in order to create a custom focusable text field are not available natively in SwiftUI. Therefore, we will resort to AppKit and we will create an NSViewRepresentable type to implement and deal with the text field. But don’t let that disappoint you; you will find out soon that things are going to become pretty interesting, pretty fast.
So, that said, the first step is to define the following NSViewRepresentable type:
1 2 3 4 5 |
struct FocusableTextField: NSViewRepresentable { } |
You can find the FocusableTextField implementation along with the demo project on Github.
There are two mandatory methods that we must implement when making a type conform to the NSViewRepresentable protocol. The first one is the place to initialize, configure and return the AppKit control that will become available in SwiftUI. An NSTextField in this case. The second is the place to update the text field according to changes coming from the SwiftUI side. These are the next two:
1 2 3 4 5 6 7 8 9 |
func makeNSView(context: Context) -> NSTextField { } func updateNSView(_ nsView: NSTextField, context: Context) { } |
Notice that NSTextField is the return type in the first method, and the same is the type of the first argument in the second method.
Before adding any code in the above methods, let’s define a few properties in the FocusableTextField
struct:
1 2 3 4 5 6 7 8 9 10 11 |
struct FocusableTextField: NSViewRepresentable { @Binding var stringValue: String var placeholder: String var autoFocus = false var onChange: (() -> Void)? var onCommit: (() -> Void)? … } |
I’m telling in advance at this point that these are not the only properties that we’ll add here; a few more will come later. But besides that, let me highlight another fact; that the order of the properties matters! It defines the order of the arguments that we’ll provide a FocusableTextField
instance with when initializing it in SwiftUI.
A quick mention on what each property is all about:
stringValue
: The actual string content of the text field. It’s marked with the@Binding
property wrapper, because we want any changes made to the string being available in SwiftUI too. This property may have a value, but it may simply be an empty string.placeholder
: A potential placeholder text to show on the text field while it has no actual text.autoFocus
: A Boolean property that will be indicating whether the text field should get the focus (become the first responder) automatically right when it appears in the view.onChange
: A closure to notify the SwiftUI part when user is typing and editing text.onCommit
: A closure to notify the SwiftUI part about the end of editing.
I aim to provide an as much general implementation as possible here, and that’s the reason the last two closures exist above. Notice that they are both optional, meaning that they can be omitted if they are not necessary when initializing a FocusableTextField
instance in SwiftUI. On the other hand, there are several cases where it’s crucial to know when the text is being edited, or when the user has finished editing. It’s obviously not necessary to pass the text field’s text as argument on any of them; the stringValue
binding property will be always containing the edited text.
Initializing a text field
In the makeNSView(context:)
method now, it’s time to initialize and configure a text field:
1 2 3 4 5 6 7 8 9 10 11 |
func makeNSView(context: Context) -> NSTextField { let textField = NSTextField() textField.stringValue = stringValue textField.placeholderString = placeholder textField.delegate = context.coordinator textField.alignment = .center textField.bezelStyle = .roundedBezel return textField } |
The first line creates an NSTextField instance. In the next two we are assigning the values of the stringValue
and placeholder
to the text field. Both are going to be given as arguments when initializing a FocusableTextField
view.
What comes after that is to set the delegate object of the text field. When implementing an NSViewRepresentable or NSViewControllerRepresentable type, the delegate must be the instance of the Coordinator class of the type; a special class that handles messages coming from the AppKit and sends messages to the SwiftUI part. We’ll implement it in a while.
Lastly, and for demonstration reasons only, I’m setting the center alignment for the text field’s text, and specifying the bezel style. That’s clearly for demonstration reasons; you can customize the text field the way you like it.
In the end, we return the newly created text field from the method. We’ll come back to make an addition later, but for now you can consider this method as ready.
Enabling auto focus
We declared a property previously in the FocusableTextField
type called autoFocus
. The purpose is to make the text field the first responder when this property is true. However, in order to achieve a proper behavior once the text field gets the focus, we are going to need one more auxiliary property:
1 2 3 |
@State private var didFocus = false |
We mark didFocus
with the @State property wrapper because we are going to update its value in the update(_:context:)
method (the second method we defined earlier), but the FocusableTextField
type is a struct; by default is immutable.
Note: A method can mutate the struct that it belongs to, as long as we annotate it with the mutating
keyword. However, we cannot do so in the next method where we’ll update the didFocus
property, because it’s not a method belonging to the struct; it’s a method coming from the NSViewRepresentable protocol. That’s why marking didFocus
with the @State
property wrapper is necessary; it enables mutation.
Going to the update(_:context:)
method, we are going to add the following condition:
1 2 3 4 5 6 7 |
func updateNSView(_ nsView: NSTextField, context: Context) { if autoFocus && !didFocus { } } |
If the above condition evaluates to true, then it’s time to give the focus to the text field; in other words, to make it the first responder:
1 2 3 4 5 6 7 8 9 |
if autoFocus && !didFocus { NSApplication.shared.mainWindow?.perform( #selector(NSApplication.shared.mainWindow?.makeFirstResponder(_:)), with: nsView, afterDelay: 0.0 ) } |
What the above code addition does is quite simple, even though the way to write it looks more complicated than what it actually is. It invokes the makeFirstResponder(_:)
method through the app’s window object (NSWindow), “saying” to the window that its first responder should become the nsView
object; the text field in this case. The window is accessed through the application’s shared instance (NSApplication.shared.mainWindow
).
The above, however, is not enough; as long as the condition results to true, the text field will keep becoming the first responder in an indefinite loop. For that reason, we are going to update the didFocus
property and make it true, but we’ll do so with a small delay:
1 2 3 4 5 6 7 8 9 10 11 |
func updateNSView(_ nsView: NSTextField, context: Context) { if autoFocus && !didFocus { … DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { didFocus = true } } } |
The delay to update the didFocus
property is necessary, otherwise Xcode will complain about modifying the view’s state while it’s being updated:
That last addition completes the required implementation in order to make the text field get the focus automatically when the autoFocus
property is set to true.
Implementing the Coordinator class
The Coordinator class is a special class that we define in NSViewRepresentable and NSViewControllerRepresentable types. Its purpose is to make an instance of it the delegate of AppKit controls and receive messages from them. Also, and depending on the business logic we apply, it can communicate with the SwiftUI part whenever that’s necessary. It does that either by updating Binding properties or calling closures defined in the NSViewRepresentable (or NSViewControllerRepresentable) container type.
Having said that, let’s add the following initial implementation to the FocusableTextField
struct:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
struct FocusableTextField: NSViewRepresentable { … class Coordinator: NSObject, NSTextFieldDelegate { let parent: FocusableTextField init(with parent: FocusableTextField) { self.parent = parent super.init() } } } |
See that we have an uninitialized FocusableTextField
property declared in the class; it gets its value in the init(with:)
initializer.
Note: You can give any name you want to the Coordinator class. However, it’s not so common to do that.
Notice in the header of the Coordinator class that it inherits from the NSObject class, and adopts the NSTextFieldDelegate protocol. The latter is necessary in order to make a Coordinator instance become the delegate of the text field. However, it also introduces the requirement to make Coordinator inherit from NSObject.
An act of caution
We saw in the previous part that in order to make the text field the first responder is necessary to access the application’s window. However, what we did there will work if only the app has become fully active. If the app is not fully initialized, the window object will be nil, and the text field won’t get the focus automatically.
That’s something that can happen if the text field is one of the first views that will show up when the app is launched. If the text field appears in subsequent views, this won’t be an issue.
To counteract this situation, we will do the following:
- We will observe for a specific system notification in the Coordinator’s
init(with:)
method. That notification will make known when the app has become active. - In a new method that we must implement as the one to call when the notification arrives (selector method), we will check if
autoFocus
is true and thedidFocus
is still false. If both conditions are met, then we will make theupdate(_:context:)
method to be called again. We will achieve that by setting false todidFocus
property.
In code now, first let’s update the init(with:)
method:
1 2 3 4 5 6 7 8 9 10 |
init(with parent: FocusableTextField) { … NotificationCenter.default.addObserver(self, selector: #selector(handleAppDidBecomeActive(notification:)), name: NSApplication.didBecomeActiveNotification, object: nil) } |
Next, we’ll implement the handleAppDidBecomeActive(notification:)
method:
1 2 3 4 5 6 7 8 9 10 |
@objc func handleAppDidBecomeActive(notification: Notification) { if parent.autoFocus && !parent.didFocus { DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { self.parent.didFocus = false } } } |
Once again a small delay is necessary in order to make sure that what we did above will have an actual result.
It might seem strange that we set false to didFocus
even though that’s its current value. It doesn’t really matter; that’s enough in order to make update(_:context:)
method get called again.
A couple of delegate methods
We can now implement a couple of delegate methods in order to detect two things:
- When the text field’s text is being edited.
- When editing has finished (the user hit the Return key):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Coordinator: NSObject, NSTextFieldDelegate { … func controlTextDidChange(_ obj: Notification) { guard let textField = obj.object as? NSTextField else { return } parent.stringValue = textField.stringValue parent.onChange?() } func control(_ control: NSControl, textShouldEndEditing fieldEditor: NSText) -> Bool { parent.stringValue = fieldEditor.string parent.onCommit?() return true } } |
The controlTextDidChange(_:)
method lets us know when user is typing. As you can see, we get the text field, and subsequently the current text from the parameter notification’s object. Once we do that, we update the stringValue
Binding property of the FocusableTextField
instance (through the parent
property), and we call the optional onChange
closure.
The invocation of the second method takes place when the user ends editing the text by pressing the Return key. We get the actual value from the fieldEditor
parameter, and once again we update the stringValue
Binding property. This time we call the onCommit
optional closure.
Similarly as we did right above, you may add and handle more delegate methods if necessary. These two demonstrated here are the most common ones, but you can similarly implement any other method you might need.
We’ll add one more delegate method in a while, but for now we’re good to go. But first, it’s necessary to implement one more method in the FocusableTextField
struct. It’s responsible for creating the Coordinator instance:
1 2 3 4 5 6 7 8 9 |
struct FocusableTextField: NSViewRepresentable { … func makeCoordinator() -> Coordinator { Coordinator(with: self) } } |
Trying out FocusableTextField
We have added enough code so far, and we are now ready to see FocusableTextField
type in action. So, let’s start experimenting a bit with it, and see how to pass arguments for the various properties we have declared.
In a SwiftUI file, let’s create a FocusableTextField
view. You can see the bare minimum implementation to initialize it right next:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
struct ContentView: View { @State private var text = “” var body: some View { VStack { FocusableTextField(stringValue: $text, placeholder: “Type something”) .frame(maxWidth: 400) } .frame(minWidth: 500, minHeight: 500) } } |
See that we declare a @State property to keep the edited text, and we pass its binding value to the FocusableTextField
‘s initializer. The placeholder text is optional; an empty string is just fine if we don’t want to show a predefined text.
This is what we get if we run the app:
It’s obvious that the text field is not the first responder, so we have to click on it first before start typing in. But that’s not a problem anymore! FocusableTextField
can get the focus automatically. All we have to do is to provide true to the autoFocus
optional argument:
1 2 3 4 5 |
FocusableTextField(stringValue: $text, placeholder: “Type something”, autoFocus: true) |
This time the text field will become the first responder right after the app starts:
Let’s use now the two optional closures we declared in the FocusableTextField
type. To keep things simple, I will just print messages while typing and when the Return key is pressed:
1 2 3 4 5 6 7 8 9 10 |
FocusableTextField(stringValue: $text, placeholder: “Type something”, autoFocus: true, onChange: { print(“Typing… Current text:”, text) }, onCommit: { print(“Did finish typing:”, text) }) |
We now get updates every time both on edit, and on end editing events:
If all you wanted is a text field that can get the focus automatically, then this is a good point to stop reading. However, if you want to find out how to make it possible to move focus among multiple text fields, then keep reading. There are a few missing bits of code that we’ll add and we’ll get there pretty fast.
Moving focus among text fields
Making possible to move focus among multiple text fields does not seem obvious at first glance. And in fact, it’s not. However, we’ll go through the entire process step by step, and you will easily understand everything.
In order to achieve that goal, we will be based on the tag value that all AppKit controls have. Tag is a numerical value that indicates the order of first responders in a view. By default, all controls have tag values equal to zero.
Outlining our course of action, we’ll assign a tag value to each FocusableTextField
instance we create. Since each text field must have a different tag value, we’ll provide it as argument. Then, with a new @Binding property we will be indicating the tag value of the text field that should get the focus.
But in addition to all that, we have to handle the Tab key as well. We must detect when the user presses it, and propagate that information to the SwiftUI environment in order to update the tag indicating the focused text field to the tag value of the text field that should get the focus next.
First things first, and let’s begin by adding three new properties to the FocusableTextField
struct. The position where we’ll declare them matters, and it’s shown clearly next:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
struct FocusableTextField: NSViewRepresentable { @Binding var stringValue: String var placeholder: String var autoFocus = false // Add these two new properties here. var tag: Int = 0 var focusTag: Binding<Int>? var onChange: (() -> Void)? var onCommit: (() -> Void)? // Add this new optional closure here. var onTabKeystroke: (() -> Void)? … } |
Maybe you are wondering about the type of the focusTag
property. Why Binding<Int>?
and not @Binding var focusTag: Int?
?
The answer is simple. We want focusTag
to be an optional binding value, not a binding value of an optional Int. It won’t be always desirable to use and provide arguments for the tag
and focusTag
; we manage to support that with a default value for the tag and by making the focusTag
optional.
There is also the onTabKeystroke
optional closure. We’ll need it later in order to notify the SwiftUI part that the Tab key was pressed.
Before going any further now, pay a visit to the makeNSView(context:)
method and assign the value of the tag
property to the text field. Do that right before the return
statement:
1 2 3 4 5 6 7 8 9 |
func makeNSView(context: Context) -> NSTextField { … textField.tag = tag return textField } |
Focusing on text field depending on the tag value
Now that we have a few new properties available in the FocusableTextField
type, let’s use them to make the text field be the first responder depending on whether its tag value matches to the one that should have the focus. We’ll do that in the updateNSView(_:context:)
method.
What we’ll do is pretty simple; at first we’ll check if the focusTag
binding value is nil or not, and obviously there is nothing to do in case of nil. In the opposite case we’ll compare the wrapped value of focusTag
to the value of the tag
property, and if they are equal, we will make the text field the first responder.
Note that in order to avoid abnormal behavior by making the text field get the focus, it’s necessary to prevent the code we’ll add next from being executed repeatedly. For that reason, we’ll set the wrapped value of the focusTag
binding property to zero after a small delay; similarly as we did when we were implementing the auto focus.
Here’s all the above in code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
func updateNSView(_ nsView: NSTextField, context: Context) { … if let focusTag = focusTag { if focusTag.wrappedValue == nsView.tag { NSApplication.shared.mainWindow?.perform( #selector(NSApplication.shared.mainWindow?.makeFirstResponder(_:)), with: nsView, afterDelay: 0.0 ) DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { self.focusTag?.wrappedValue = 0 } } } } |
Detecting the Tab key
In order to detect when the user presses the Tab key, it’s necessary to add one more delegate method to the Coordinator class:
1 2 3 4 5 |
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { } |
In AppKit, there are binding methods implemented in the NSResponder class for various keys, which are invoked when the respective keys are pressed. However, we can provide custom implementation and achieve custom behavior for any of those keys with the above delegate method. And as you’re probably guessing, Tab is one of these keys.
What we need to do here is to check if the commandSelector
that shows the binding method invoked in NSResponder is the one matching to the Tab key. Its name is insertTab(_:)
.
Right next you can see how we do that check. In the condition’s body we will call the onTabKeystroke
closure. We’ll make known to the SwiftUI part that way that the Tab key was pressed. There, we’ll finally change the currently focused text field by specifying a different tag. More about that in a while.
1 2 3 4 5 6 7 8 9 |
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { if commandSelector == #selector(NSStandardKeyBindingResponding.insertTab(_:)) { parent.onTabKeystroke?() return true } return false } |
See that the return value in the if
body is true; that’s the way to indicate that we take charge of handling the press event of the specific key (Tab). For all the other keys, we return false and let NSResponder handle them normally.
Changing the focused text field
Back to SwiftUI, where we are going to use all the new things we added previously. For demonstration purposes, we are going to add three FocusableTextField
instances. Therefore we’ll begin by declaring three @State properties to keep their texts:
1 2 3 4 5 |
@State private var text1 = “” @State private var text2 = “” @State private var text3 = “” |
In addition to these, we’ll also add one more @State property; this one is really important, because it indicates the currently selected text field:
1 2 3 |
@State private var focusTag = 1 |
Now, in the SwiftUI view’s body we are going to create the three FocusableTextField
instances I mentioned a few lines above. What’s crucial here is to provide a tag value to all of them, to pass the focusTag
binding value and to provide a closure argument for the onTabKeystroke
parameter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
FocusableTextField(stringValue: $text1, placeholder: “Text field #1”, autoFocus: true, tag: 1, focusTag: $focusTag, onTabKeystroke: { focusTag = 2 }) FocusableTextField(stringValue: $text1, placeholder: “Text field #2”, tag: 2, focusTag: $focusTag, onTabKeystroke: { focusTag = 3 }) FocusableTextField(stringValue: $text1, placeholder: “Text field #3”, tag: 3, focusTag: $focusTag, onTabKeystroke: { focusTag = 1 }) |
See what happens on each onTabKeystroke
closure above; depending on the text field, we set the tag value of the next one that should become the first responder to the focusTag
property. This is what actually makes that functionality possible; and always in combination with the additions to the previous parts.
The result of the above is this:
Summary
So, that’s the focusable text field, which I hope that you will find handy in your projects. I admit that this was a long post, however there was a lot of code to explain, and several aspects to talk about. Even if you won’t use FocusableTextField in your projects, the content of this post shows how to create your own custom views based on AppKit, or UIKit on iOS. And of course, if you are up to using FocusableTextField, feel free to extend or modify in any possible way that suits you the most. Thanks for reading, take care!
You can find the FocusableTextField implementation along with the demo project on Github.