SwiftUI List autoscroll disabling

In this simple app I don't want list scroll position to change when new items are added (to the top). The list correctly doesn't autoscroll when it's scrolled away from top, but if the scroll position is at top then list autoscrolls every time the new item is added on top. Is it possible to prohibit this autoscroll?

import SwiftUI

class Model: ObservableObject {
    @Published var items: [String] = []
    init() {
        for _ in 0 ..< 30 { addItem() }
        let timer = Timer.init(timeInterval: 1, repeats: true) { [self] _ in addItem() }
        RunLoop.main.add(timer, forMode: .common)
    }
    func addItem() {
        items.insert("Hello \(items.count)", at: 0)
    }
}

struct ContentView: View {
    @StateObject var model = Model()
    var body: some View {
        List {
            ForEach(model.items, id: \.self) { item in
                Text(item)
            }
        }.listStyle(.plain)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View { ContentView() }
}

@main struct TheApp: App {
    var body: some Scene {
        WindowGroup { ContentView() }
    }
}

See here.

isScrollEnabled overriding doesn't work for me. isScrollEnabled is not called in this case, although I can see it is being called on status bar taps (for scrollToTop functionality). In case of adding items to top according to asm setContentOffset in is being called inside setContentSize > _adjustContentOffsetIfNecessary (wish there was a way to say: "hey, it's not necessary".

The following horrible workaround fixes the problem when the list is scrolled to be at top.
It assumes the list is at top, it scrolls the list a bit from top (to the second top item), then insert items and then scrolls the list back to the original "top" item:

import SwiftUI

struct ContentView: View {
    @State var items: [Int] = (1 ..< 100).map { $0 }
    var body: some View {
        ScrollViewReader { proxy in
            VStack {
                Button("Add 10 items to top") {
                    let first = items.first!
                    DispatchQueue.main.async { [self] in
                        proxy.scrollTo(first + 1, anchor: .top)
                        DispatchQueue.main.async {
                            for _ in 0 ..< 10 {
                                items.insert(items.first! - 1, at: 0)
                            }
                            DispatchQueue.main.async {
                                proxy.scrollTo(first, anchor: .top)
                            }
                        }
                    }
                }

                List(items, id: \.self) { i in
                    Text("\(i)")
                    .id(i)
                }.listStyle(.plain)
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View { ContentView() }
}

@main struct TestApp: App {
    var body: some Scene {
        WindowGroup { ContentView() }
    }
}

Other than being a horrible hack there are two further problems with this fix:

  • somehow I need to not apply this fix when list is not at top. Don't see how.
  • it flickers a bit, not surprisingly.
Terms of Service

Privacy Policy

Cookie Policy