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
})
}
}