How to change SwiftUI TextField FocusState without having View refresh and cause a bounce effect

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()
            }
    }
}
  • Some potential clarity since I cant edit the post

    My Problem: I want the user to be able to go from Textfield to TextField without the view bouncing as shown in the gif below.

    My Use Case: I have multiple TextFields and TextEditors in multiple child views. These TextFields are generated dynamically so I want the FocusState to be a separate concern.

    I made an example gif and code sample below which apple wont let me post a link to:

    https://imgur.com/a/X0KJa45

    I think this is from the state change refresh. If it's not the refresh causing the bounce(as the user suggests in the comments) what is? Is there a way to stop this bounce while using FocusState?

Add a Comment

Accepted Reply

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()
        }
}
}
  • This solution does not work for anyone wondering. Copy pasted the same code in a new view and the keyboard still bounce between textfields. ios 16.2

Add a Comment

Replies

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()
        }
}
}
  • This solution does not work for anyone wondering. Copy pasted the same code in a new view and the keyboard still bounce between textfields. ios 16.2

Add a Comment

I have written a small library that enables automatic navigation between TextFields upon submission: FormView. It also has useful features such as:

  • Validation of TextFields based on specified rules
  • Prevention of incorrect input based on specified rules