I have a bunch of Textfields and I want to have the user be able to hit Enter/Next on the keyboard to go through the textfields which are not in a form. They are in multiple views and one is a TextArea which makes certain things not work.
I made an example but it seems to refresh the view when the user goes from one textfield to the next, except for at the end.
I have this code, please try it out or read it, any suggestions appreciated.
import SwiftUI
class MyObject: Hashable, Equatable, ObservableObject {
public let name: String
@Published public var value: String
init(name: String, value: String) {
self.name = name
self.value = value
}
static func == (lhs: MyObject, rhs: MyObject) -> Bool {
return lhs.name == rhs.name
}
func hash(into hasher: inout Hasher) {
hasher.combine(name);
hasher.combine(value);
}
}
class MyObjViewModel: ObservableObject {
@Published var myObjects: [MyObject] = []
@Published var focus: MyObject?
func nextFocus() {
guard let focus = focus,
let index = self.myObjects.firstIndex(of: focus) else {
return
}
self.focus = myObjects.indices.contains(index + 1) ? myObjects[index + 1] : nil
}
}
struct ContentView: View {
@ObservedObject var viewModel = MyObjViewModel()
init() {
self.viewModel.myObjects.append(contentsOf:[
MyObject(name: "aa", value: "1"),
MyObject(name: "bb", value: "2"),
MyObject(name: "cc", value: "3"),
MyObject(name: "dd", value: "4")
])
}
var body: some View {
VStack {
ForEach(self.viewModel.myObjects, id: \.self) { obj in
FocusField(viewModel: viewModel, displayObject: obj)
}
}
}
}
struct FocusField: View {
@ObservedObject var viewModel: MyObjViewModel
@ObservedObject var displayObject: MyObject
@FocusState var isFocused: Bool
var body: some View {
TextField("Test", text: $displayObject.value)
.onChange(of: viewModel.focus, perform: { newValue in
self.isFocused = newValue == displayObject
})
.focused(self.$isFocused)
.submitLabel(.next)
.onSubmit {
if self.viewModel.focus == nil {
self.viewModel.focus = self.displayObject
}
print(displayObject.name)
self.viewModel.nextFocus()
}
}
}
Solution: It needs to be inside a ScrollView, List, GeometryReader, or some similar type of view
Solution Code:
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
}
}
struct ContentView: View {
@StateObject var viewModel = MyObjViewModel([
MyObject(name: "aa", value: "1"),
MyObject(name: "bb", value: "2"),
MyObject(name: "cc", value: "3"),
MyObject(name: "dd", value: "4")
])
@State var focus: UUID?
var body: some View {
VStack {
Form {
Text("Header")
ForEach($viewModel.myObjects) { $obj in
FocusField(object: $obj, focus: $focus, nextFocus: {
guard let index = viewModel.myObjects.map( { $0.id }).firstIndex(of: obj.id) else {
return
}
focus = viewModel.myObjects.indices.contains(index + 1) ? viewModel.myObjects[index + 1].id : viewModel.myObjects[0].id
})
}
Text("Footer")
}
}
}
}
struct FocusField: View {
@Binding var object: MyObject
@Binding var focus: UUID?
var nextFocus: () -> Void
@FocusState var isFocused: UUID?
var body: some View {
TextField("Test", text: $object.value)
.onChange(of: focus, perform: { newValue in
self.isFocused = newValue
})
.focused(self.$isFocused, equals: object.id)
.onSubmit {
self.nextFocus()
}
}
}