Updated to provide full reproducible example.
I have a view MainView
with two sub-views, MainTextView
and TableOfContentsView
. I want to select an item in TableOfContentsView
which triggers a scroll position change in MainTextView
.
I have an @State var scrollPosition: Int
in MainView
which is passed to an @Binding var scrollPosition: Int
in MainTextView
. Here is the code for my 3 views.
struct MainView: View {
@State private var showingToc = false
@State private var scrollPosition = 0
var body: some View {
VStack(alignment: .leading) {
Text("Table of Contents (chapter \(scrollPosition + 1))")
.onTapGesture {
showingToc = !showingToc
}
Divider()
if showingToc {
TableOfContentsView(
onChapterSelected: { chapter in
scrollPosition = chapter
showingToc = false
}
)
} else {
MainTextView(scrollPosition: $scrollPosition)
}
}
}
}
struct TableOfContentsView: View {
var onChapterSelected: (Int) -> Void
var body: some View {
ScrollView {
VStack {
ForEach(0 ..< 20) { i in
Text("Chapter \(i + 1)")
.onTapGesture {
onChapterSelected(i)
}
}
}
}
}
}
struct MainTextView: View {
let lorumIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
@Binding var scrollPosition: Int
@State private var scrollProxy: ScrollViewProxy? = nil
var body: some View {
ScrollView {
ScrollViewReader { proxy in
VStack(alignment: .leading) {
ForEach(0 ..< 20) { i in
VStack(alignment: .leading) {
Text("Chapter \(i + 1)")
.frame(maxWidth: .infinity, alignment: .center)
.padding(.top)
Text(lorumIpsum)
.font(.system(size: 18, design: .serif))
.padding(.top)
}
.padding(.bottom, 20)
.id(i)
}
}
.padding(.leading)
.padding(.trailing)
.onAppear {
scrollProxy = proxy
}
}
}
.onChange(of: scrollPosition) { target in
scrollProxy?.scrollTo(target, anchor: .top)
}
}
}
My .onChange
function is never called - if I place a breakpoint inside, it never hits the breakpoint. What am I missing here? How do I observe an @Binding
that is changed from outside of the view?
.onChange
is not going to get called cause MainTextView
doesn't exist when showingToc = true
because of the conditional block:
if showingToc {
TableOfContentsView(onChapterSelected: { chapter in
scrollPosition = chapter
showingToc = false
}
)
} else {
MainTextView(scrollPosition: $scrollPosition)
}
You might want to consider showing TOC as an overlay. But to get your current code working, you need to call proxy.scrollTo
in your .onAppear
of MainTextView
:
.onAppear {
scrollProxy = proxy
scrollProxy?.scrollTo(scrollPosition, anchor: .top)
}