This question is coming on the heels of this question that I asked (and had answered by @Asperi) yesterday, but it introduces a new unexpected element.
The basic setup is a 3 column macOS SwiftUI app. If you run the code below and scroll the list to an item further down the list (say item 80) and click, the List
will re-render and occasionally "jump" to a place (like item 40), leaving the actual selected item out of frame. This issue was solved in the previous question by encapsulating SidebarRowView
into its own view.
However, that solution works if the active binding (activeItem
) is stored as a @State
variable on the SidebarList
view (see where I've marked //#1
). If the active item is stored on an ObservableObject
view model (see //#2
), the scrolling behavior is affected.
I assume this is because the diffing algorithm somehow works differently with the @Published
value and the @State
value. I'd like to figure out a way to use the @Published
value since the active item needs to be manipulated by the state of the app and used in the NavigationLink
via isActive:
(say if a push notification comes in that affects it).
Is there a way to use the @Published
value and not have it re-render the whole List
and thus not affect the scrolled position?
Reproducible code follows -- see the commented line for what to change to see the behavior with @Published
vs @State
struct Item : Identifiable, Hashable {
let id = UUID()
var name : String
}
class SidebarListViewModel : ObservableObject {
@Published var items = Array(0...300).map { Item(name: "Item \($0)") }
@Published var activeItem : Item? //#2
}
struct SidebarList : View {
@StateObject private var viewModel = SidebarListViewModel()
@State private var activeItem : Item? //#1
var body: some View {
List(viewModel.items) {
SidebarRowView(item: $0, activeItem: $viewModel.activeItem) //change this to $activeItem and the scrolling works as expected
}.listStyle(SidebarListStyle())
}
}
struct SidebarRowView: View {
let item: Item
@Binding var activeItem: Item?
func navigationBindingForItem(item: Item) -> Binding<Bool> {
.init {
activeItem == item
} set: { newValue in
if newValue {
activeItem = item
}
}
}
var body: some View {
NavigationLink(destination: Text(item.name),
isActive: navigationBindingForItem(item: item)) {
Text(item.name)
}
}
}
struct ContentView : View {
var body: some View {
NavigationView {
SidebarList()
Text("No selection")
Text("No selection")
.frame(minWidth: 300)
}
}
}
(Built and tested with Xcode 13.0 on macOS 11.3)