I’m constructing a SwiftUI view with two synchronized scrollable areas:
- A horizontal ScrollView that shows an inventory of sections.
- A vertical ScrollView that shows content material corresponding to those sections.
Drawback:
The implementation works when every part has a uniform variety of objects. Nonetheless, when sections comprise various numbers of things, the synchronization breaks, and the vertical ScrollView usually scrolls to the improper part. Right here’s an instance of my code:
struct ContentView: View {
// Pattern knowledge
personal let sections = (1...10).map { sectionIndex in
SectionData(
title: "Part (sectionIndex)",
objects: (1...(Int.random(in: 80...150))).map { "Merchandise ($0)" }
)
}
@State personal var selectedSection: String? = nil
@State personal var currentVisibleSection: String? = nil
var physique: some View {
VStack(spacing: 0) {
// Horizontal Selector
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 10) {
ForEach(sections) { part in
Button(motion: {
selectedSection = part.title
}) {
Textual content(part.title)
.font(.headline)
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(currentVisibleSection == part.title ? Shade.blue : Shade.grey.opacity(0.2))
)
.foregroundColor(currentVisibleSection == part.title ? .white : .major)
}
}
}
.padding()
}
.background(Shade(UIColor.systemGroupedBackground))
// Vertical Scrollable Content material
ScrollViewReader { proxy in
ScrollView(.vertical, showsIndicators: false) {
LazyVStack(spacing: 20) {
ForEach(sections) { part in
VStack(alignment: .main, spacing: 10) {
// Part Header
SectionHeader(title: part.title)
.id(part.title) // Every part has a novel ID
// Part Content material
LazyVGrid(columns: Array(repeating: GridItem(.versatile()), depend: 3), spacing: 10) {
ForEach(part.objects, id: .self) { merchandise in
Textual content(merchandise)
.body(peak: 100)
.body(maxWidth: .infinity)
.background(Shade.blue.opacity(0.2))
.cornerRadius(8)
}
}
}
.background(
GeometryReader { geo in
Shade.clear.choice(
key: VisibleSectionPreferenceKey.self,
worth: [section.name: calculateVisibleHeight(geo)]
)
}
)
}
}
.onPreferenceChange(VisibleSectionPreferenceKey.self) { visibleSections in
updateLargestVisibleSection(visibleSections)
}
.onChange(of: selectedSection) { sectionName in
guard let sectionName else { return }
withAnimation {
proxy.scrollTo(sectionName, anchor: .prime)
}
}
}
}
}
}
// Replace the biggest seen part
personal func updateLargestVisibleSection(_ visibleSections: [String: CGFloat]) {
if let largestVisibleSection = visibleSections.max(by: { $0.worth < $1.worth })?.key {
currentVisibleSection = largestVisibleSection
}
}
// Calculate the seen peak of a bit
personal func calculateVisibleHeight(_ geometry: GeometryProxy) -> CGFloat {
let body = geometry.body(in: .international)
let screenHeight = UIScreen.foremost.bounds.peak
return max(0, min(body.maxY, screenHeight) - max(body.minY, 0))
}
}
// PreferenceKey to trace seen sections
personal struct VisibleSectionPreferenceKey: PreferenceKey {
static var defaultValue: [String: CGFloat] = [:]
static func cut back(worth: inout [String: CGFloat], nextValue: () -> [String: CGFloat]) {
worth.merge(nextValue(), uniquingKeysWith: max)
}
}
// Supporting Views and Fashions
struct SectionHeader: View {
let title: String
var physique: some View {
Textual content(title)
.font(.headline)
.padding()
.body(maxWidth: .infinity, alignment: .main)
.background(Shade.grey.opacity(0.2))
}
}
struct SectionData: Identifiable {
var id: String { title }
let title: String
let objects: [String]
}
- LazyVStack: Works nicely for efficiency, however synchronization breaks when sections comprise various numbers of things.
- VStack: Fixes synchronization points however introduces poor efficiency with massive knowledge units since all content material is eagerly loaded into reminiscence.
- Moreover, interacting with lazy subviews (like LazyVGrid) inside a VStack causes scroll jumps, breaking the person expertise.
- onPreferenceChange: Used a customized PreferenceKey to trace seen sections, however this method turns into unreliable with lazy-loaded sections and dynamic merchandise counts.