在 ForEach 循环中绑定时,如何阻止 SwiftUI TextField 失去焦点?

Posted

技术标签:

【中文标题】在 ForEach 循环中绑定时,如何阻止 SwiftUI TextField 失去焦点?【英文标题】:How can I stop a SwiftUI TextField from losing focus when binding within a ForEach loop? 【发布时间】:2021-05-16 07:14:41 【问题描述】:

在表单中,我希望用户能够动态维护电话号码列表,包括根据需要添加/删除号码。

我目前正在维护 ObservableObject 类的已发布数组属性中的数字列表,这样当向数组中添加新数字时,SwiftUI 表单将通过其 ForEach 循环重建列表。 (每个电话号码都表示为一个PhoneDetails 结构,具有号码本身和电话类型[工作、手机等] 的属性。)

添加/删除非常好,但是当我尝试在 TextField 中编辑电话号码时,只要我输入一个字符,TextField 就会失去焦点。

我的直觉是,由于 TextField 绑定到其中一个数组项的 phoneNumber 属性,所以一旦我修改它,类中的整个数组就会发布它已被更改的事实,因此 SwiftUI 会尽职尽责地重建ForEach 循环,从而失去焦点。尝试输入新电话号码时,这种行为并不理想!

我还尝试直接循环遍历 PhoneDetails 对象的数组,而不使用 ObservedObject 类作为中间存储库,并且相同的行为仍然存在。

以下是最小可重现的示例代码;如前所述,添加/删除项目效果很好,但尝试输入任何 TextField 会立即失去焦点。

有人可以帮我指出我做错了什么的正确方向吗?

class PhoneDetailsStore: ObservableObject 
    @Published var allPhones: [PhoneDetails]
    
    init(phones: [PhoneDetails]) 
        allPhones = phones
    
    
    func addNewPhoneNumber() 
        allPhones.append(PhoneDetails(phoneNumber: "", phoneType: "cell"))
    
    
    func deletePhoneNumber(at index: Int) 
        if allPhones.indices.contains(index) 
            allPhones.remove(at: index)
        
    



struct PhoneDetails: Equatable, Hashable 
    var phoneNumber: String
    var phoneType: String



struct ContentView: View 
    
    @ObservedObject var userPhonesManager: PhoneDetailsStore = PhoneDetailsStore(
        phones: [
            PhoneDetails(phoneNumber: "800–692–7753", phoneType: "cell"),
            PhoneDetails(phoneNumber: "867-5309", phoneType: "home"),
            PhoneDetails(phoneNumber: "1-900-649-2568", phoneType: "office")
        ]
    )
    
    var body: some View 
        List 
            ForEach(userPhonesManager.allPhones, id: \.self)  phoneDetails in
                let index = userPhonesManager.allPhones.firstIndex(of: phoneDetails)!
                HStack 
                    Button(action:  userPhonesManager.deletePhoneNumber(at: index) ) 
                        Image(systemName: "minus.circle.fill")
                    .buttonStyle(BorderlessButtonStyle())
                    TextField("Phone", text: $userPhonesManager.allPhones[index].phoneNumber)
                
            
            Button(action:  userPhonesManager.addNewPhoneNumber() ) 
                Label 
                    Text("Add Phone Number")
                 icon: 
                    Image(systemName: "plus.circle.fill")
                
            .buttonStyle(BorderlessButtonStyle())
        
    

【问题讨论】:

我确实发现 this SO post 提出了类似的问题,但其接受的答案无助于解决我的问题。 【参考方案1】:

试试这个:

    ForEach(userPhonesManager.allPhones.indices, id: \.self)  index in
        HStack 
            Button(action: 
                userPhonesManager.deletePhoneNumber(at: index)
            ) 
                Image(systemName: "minus.circle.fill")
            .buttonStyle(BorderlessButtonStyle())
            TextField("Phone", text: $userPhonesManager.allPhones[index].phoneNumber)
        
    

EDIT-1:

查看我的评论并考虑到新的兴趣,这里是一个不使用indices 的版本。 它使用ForEach 与 SwiftUI 3 for ios 15+ 的绑定功能:

class PhoneDetailsStore: ObservableObject 
    @Published var allPhones: [PhoneDetails]
    
    init(phones: [PhoneDetails]) 
        allPhones = phones
    
    
    func addNewPhoneNumber() 
        allPhones.append(PhoneDetails(phoneNumber: "", phoneType: "cell"))
    
    
    // -- here --
    func deletePhoneNumber(of phone: PhoneDetails) 
        allPhones.removeAll(where:  $0.id == phone.id )
    
    


struct PhoneDetails: Identifiable, Equatable, Hashable 
    let id = UUID() // <--- here
    var phoneNumber: String
    var phoneType: String


struct ContentView: View 
    
    @ObservedObject var userPhonesManager: PhoneDetailsStore = PhoneDetailsStore(
        phones: [
            PhoneDetails(phoneNumber: "800–692–7753", phoneType: "cell"),
            PhoneDetails(phoneNumber: "867-5309", phoneType: "home"),
            PhoneDetails(phoneNumber: "1-900-649-2568", phoneType: "office")
        ]
    )
    
    var body: some View 
        List 
            ForEach($userPhonesManager.allPhones)  $phone in  // <--- here
                HStack 
                    Button(action: 
                        userPhonesManager.deletePhoneNumber(of: phone)  // <--- here
                    ) 
                        Image(systemName: "minus.circle.fill")
                    .buttonStyle(BorderlessButtonStyle())
                    TextField("Phone", text: $phone.phoneNumber)  // <--- here
                
            
            Button(action:  userPhonesManager.addNewPhoneNumber() ) 
                Label 
                    Text("Add Phone Number")
                 icon: 
                    Image(systemName: "plus.circle.fill")
                
            .buttonStyle(BorderlessButtonStyle())
        
    

   

【讨论】:

谢谢!您的解决方案确实有效,但根据another SO post's comment,不建议在ForEach 中使用.indices,因为它可能导致超出范围的错误。为什么使用.indices 不会导致我的代码崩溃? 我已经看到声称在添加或删除元素时使用索引不是“推荐的”,理由是范围发生了变化。好吧,这不是我的经验,示例表明它可以正常工作而不会崩溃。据我所知,当 SwiftUI 重新计算视图时,它将获得新的索引范围,从而避免任何崩溃。这就是我在这个例子中看到的。但话又说回来,我可能错了。 这救了我的命!我们知道为什么 OP 的代码会导致这个问题,或者它是一个错误吗?这已经困扰我好几个小时了 用不使用indices的(更强大的)方法更新了我的答案

以上是关于在 ForEach 循环中绑定时,如何阻止 SwiftUI TextField 失去焦点?的主要内容,如果未能解决你的问题,请参考以下文章

如何在敲除时在 foreach 循环中绑定输入值

绑定两个 ForEach 循环以更新每个项目单元格

在 foreach 循环中绑定数组值时,IMSSP“尝试绑定参数号 0”错误

如何在刀片foreach循环中获取迭代次数

从 SwiftUI 中的列表中删除绑定

动态 HTML 上的事件绑定,使用 fetch 和 forEach 内