3

In a form, I'd like a user to be able to dynamically maintain a list of phone numbers, including adding/removing numbers as they wish.

I'm currently maintaining the list of numbers in a published array property of an ObservableObject class, such that when a new number is added to the array, the SwiftUI form will rebuild the list through its ForEach loop. (Each phone number is represented as a PhoneDetails struct, with properties for the number itself and the type of phone [work, cell, etc].)

Adding/removing works perfectly fine, but when I attempt to edit a phone number within a TextField, as soon as I type a character, the TextField loses focus.

My instinct is that, since the TextField is bound to the phoneNumber property of one of the array items, as soon as I modify it, the entire array within the class publishes the fact that it's been changed, hence SwiftUI dutifully rebuilds the ForEach loop, thus losing focus. This behavior is not ideal when trying to enter a new phone number!

I've also tried looping over an array of the PhoneDetails objects directly, without using an ObservedObject class as an in-between repository, and the same behavior persists.

Below is the minimum reproducible example code; as mentioned, adding/removing items works great, but attempting to type into any TextField immediately loses focus.

Can someone please help point me in the right direction as to what I'm doing wrong?

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())
        }
    }
}
  • 1
    I did find [this SO post](https://stackoverflow.com/questions/61840311/textfield-in-swiftui-loses-focus-when-i-enter-a-character) that asks a similar question, but its accepted answer did not help solve my problem. – Logan Kriete May 16 '21 at 07:16

1 Answers1

4

try this:

    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:

Reviewing my comment and in light of renewed interest, here is a version without using indices. It uses the ForEach with binding feature of 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())
        }
    }
}
   
  • Thank you! Your solution does indeed work, but per [another SO post's comment](https://stackoverflow.com/a/61687192), using `.indices` within a `ForEach` is not recommended since it can cause out-of-range errors. Why is using `.indices` not causing crashes in my code? – Logan Kriete May 16 '21 at 17:34
  • I've seen claims that using indices when adding or removing elements is not "recommended", the argument being that the range changes. Well this is not my experience and the example shows that it works without crashing. As far as I know when SwiftUI recomputes the view it will get the new range of indices avoiding any crash. This is what I see in this example. But then again I maybe wrong. – workingdog support Ukraine May 16 '21 at 23:13
  • This saved my life! Do we know why does the OP's code cause that problem, or is it a bug? It's been bothering me for hours – Jongwoo Lee Feb 05 '22 at 02:37
  • updated my answer with a (more robust) approach that does not use `indices` – workingdog support Ukraine Feb 05 '22 at 04:58