【问题标题】:How can one change the FocusState of a SwiftUI app with TextFields in different child views without having View refresh which causes a bounce effect?如何在不同子视图中使用 TextFields 更改 SwiftUI 应用程序的 FocusState 而不会导致视图刷新导致反弹效果?
【发布时间】:2021-12-08 00:02:11
【问题描述】:

我的问题:我希望用户能够从 Textfield 转到 TextField 而不会出现视图弹跳,如下面的 gif 所示。

我的用例:我在多个子视图中有多个 TextField 和 TextEditor。这些 TextField 是动态生成的,所以我希望 FocusState 成为一个单独的关注点。

我在下面制作了一个示例 gif 和代码示例。

请检查一下,任何建议表示赞赏。

按照 cmets 中的建议,我做了一些更改,但对反弹没有影响:

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

我认为这是来自状态更改刷新。如果不是刷新导致反弹(正如用户在 cmets 中建议的那样),那是什么?有没有办法在使用 FocusState 时阻止这种反弹?

重现:创建一个新的 iOS 应用程序 xcode 项目并将内容视图替换为下面的代码体。当用户从一个文本字段转到下一个文本字段时,它似乎会刷新视图,从而导致整个屏幕反弹。

代码示例

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

【问题讨论】:

  • 看这个,你已经做了一个非常复杂的结构来处理一个简单的切换焦点的任务。为什么MyObjectMyObjViewModel 都可以观察到,为什么两者都可以在FocusField 中观察到?我不确定您是否需要观察MyObject,所以它不应该只是一个结构并且可以识别吗?如果您不使用它,为什么会有.submitLabel?刷新的事实不应导致此代码发生反弹。这个问题还有更多。实现这一点的最简单方法是将焦点代码保留在 ContenView 中并将其从模型中拉出。
  • 我的任务是关注多个孩子,这是一个没有使用多个视图的例子。由于 ForEach 和 == 比较,MyObject 确实需要是 Hashable/Equatable 才能工作,也许 Identifiable 也可以工作。我将焦点代码拉到内容视图中并且问题仍然存在,我根据您的建议在原始问题的编辑中发布了一个带有该更新的新示例。另外,我正在使用 .submitLabel,你的评论是什么意思?如果不是从状态更改刷新,是什么导致反弹呢?请试一下代码看看
  • Identifiable 将起作用并使 ForEach 更好地工作。使用 submitLabel 标记文本字段,但我看不到它在 onSubmit 中的使用位置。我错过了吗?明天我得再看看这个。
  • 我更改了第二个代码块以使用可识别的。 Identifiable 确实有效,但它不能解决我要问的任何问题。它是语法糖,使 ForEach 和 Object 定义更清晰,虽然这很好。但不幸的是,它并没有解决问题,而且是关注的错误方面,与keyboard's return key 上的下一个标签相同。如果您尚未将代码加载到 Xcode 中,问题仍然存在。如果您对解决弹跳问题有任何想法,请告诉我
  • Identifiable 不起作用,因为您将 MyObject 保留为一个类并且没有放入 id:。它不是 structid: 中的语法糖。您的 MRE 现在是否准确地反映了您的应用程序中的视图层次结构?您提到了 TextArea 在这种情况下将是 TextEditor,但我没有看到列出。这可能无关紧要,但你特别提到了这一点并说“这使得某些事情不起作用。”

标签: ios swift swiftui focus textfield


【解决方案1】:

在经历了很多次之后,我突然意识到,当使用FocusState 时,你真的应该处于ScrollViewForm 或其他某种贪婪的视图中。即使是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 是一个非常糟糕的主意。 id 应该是唯一的。它在这里有效,但最佳做法是UUID

第二次编辑:收紧代码。

【讨论】:

  • 我很高兴终于能以这种方式为 textfield 焦点创建一个故事,谢谢!我的数据库 ID 是唯一的,但感谢您的关注。我只是在学习 SwiftUI,我想对此更加熟悉。我肯定会研究贪婪的观点,希望能得到类似的理解
  • 我对发布的代码不太满意,所以我收紧并重新发布了所有内容。你会看到我使用了id 而不是对象,并将var nextFocus: (MyObject) -> Void 更改为var nextFocus: () -> Void,因为不需要将任何内容发送到闭包中。 (顺便说一句,我喜欢你的解决方案)。基本上所有修改TextField 的内容都已收紧。
猜你喜欢
  • 1970-01-01
  • 2022-10-20
  • 1970-01-01
  • 1970-01-01
  • 2022-10-15
  • 1970-01-01
  • 2022-11-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多