SwiftUI ScrollViewProxy.scrollTo(_:anchor:) always crash in iOS 16 beta 7: NSInternalInconsistencyException

Hello,

I found that ScrollViewProxy.scrollTo(_:anchor:) always crash on iOS 16 beta 1-7. As soon as the Section in the List changes, the ScrollViewProxy does not scroll properly to the specified cell. This issue only appeared on iOS 16.

Sample Code (for Xcode 14)

import SwiftUI

struct ContentView: View {
    @State private var hideSection1 = false

    var body: some View {
        ScrollViewReader { scrollView in
            List {
                if !hideSection1 {
                    Section(header: Text("Section 1")) {
                        ForEach(1...10, id: \.self) { i in
                            Text("\(i)")
                        }
                    }
                }

                Section(header: Text("Section 2")) {
                    ForEach(11...20, id: \.self) { i in
                        Text("\(i)")
                    }
                }
            }
            .safeAreaInset(edge: .bottom, spacing: 0) {
                VStack {
                    // Crash Steps:
                    //   1. Tap "Go to 20", OK ✅
                    //   2. Hide Section 1
                    //   3. Tap "Go to 20" again <--- ☠️ crash
                    Button("Go to 20") {
                        withAnimation {
                            scrollView.scrollTo(20, anchor: .center)
                        }
                    }

                    Toggle("Hide Section 1", isOn: $hideSection1)
                }
                .padding()
                .background {
                    Rectangle()
                        .fill(.ultraThinMaterial)
                        .ignoresSafeArea()
                }
            }
        }
    }
}

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

Crash Message

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Attempted to scroll the collection view to an out-of-bounds section (1) when there are only 1 sections. Collection view: <SwiftUI.UpdateCoalescingCollectionView: 0x13c88cc00; baseClass = UICollectionView; frame = (0 0; 390 844); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x60000323fc60>; backgroundColor = <UIDynamicSystemColor: 0x600002922dc0; name = systemGroupedBackgroundColor>; layer = <CALayer: 0x600003c2f260>; contentOffset: {0, -47}; contentSize: {390, 498.66666666666669}; adjustedContentInset: {47, 0, 129.66666666666674, 0}; layout: <UICollectionViewCompositionalLayout: 0x13c510ed0>; dataSource: <_TtGC7SwiftUI31UICollectionViewListCoordinatorGOS_19SelectionManagerBoxOs5Never__: 0x13c513670>>.'

Any resolution to this? I'm getting this crash too. My code seemed to work fine until I built with Xcode 14.0 (release), although I'm not sure that Xcode 14 the determining factor.

@Gong

I add items to the existing sections. When they tap the item at the top of the section, that section expands with additional items. I want the section to scroll to top. But because of this bug, one you expand and scroll to a section, then try to expand and scroll to another section that comes after the first one expanded, the app crashes.

To mitigate the crash, I give the list a view Id, which, for iOS 16, is based on the expanded section. Now when the expandedItemId changes, the whole list reloads and the scroll happens without crashing!

It's not perfect, but it gets the item to the top without crashing.

Now, when I tap the item again to collapse it, when the list reloads, it is positioned at the top of the list. That's why I added the scrolledToTopItemId. I set it when the item was expanded and the list scrolled to that item. Now when the item is collapsed, and the view is refreshed (because of the list view id), I can scroll to the previously selected item so that the list maintains its context.

I'm open for betters ideas. This isn't perfect, but so far its as good as I've been able to come up with

    @State private var expandedItemId: String?
    @State private var scrolledToTopItemId: String?
    private var iOS16CrashAvoidingViewId: String {
        guard #available(iOS 16, *) else { return "-view" }
        // This prevents a iOS 16 crash when to you expand a key indicator further down the screen from the 1st one you expanded. With this solution,
        // each key indicator doesn't scroll quite to the top: 1st one does, 2nd a little lower, third one yet lower, etc. But at least it doesn't crash!
        // Changing the view ID causes the whole to get reloaded when ever the expanded section changes.
        return "\(expandedItemId ?? "")-view"
    }


    var body: some View {
        ScrollViewReader { scrollView in
            List {
                statusSection(title: "not_submitted".localized, items: agent.initiatedForms)
                statusSection(title: "pending".localized,       items: agent.pendingForms)
                statusSection(title: "returned".localized,      items: agent.rejectedForms)
                statusSection(title: "success".localized,       items: agent.successForms)
            }
            .id(iOS16CrashAvoidingViewId)
            .onChange(of: expandedItemId) { newValue in
                withAnimation {
                    if let value = newValue {
                        scrollView.scrollTo(value, anchor: .top)
                        scrolledToTopItemId = value
                    }
                    DispatchQueue.main.async {
                        guard scrolledToTopItemId != nil else { return }
                        withAnimation {
                            scrollView.scrollTo(scrolledToTopItemId, anchor: .top)
                        }
                    }
                }
            }
        }
        .navigationTitle("Submitted Forms")
    }

I ran into this crash with ios 16 as well. Here is what I eventually came up with that works:

  • use the SwiftUI Introspection library to get the UIScrollView
  • interact with the UIScrollView directly

This code works on ios 15 and ios 16. There is likely tweaks to be done to make it feel right. I had to tweak the timing of asyncAfter() in my code where the keyboard is animated. Using .now() + 0.25 seems to do the trick. So far this has been working and has been stable.

struct ScrollListOnChangeIos16: View {
    @State private var items: [String]
    
    init() {
        _items = State(initialValue: Array(0...25).map { "Placeholder \($0)" } )
    }

    // The .introspectX view modifiers will populate scroller
    // they are actually UITableView or UICollectionView which both decend from UIScrollView
    // https://github.com/siteline/SwiftUI-Introspect/releases/tag/0.1.4
    @State private var scroller: UIScrollView?
    
    func scrollToTop() {
        scroller?.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
    }
    
    func scrollToBottom() {
        // Making this async seems to make scroll more consistent to happen after
        // items has been updated. *shrug?*
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {        
            guard let scroller = self.scroller else { return }
            let yOffset = scroller.contentSize.height - scroller.frame.height
            if yOffset < 0 { return }
            scroller.setContentOffset(CGPoint(x: 0, y: yOffset), animated: true)
        }
    }
        
    var body: some View {
        VStack {
            
            HStack {
                Button("Top") {
                    scrollToTop()
                }
                Spacer()
                
                Button("Add Item") {
                    items.append(Date.timeIntervalSinceReferenceDate.description)
                    scrollToBottom()
                }.buttonStyle(.borderedProminent)

                Spacer()
                Button("Bottom") {
                    scrollToBottom()
                }
            }.padding()

            // The source of all my pain ...
            List{
                ForEach(items, id: \.self) {
                    Text($0)
                }
                .onDelete { offsets in
                    items.remove(atOffsets: offsets)
                }
            }
            .listStyle(.plain)
            .padding(.bottom, 50)
            
        }
            
        /* in iOS 16 List is backed by UICollectionView, no out of the box .introspectMethod ... nbd. */
        .introspect(selector: TargetViewSelector.ancestorOrSiblingContaining, customize: { (collectionView: UICollectionView) in
            guard #available(iOS 16, *) else { return }
            self.scroller = collectionView
        })

        /* in iOS 15 List is backed by UITableView ... */
        .introspectTableView(customize: { tableView in
            guard #available(iOS 15, *) else { return }
            self.scroller = tableView
        })
    }
}

Confirmed still crash in Xcode 14.1 RC + iOS 16.1 RC

Still crash on iOS 16.3 + Xcode 14.2

Xcode 14.3 Beta, still an issue

@mostlygeek thanks the solution using Introspect worked for the project I'm working on

SwiftUI ScrollViewProxy.scrollTo(_:anchor:) always crash in iOS 16 beta 7: NSInternalInconsistencyException
 
 
Q