macOS programming: Implementing a focusable text field in SwiftUI

macOS programming: Implementing a focusable text field in SwiftUI
⏱ Reading Time: 14 mins

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:

  1. 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.
  2. 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:

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:

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:

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:

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:

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:

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:

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:

The delay to update the didFocus property is necessary, otherwise Xcode will complain about modifying the view’s state while it’s being updated:

Xcode warning saying that modifying state during view update.

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:

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 the didFocus is still false. If both conditions are met, then we will make the update(_:context:) method to be called again. We will achieve that by setting false to didFocus property.

In code now, first let’s update the init(with:) method:

Next, we’ll implement the handleAppDidBecomeActive(notification:) method:

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):

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:

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:

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:

An empty text field that gets clicked to get the focus and the phrase Hello World is typed in.

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:

This time the text field will become the first responder right after the app starts:

A text field that has the focus.

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:

We now get updates every time both on edit, and on end editing events:

A text field next to Xcode's output. The phrase Hello World is typed in and the characters are showing up in the console while typing along with a message when the Return key is pressed and editing is finished.

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:

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:

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:

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:

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.

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:

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:

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:

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:

Three text fields that get the focus subsequently when the Tab key is pressed.

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.

If you found this post useful then please consider sharing it! Also, subscribe to my newsletter in order to be notified about everything new published here directly in your inbox, and follow me on Twitter, on YouTube, on Medium and other social media.

👉 Wanna say thanks or support me?

Buy Me a Coffee!

Stay Up To Date

Subscribe to my newsletter and get notifiied instantly when I post something new on SerialCoder.dev.

    We respect your privacy. Unsubscribe at any time.