Issues with FocusState in List with views containing Textfields

We are having issues with implementing a List that has Views in it that contain a Textfield.

The criteria we are trying to achieve is

  • Select to edit the quantity of a product
  • Auto focus on the row with that textfield, with the textfield's contents selected
  • Display/Dismiss the keyboard
  • Mask other rows in the list while interacting with a qty field

We explored many routes and are looking for direction on what the designated approach is. This originally was a Tech Support Incident, and I was instructed to post here. There were 2 working project examples available if needed.

  1. In an implementation that has the FocusState on the parent view, we see collisions in animation / weird jumpiness

    // MARK: - Constant

    enum Constant {
        static let logTag = "AddReplenishmentProductView"
    }

    @Binding var state: ContentViewState

    // MARK: - Private Properties

    @State private var focusedLineItemId: String?

    // MARK: - Life cycle

    var body: some View {
        VStack {
            replenishmentProductList
        }
        .background(.tertiary)
        .navigationTitle("Add Products")
        .navigationBarTitleDisplayMode(.inline)
        .toolbarBackground(.visible, for: .navigationBar)
    }

    // MARK: - Private Computed properties

    @ViewBuilder
    private var replenishmentProductList: some View {
        ScrollViewReader { proxy in
            List {
                let list = Array(state.lineItems.enumerated())
                ForEach(list, id: \.1.product.id) { (index, lineItem) in
                    RowView(
                        lineItem: $state.lineItems[index],
                        focusedLineItemId: $focusedLineItemId
                    )
                    .id(lineItem.id.uuidString)
                    .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
                    .alignmentGuide(.listRowSeparatorLeading) { _ in
                        return 0
                    }
                    //  Blocks all the other rows that we are not focusing on.
                    .maskingOverlay(focusId: $focusedLineItemId, elementId: "\(lineItem.id)")
                }
                .listSectionSeparator(.hidden)
            }
            .listStyle(.plain)
            .scrollDismissesKeyboard(.never)
            .scrollContentBackground(.hidden)
            /* 
             We are looking for a solution that doesn't require us to have this onChange modifier
             whenever we want to change a focus.
             */
            .onChange(of: focusedLineItemId) {
                guard let lineItemId = focusedLineItemId else { return }
                /*  
                 We need to scroll to a whole RowView so we can see both done and cancel buttons.
                 Without this, the focus will auto-scroll only to the text field, due to updating FocusState.
                 
                 We are experiencing weird jumping issues. It feels like the animations for focus on
                 text field and RowView are clashing between each other.

                 To fix this, we added a delay to the scroll so the focus animation completes first and then we 
                 scroll to the RowView.
                 However, when we attempt to focus on a row that is partially shown, sometimes the RowView won't
                 update it's focus and won't focus ultimately on the TextField until we scroll.
                 */
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                    withAnimation {
                        //  We need to add the withAnimation call to animate the scroll to the whole row.
                        proxy.scrollTo(lineItemId, anchor: .top)
                    }
                }
            }
        }
    }
}
  1. In an implementation where the FocusState is on the row views, we see issues with actually being able to focus. When quantity field we tap is located on a row near the top/bottom of the screen it does not look to be identified correctly, and the ability to scrollTo / the keyboard being presented are broken.

struct ContentView: View {

    // MARK: - Constant

    enum Constant {
        static let logTag = "AddReplenishmentProductView"
    }

    @Binding var state: ContentViewState

    // MARK: - Private Properties

    @State private var focusedLineItemId: String?
    @FocusState private var focus: String?

    // MARK: - Life cycle

    var body: some View {
        VStack {
            replenishmentProductList
        }
        .background(.tertiary)
        .navigationTitle("Add Products")
        .navigationBarTitleDisplayMode(.inline)
        .toolbarBackground(.visible, for: .navigationBar)
    }

    // MARK: - Private Computed properties

    @ViewBuilder
    private var replenishmentProductList: some View {
        ScrollViewReader { proxy in
            List {
                let list = Array(state.lineItems.enumerated())
                ForEach(list, id: \.1.product.id) { (index, lineItem) in
                    RowView(
                        lineItem: $state.lineItems[index],
                        focusedLineItemId: $focusedLineItemId,
                        focus: $focus
                    )
                    .id(lineItem.id.uuidString)
                    .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
                    .alignmentGuide(.listRowSeparatorLeading) { _ in
                        return 0
                    }
                    //  Blocks all the other rows that we are not focusing on.
                    .maskingOverlay(focusId: $focusedLineItemId, elementId: "\(lineItem.id)")
                }
                .listSectionSeparator(.hidden)
            }
            .listStyle(.plain)
            .scrollDismissesKeyboard(.never)
            .scrollContentBackground(.hidden)
            /* 
             We are looking for a solution that doesn't require us to have this onChange modifier
             whenever we want to change a focus.
             */
            .onChange(of: focusedLineItemId) {
                /* 
                 We need to scroll to a whole RowView so we can see both done and cancel buttons.
                 Without this, the focus will auto-scroll only to the text field, due to updating FocusState.

                 However, we are experiencing weird jumping issues. It feels like the animations for focus on
                 text field and RowView are clashing between each other.
                 */
                focus = focusedLineItemId
                guard let lineItemId = focusedLineItemId else { return }
                withAnimation {
                    //  We need to add the withAnimation call to animate the scroll to the whole row.
                    proxy.scrollTo(lineItemId, anchor: .top)
                }
            }
        }
    }
}
Issues with FocusState in List with views containing Textfields
 
 
Q