If you have ever built an editable list in SwiftUI using List and TextField, you may have encountered a frustrating quirk: when you tap on a text field, the keyboard springs up immediately, but the view waits around half a second before awkwardly scrolling the active row into view.

This noticeable lag breaks the fluid, native feel of an iOS app. Fortunately, you can eliminate this delay and achieve instant, smooth keyboard scrolling. Let's look at why this happens and how to fix it.

Why Does the Delay Occur?

In SwiftUI, List is backed under the hood by UICollectionViewController / UITableView. By default, UIKit applies automatic content inset adjustments when the software keyboard appears. SwiftUI also attempts to handle safe-area keyboard avoidance.

Because the framework waits for the keyboard's layout animation pass and the focus transition to fully coordinate, the scroll event gets postponed, causing that annoying ~0.5-second pause. To fix this, you need to take control of scrolling explicitly as soon as the focus state changes.

Solution 1: Use ScrollViewReader with an Immediate Focus Trigger

The most reliable way to bypass the default delayed scroll animation is to wrap your List in a ScrollViewReader and trigger an explicit scrollTo command immediately when focusedField changes.

Here is how you can update your code:

import SwiftUI
import SwiftData

@Model
final class TaskItem {
    var id: UUID = UUID()
    var title: String = ""
    init(title: String) {
        self.title = title
    }
}

struct DynamicListKeyboardTest: View {
    @Query private var taskItems: [TaskItem]
    @FocusState private var focusedField: UUID?

    var body: some View {
        ScrollViewReader { proxy in
            List {
                ForEach(taskItems) { task in
                    @Bindable var task = task
                    TextField("Task Title", text: $task.title, axis: .vertical)
                        .textFieldStyle(.roundedBorder)
                        .focused($focusedField, equals: task.id)
                        .id(task.id) // Ensure the item has an explicit ID for the reader
                }
            }
            .listStyle(.insetGrouped)
            .scrollDismissesKeyboard(.interactively)
            .onChange(of: focusedField) { _, newFocus in
                guard let newFocus else { return }
                // Animate immediately without waiting for default UIKit keyboard avoidance
                withAnimation(.easeOut(duration: 0.25)) {
                    proxy.scrollTo(newFocus, anchor: .center)
                }
            }
        }
    }
}

Why anchor: .center?

Using anchor: .bottom can cause the field to sit right on top of the keyboard or occasionally jitter if the keyboard frame changes (for instance, with the predictive text bar). Using anchor: .center centers the field in the remaining visible viewport, ensuring plenty of breathing room above and below.

Solution 2: Use Modern iOS 17+ Scroll APIs

If you are targeting iOS 17 or later, you can leverage native target-based scroll positioning with .scrollPosition(id:). While this is primarily designed for ScrollView, you can combine ScrollView + LazyVStack if you want pixel-perfect, conflict-free keyboard handling:

struct ModernScrollViewKeyboardTest: View {
    @Query private var taskItems: [TaskItem]
    @FocusState private var focusedField: UUID?
    @State private var scrollPosition: UUID?

    var body: some View {
        ScrollView {
            LazyVStack(spacing: 12) {
                ForEach(taskItems) { task in
                    @Bindable var task = task
                    TextField("Task Title", text: $task.title, axis: .vertical)
                        .textFieldStyle(.roundedBorder)
                        .focused($focusedField, equals: task.id)
                        .id(task.id)
                }
            }
            .padding()
        }
        .scrollPosition(id: $scrollPosition, anchor: .center)
        .scrollDismissesKeyboard(.interactively)
        .onChange(of: focusedField) { _, newFocus in
            if let newFocus {
                withAnimation(.snappy(duration: 0.25)) {
                    scrollPosition = newFocus
                }
            }
        }
    }
}

Key Takeaways

  • Add Explicit View IDs: Make sure your target rows have .id(...) applied so the scroll reader can identify them instantly.
  • Listen to .onChange(of: focusedField): Intercepting the focus state allows you to dispatch the scroll command in parallel with the keyboard appearance rather than waiting for UIKit's internal avoidance loop.
  • Pick the Right Anchor: Prefer .center over .bottom to keep multiline TextField controls clearly visible without being obscured by auto-complete bars.