Post

Replies

Boosts

Views

Activity

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. 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) } } } } } } 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) } } } } }
4
0
150
2w