#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:
. Это не синтаксический сахар в astruct
с anid:
. Теперь ваш MRE точно отражает иерархию представлений в вашем приложении? Вы упомянули aTextArea
, который был бы aTextEditor
в этом контексте, но я не вижу его в списке. Это, вероятно, не имеет значения, но вы упомянули об этом конкретно и сказали: «что заставляет определенные вещи не работать».
Ответ №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
были ужесточены.