潘星宇
潘星宇

Reputation: 51

SwiftUI How to scroll the outer scrollview when the inner view is already at the top

enter image description here

I have a target page like this. The entire page contains four parts.

First Problem

The firs problem that bothers me is that when the user is in the initial state of the page, the list at the bottom will slide directly at the bottom instead of pushing the parent scroll view to scroll collaboratively.

enter image description here

Second Problem

enter image description here

Here is the demo code. I use a horizontal scrollview and scrollTargetBehavior api to switch tabs. Each tab is an inner vertical scrollview which contains a card list in VStack.

import SwiftUI

struct ContentView: View {
    @State private var currentTab: String? = "Posted"
    @State private var tabProgress: CGFloat = 0
    
    var body: some View {
        let screenWidth: CGFloat = UIScreen.main.bounds.width
        let bw: CGFloat = UIScreen.main.bounds.width / 375
        let screenHeight: CGFloat = UIScreen.main.bounds.height
        
        GeometryReader { geometry in
            ZStack {
                // Sticky top bar, on the top of any other views
                VStack {
                    VStack {
                        NavRow()
                            .padding(.top, geometry.safeAreaInsets.top)
                    }
                    .frame(alignment: .top)
                    .background(.green)
                    Spacer()
                }
                .zIndex(999)
                
                // Outer Scroll View
                ScrollView (.vertical, showsIndicators: false) {
                    VStack (spacing: 0) {
                        // Same height as the top nav row
                        Spacer().frame(height: 64 * bw + geometry.safeAreaInsets.top)
                        // Info View
                        InfoView()
                        // Tab Row
                        TabRow(currentTab: $currentTab)
                    }
                    // Scroll Views
                    // Use horizontal paging scroll to implement tab switching
                    ScrollView(.horizontal, showsIndicators: false) {
                        LazyHStack(spacing: 0) {
                            ScrollView (.vertical, showsIndicators: false) {
                                PostedList()
                            }
                            .frame(width: screenWidth, height: screenHeight - 112 * bw - geometry.safeAreaInsets.top)
                            .id("Posted")
                            ScrollView (.vertical, showsIndicators: false) {
                                LikedList()
                            }
                            .frame(width: screenWidth, height: screenHeight - 112 * bw - geometry.safeAreaInsets.top)
                            .id("Liked")
                        }
                        .scrollTargetLayout()
                        .offsetX { value in
                            /// Converting Offset into Progress
                            let progress = -value / (screenWidth * CGFloat(1))
                            /// Capping Progress BTW 0-1
                            tabProgress = max(min(progress, 1), 0)
                        }
                    }
                    .scrollPosition(id: $currentTab)
                    .scrollTargetBehavior(.paging)
                    .scrollClipDisabled()
                }
                .ignoresSafeArea()
                .frame(width: screenWidth, height: screenHeight)
            }
            .ignoresSafeArea()
            .frame(width: screenWidth, height: screenHeight)
        }

    }
}

// Top nav row
struct NavRow: View {
    var body: some View {
        let screenWidth: CGFloat = UIScreen.main.bounds.width
        let bw: CGFloat = UIScreen.main.bounds.width / 375
        
        Rectangle().fill(.clear)
            .frame(width: screenWidth, height: 64 * bw)
            .overlay(alignment: .center, content: {
                Text("Nav Row")
                    .font(.system(size: 18, weight: .semibold))
                    .foregroundColor(.white)
            })
    }
}

struct InfoView: View {
    var body: some View {
        let bw: CGFloat = UIScreen.main.bounds.width / 375
        
        Rectangle().fill(.red.opacity(0.6))
            .frame(width: 343 * bw, height: 212 * bw)
            .cornerRadius(12 * bw)
            .overlay {
                Text("Info View")
                    .foregroundColor(.white)
                    .font(.system(size: 24, weight: .semibold))
            }
    }
}

struct TabRow: View {
    @Binding var currentTab: String?
    
    @ViewBuilder
    func SingleTab(tab: String) -> some View {
        let isCurrent = currentTab == tab
        Text(tab)
            .font(.system(size: isCurrent ? 16 : 15, weight:  isCurrent ? .semibold : .regular))
            .onTapGesture {
                if (!isCurrent) {
                    withAnimation(.snappy) {
                        currentTab = tab
                    }
                }
            }
    }
    
    var body: some View {
        let screenWidth: CGFloat = UIScreen.main.bounds.width
        let bw: CGFloat = UIScreen.main.bounds.width / 375
        
        Rectangle().fill(.yellow.opacity(0.5))
            .frame(width: screenWidth, height: 64 * bw)
            .overlay(alignment: .leading, content: {
                HStack {
                    SingleTab(tab: "Posted")
                    Spacer().frame(width: 24 * bw)
                    SingleTab(tab: "Liked")
                }
                .padding(.leading, 16 * bw)
            })
    }
}

@ViewBuilder
func sampleCard(cardText: String, cardColor: Color) -> some View {
    let bw: CGFloat = UIScreen.main.bounds.width / 375
    
    Rectangle().fill(cardColor)
        .frame(width: 343 * bw, height: 166 * bw)
        .overlay {
            Text(cardText)
                .font(.system(size: 18, weight: .semibold))
        }
        .padding(.bottom, 32 * bw)
}

struct PostedList: View {
    // 11 colors
    private let colorList: [Color] = [.gray, .red, .purple, .orange, .blue, .yellow, .pink, .brown, .cyan, .indigo, .teal]
    
    var body: some View {
        let screenWidth: CGFloat = UIScreen.main.bounds.width
        
        VStack (spacing: 0) {
            ForEach(0...colorList.count - 1, id: \.self) { i in
                sampleCard(cardText: "Post \(i)", cardColor: colorList[i])
            }
        }
        .frame(width: screenWidth)
    }
}


struct LikedList: View {
    var body: some View {
        let screenWidth: CGFloat = UIScreen.main.bounds.width
        
        VStack (spacing: 0) {
            ForEach(0...19, id: \.self) { i in
                sampleCard(cardText: "Liked \(i)", cardColor: .gray.opacity(0.2))
            }
        }
        .frame(width: screenWidth)
    }
}

/// Offset Key
struct OffsetKey: PreferenceKey {
    static var defaultValue: CGFloat = .zero
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

extension View {
    @ViewBuilder
    func offsetX(completion: @escaping (CGFloat) -> ()) -> some View {
        self
            .overlay {
                GeometryReader {
                    let minX = $0.frame(in: .scrollView(axis: .horizontal)).minX
                    Color.clear
                        .preference(key: OffsetKey.self, value: minX)
                        .onPreferenceChange(OffsetKey.self, perform: completion)
                }
            }
    }
}

I've been stuck with this problem for a few days and would really appreciate it if anyone could offer some help.

Upvotes: 1

Views: 115

Answers (0)

Related Questions