How to programmatically manage FocusState for dynamically created TextFields and TextEditors in SwiftUI for iOS 15+

In iOS 15 a much needed feature, @FocusState the property wrapper, was added to SwiftUI.

This allows developers to easily control the first responder using SwiftUI and NOT UIKit. Here’s an example without using a complicated view hierarchy:

Some Docs and Info

First Responder: The first responder is usually the first object in a responder chain to receive an event or action message. In most cases, the first responder is a view object that the user selects or activates with the mouse or keyboard.

Basically it’s the currently selected or focused object, usually an input such as a TextField.

FocusState:

Available in iOS15+; A property wrapper type that can read and write a value that SwiftUI updates as the placement of focus within the scene changes.

Basically it replaces .becomeFirstResponder for TextField and TextEditor for SwiftUI, which asks UIKit to make this object the first responder in its window. Setting the value FocusState attached to a TextField to true will ask SwiftUI to make the TextField become the first responder in it’s window.

Understand the Problem

A lot of the examples out there right now use an Enum to create a list of the fields that the view will be able to focus on, but what if your TextFields are in different children that are generated dynamically at runtime? Like most form-based app makers I have that problem.

Let’s say you have some dynamic object, MyObjector collection of MyObjects in a view model. When the user wants to edit that information using TextFields we can generate whatever view is needed including inputs; but they might not all be on the same form in the same container.

Here is an example Object and View Model for this demonstration

// This doesn't need to be identifiable, but it probably needs to be equatable
struct MyObject: Identifiable, Equatable {
public let id = UUID()
public var name: String
public var value: String
}

class MyObjViewModel: ObservableObject {

@Published var myObjects: [MyObject]

init(_ objects: [MyObject]) {
myObjects = objects
}
}

However you abstract your code, you end up with a bunch of items to edit, text, numbers, whatever. So if you can make an array to represent those items in order you can use this example in the article. It’s important to have these items in the view model in the order you want the user to focus on them, or you can manage the order some other way.

Create The Views To Edit Our Objects

If you like to make DRY code you might want to loop over the data elements and create TextFields or Inputs as needed. So below I show just that, creating a bunch of TextFields based on the data available.

Now we just need to add the code for the correct TextFields to become First Responder or use Focus State when the user hits Return on the keyboard.

Note: In this example I am using a simple view to demonstrate the problem. This also applies when the TextFields are in different child views and you want the Focus State or First Responder to go to the next logical TextField for the user. This makes text entry with many required fields easier for the user.

struct ContentView: View {
@StateObject var viewModel = MyObjViewModel([
MyObject(name: "Aaa", value: "111"),
MyObject(name: "Bbb", value: "222"),
MyObject(name: "Ccc", value: "333"),
MyObject(name: "Ddd", value: "444")
])

var body: some View {
VStack {
Text("Header")
ForEach($viewModel.myObjects) { $obj in
TextField(obj.name, text: $obj.value)
}
Text("Footer")
}
}
}

But this doesn’t get the Return key on the keyboard to go to the next TextField which is what we want.

By the way, change the text of the Return key using .submitLabel() on a Textfield.

Now we can add the FocusState

In the View we need to replace the TextField with a custom View that contains a TextField. This TextField wrapper will manage the FocusState of the field as well as the binding for the data.

Notice that a function/closure is passed, to the child FocusField, or TextField wrapper. This function, when executed by the child, will update the Focused object maintained and owned by the Parent. By updating this value SwiftUI does its magic and changes the FocusState of the next TextField in the array myObjects.

struct ContentView: View {

@State var myObjects: [MyObject] = ...
@State var focus: MyObject?

var body: some View {
VStack {
Text("Header")
ForEach(self.myObjects) { obj in
Divider()
FocusField(displayObject: obj, focus: $focus, nextFocus: {
guard let index = self.myObjects.firstIndex(of: $0) else {
return
}
self.focus = myObjects.indices.contains(index + 1) ? myObjects[index + 1] : nil
})
}
Divider()
Text("Footer")
}
}
}

The TextField Wrapper to Manage FocusState

In the TextField wrapper we have:

displayedObject The object that is being edited

isFocused The FocusState of this TextField denoted by the Property Wrapper

focus The object to be focused on, which is binding from the parent. We only monitor its changes in the FocusField

nextFocus() Which allows us to execute code from the scope of the Parent,

The FocusField doesn’t need to have any reference to the myObjects array in the Parent but it can still give the FocusState to the next TextField to iterate through that array

struct FocusField: View {

@State var displayObject: MyObject
@FocusState var isFocused: Bool
@Binding var focus: MyObject?
var nextFocus: (MyObject) -> Void

var body: some View {
TextField("Test", text: $displayObject.value)
.onChange(of: focus, perform: { newValue in
self.isFocused = newValue == displayObject
})
.focused(self.$isFocused)
.submitLabel(.next)
.onSubmit {
self.nextFocus(displayObject)
}
}
}

So with this code you should be able to use Focus State dynamically without Enums or UIKit, right?

The Example Is Too Simple

Here is an example of what will happen so far, because VStack doesn’t take up all available space it bounces if you are doing this as a proof of concept. But there’s a simple fix!

Use a Form, ScrollView, List, GeometryReader, or similar expanding/greedy layout view then the bouncing will stop and it will look perfect. No enum needed.

Here is a gist with the final code, Good Luck Out There!

--

--

--

MichaelRobertEllis.com

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Ready for Swift Package Manger?

Sidebar in iPad iOS 14, explained.

How to develop a mobile app

Constants and Variables

Build a TextField for Numbers in SwiftUI

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Michael Ellis

Michael Ellis

MichaelRobertEllis.com

More from Medium

Working with GroupBox in SwiftUI

SwiftUI Tutorial — How to use a ProgressView with URLSession

Skeleton Placeholder Views in SwiftUI

How to Use Alignments in SwiftUI (Part III)— Custom Alignments