Search code examples
iosswiftswiftuibindingswiftui-list

Dynamic List with textfield binding and focus state causes infinite loop on iOS 18


I have a perfectly working List, which contains dynamic data and a text field with a binding and focus state in one of the row.

Since iOS 18, loading this list will cause an infinite loop at the first load due to a constant update of the binding value, despite I’m not updating it at this point.

Here is an example reproducing the problem that was working fine prior to iOS 18 :

struct MyTextFieldView: View {
  @Binding var text: String
  @FocusState private var isFocused: Bool

  private let title: String

  init(title: String,
       text: Binding<String>) {
    self.title = title
    self._text = text
  }

  var body: some View {
    TextField(title, text: $text)
      .focused($isFocused)
      .keyboardType(.asciiCapable)
  }
}

struct ListView: View {
  final class Row: ObservableObject, Identifiable {
    @Published var text: String = ""

    var id: String {
      UUID().uuidString
    }
  }

  @State private var data: [Row] = [
    .init(),
    .init()
  ]

  var body: some View {
    List($data) { row in
      Section {
        MyTextFieldView(title: row.id,
                        text: row.text)
      }
    }
  }
}

struct ContentView: View {
  var body: some View {
    NavigationView {
      NavigationLink(destination: ListView()) {
        Text("Go to list")
      }
    }
  }
}

When replacing the dynamic list with a static one, it doesn’t loop indefinitely:

    List {
      Section {
        MyTextFieldView(title: "1",
                        text: $text)
      }
    }

Can someone help me understand what’s going on?


Solution

  • I post the answer so anyone having the same problem when updating from iOS 17 to 18 will find the answer.

    As @Sweeper said in comments, the problem comes from the ID being a computed property. A new ID will be generated after each redraw.

    The following change fixes the problem :

      final class Row: ObservableObject, Identifiable {
        @Published var text: String = ""
    
        let id: String = UUID().uuidString
      }
    

    It doesn’t make sense to use a computed property, but it’s still strange that we never had the problem before on iOS 17. Something has definitely changed in the way iOS 18 detects the view needs to be redrawn.