List jumps back to the top

Following on from this thread: https://developer.apple.com/forums/thread/805037 my list of items is now correctly maintaining state (no more disappearing rows), but I'm now hitting a really annoying issue: Every time something changes - even just changing the dark mode of the device - the entire list of items is refreshed, and the list jumps back to the top.

A simple representation:

// modelData.filteredItems is either all items or some items, depending on whether the user is searching
List {
	ForEach(modelData.filteredItems) { item in
		ItemRow(item: item)
	}
}

When the user isn't searching, filteredItems has everything in it. When they turn on search, I filter and sort the data in place:

// Called when the user turns on search, or when the searchString or searchType changes
func sortAndFilterItemsInModelData() {
	modelData.filteredItems.removeAll()  // Remove all items from the filtered array
	modelData.filteredItems.append(contentsOf: modelData.allItems)  // Add all items back in
	let searchString: String = modelData.searchString.lowercased()

	switch(modelData.searchType) {
		case 1:
			// Remove all items from the filtered array that don't match the search string
			modelData.filteredItems.removeAll(where: { !$0.name.lowercased().contains(searchString) })
		...
	}

	// Sorting
	switch(modelData.sortKey) {
		case sortKeyDate:
			modelData.sortAscending ? modelData.filteredItems.sort { $0.date < $1.date } : modelData.filteredItems.sort { $0.date > $1.date }  // Sorts in place
		...
	}
}

The method doesn't return anything because all the actions are done in place on the data, and the view should display the contents of modelData.filteredItems.

If you're searching and there are, say 10 items in the list and you're at the bottom of the list, then you change the search so there are now 11 items, it jumps back to the top rather than just adding the extra ItemRow to the bottom. Yes, the data is different, but it hasn't been replaced; it has been altered in place.

The biggest issue here is that you can simply change the device to/from Dark Mode - which can happen automatically at a certain time of day - and you're thrown back to the top of the list. The array of data hasn't changed, but SwiftUI treats it as though it has.

There's also a section in the List that can be expanded and contracted. It shows or hides items of a certain type. When I expand it, I expect the list to stay in the same place and just show the extra rows, but again, it jumps to the top. It's a really poor user experience.

Am I doing something wrong (probably, yes), or is there some other way to retain the scroll position in a List? The internet suggests switching to a LazyVStack, but I lose left/right swipe buttons and the platform-specific styling.

Thanks.

If that may help, I found this, but did not test, using ScrollViewReader, but you've probably found as well:

Get the scroll position: https://stackoverflow.com/questions/62588015/get-the-current-scroll-position-of-a-swiftui-scrollview

Set the scroll position (in .onAppear ?): https://www.hackingwithswift.com/quick-start/swiftui/how-to-scroll-to-a-specific-row-in-a-list

Or would it be enough to set the selection when it exists ? https://stackoverflow.com/questions/74365665/force-deselection-of-row-in-swiftui-list-view/74368833#74368833

Thanks @Claude31, I have found stuff like that, yeah.

For the section in my list that can be expanded and contracted, ScrollViewReader and .scrollTo work to keep you in the right place, so thanks for the hint.

This is the code for the List, if it helps:

ScrollViewReader { proxy in
	List {
		// Standard items
		ForEach(modelData.filteredItems.filter { !$0.archived }) { item in
			ItemRow(item)
		}

		// Archived items
		if(modelData.filteredItems.filter { $0.archived }.count > 0) {
			ListSectionHeader_Archived()
				.id("ListSectionHeader_Archived")
				.onTapGesture {
					modelData.archivedItemsExpanded.toggle()
					DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
						proxy.scrollTo("ListSectionHeader_Archived")
					}
				}

			if(modelData.archivedItemsExpanded) {
				ForEach(modelData.filteredItems.filter { $0.archived }) { item in
					ItemRow(item)
				}
			}
		}
	}  // List
	.id(UUID())
}

I still can't figure out a way to stop the list from jumping back to the top when the device's Dark Mode setting changes. Why does SwiftUI think the data is different?

Why does SwiftUI think the data is different?

Maybe it's not that it evaluates data has changed but just that it needs redraw (even only for dark mode switch). But I'm not expert enough in SwiftUI to be sure.

That's an issue I find with SwiftUI, understanding and possibly controlling what it is doing in the back or find a workaround. I'm still much more comfortable with UIKit.

I'm not sure either, @Claude31, but before I implemented the @StateObject for my model data this didn't happen.

I understand that using @StateObject is the correct thing to do, but why does SwiftUI throw in these extra problems when you're doing things the right way?

List jumps back to the top
 
 
Q