ios – SwiftUI Sync Horizontal and Vertical ScrollViews with Dynamic Content material

ios – SwiftUI Sync Horizontal and Vertical ScrollViews with Dynamic Content material


I’m constructing a SwiftUI view with two synchronized scrollable areas:

  1. A horizontal ScrollView that shows an inventory of sections.
  2. 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]
}

  1. LazyVStack: Works nicely for efficiency, however synchronization breaks when sections comprise various numbers of things.
  2. 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.
  1. onPreferenceChange: Used a customized PreferenceKey to trace seen sections, however this method turns into unreliable with lazy-loaded sections and dynamic merchandise counts.

Leave a Reply

Your email address will not be published. Required fields are marked *