Как можно изменить состояние фокусировки приложения SwiftUI с текстовыми полями в разных дочерних представлениях без обновления представления, которое вызывает эффект отскока?

#ios #swift #swiftui #focus #textfield

Вопрос:

Моя проблема: я хочу, чтобы пользователь мог переходить от текстового поля к текстовому полю без скачка представления, как показано в gif ниже.

Мой вариант использования: у меня есть несколько текстовых полей и текстовых редакторов в нескольких дочерних представлениях. Эти текстовые поля генерируются динамически, поэтому я хочу, чтобы состояние фокуса было отдельной проблемой.

Я сделал пример gif и пример кода ниже.

введите описание изображения здесь

Пожалуйста, проверьте это, любые предложения приветствуются.

Как было предложено в комментариях, я внес некоторые изменения, которые не повлияли на отказ:

  - Using Identfiable does not change the bounce
 - A single observed object or multiple and a view model does not change the bounce
 

Я думаю, что это связано с обновлением изменения состояния. Если это не обновление, вызывающее отказ (как предполагает пользователь в комментариях), что это? Есть ли способ остановить этот отскок при использовании FocusState?

Для воспроизведения: создайте новый проект xcode приложения iOS и замените представление содержимого приведенным ниже текстом кода. Кажется, что представление обновляется, когда пользователь переходит от одного текстового поля к следующему текстовому полю, вызывая отскок всего экрана.

Пример кода

 import SwiftUI

struct MyObject: Identifiable, Equatable {
    var id: String
    public var value: String
    init(name: String, value: String) {
        self.id = name
        self.value = value
    }
}

struct ContentView: View {

    @State var myObjects: [MyObject] = [
        MyObject(name: "aa", value: "1"),
        MyObject(name: "bb", value: "2"),
        MyObject(name: "cc", value: "3"),
        MyObject(name: "dd", value: "4")
    ]
    @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")
        }
    }
}

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

Комментарии:

1. Глядя на это, вы создали очень сложную конструкцию для решения простой задачи переключения фокуса. Почему оба MyObject и MyObjViewModel оба наблюдаемы и почему оба наблюдаются в FocusField ? Я не уверен, что вам нужно наблюдать MyObject , так что разве это не должно быть просто структурой и быть идентифицируемым? Почему у вас есть a .submitLabel , если вы его не используете? Факт обновления не должен вызывать отказ, который возникает с этим кодом. В этой проблеме есть еще кое-что. Самым простым способом реализации этого было бы сохранить код фокусировки ContenView и извлечь его из модели.

2. Моя задача — сфокусироваться на нескольких дочерних элементах, это пример без использования нескольких представлений. MyObject должен быть хешируемым / приравниваемым, чтобы это работало из-за ForEach и == сравнения, возможно, Identified тоже будет работать. Я перенес код фокуса в представление содержимого, и проблема сохраняется, я опубликовал новый пример с этим обновлением в редактировании исходного вопроса в соответствии с вашими предложениями. Кроме того, я использую .submitLabel, что вы подразумеваете под этим комментарием? Что вызывает отказ, если не обновление при изменении состояния? Пожалуйста, попробуйте код, чтобы увидеть

3. Identified будет работать и улучшит работу ForEach. С помощью submitLabel вы помечаете текстовое поле, но я не вижу, где оно используется в onSubmit. Я пропустил это? Мне придется еще раз взглянуть на это завтра.

4. Я изменил второй блок кода, чтобы использовать identified. Identified действительно работает, но это не исправляет ничего, о чем я спрашиваю. Это синтаксический сахар, который делает определение ForEach и Object более чистым, хотя и это приятно. Но, к сожалению, это не решает проблему и является неправильным аспектом для фокусировки, то же самое со следующей меткой на клавише возврата клавиатуры . Проблема сохраняется и в случае, если вы не загрузили код в Xcode. Если у вас есть какие-либо идеи по конкретному решению проблемы отказов, пожалуйста, дайте мне знать

5. Identified не работает, потому что вы сохранили MyObject как класс и не вставили id: . Это не синтаксический сахар в a struct с an id: . Теперь ваш MRE точно отражает иерархию представлений в вашем приложении? Вы упомянули a TextArea , который был бы a TextEditor в этом контексте, но я не вижу его в списке. Это, вероятно, не имеет значения, но вы упомянули об этом конкретно и сказали: «что заставляет определенные вещи не работать».

Ответ №1:

Пройдя через это кучу раз, до меня дошло, что при использовании FocusState вы действительно должны находиться в режиме a ScrollView Form или каком-то другом типе жадного просмотра. Даже a GeometryReader будет работать. Любой из них устранит отскок.

 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()
            }
    }
}
 

Редактировать:

Кроме того, это действительно плохая идея — устанавливать id структуру так, как вы это сделали. Идентификатор должен быть уникальным. Это работает здесь, но лучшая практика — это UUID .

Второе редактирование: ужесточен код.

Комментарии:

1. Я рад, наконец, создать историю для фокусировки текстового поля таким образом, спасибо! Мои идентификаторы базы данных уникальны, но спасибо за вашу заботу. Я просто изучаю SwiftUI по ходу дела, и я хотел бы лучше ознакомиться с этим. Я обязательно буду изучать жадные взгляды и, надеюсь, приду к аналогичному пониманию

2. Я был не совсем доволен опубликованным кодом, поэтому я ужесточил его и перепечатал все. Вы увидите, что я использовал id вместо объекта и изменил var nextFocus: (MyObject) -> Void на var nextFocus: () -> Void , так как ничего не нужно отправлять в закрытие. (Кстати, мне нравится ваше решение по этому поводу). По сути, все изменения TextField были ужесточены.