You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

不使用SwiftUI ScrollView实现自定义可拖拽滚动组件:内容顶部缺失问题求助

Fixing Top Content Clipping in Custom SwiftUI ScrollView (Pre-Drag Implementation)

Hey there! Let's tackle that top content missing issue first. Looking at your code, the core problem is that your CustomScrollView isn't setting up a proper bounded viewport for your content—right now it's just passing the content through directly, which means when the content is taller than the parent view, SwiftUI's default layout might clip the top without you realizing it.

Here's how to fix it step by step:

1. Add a Viewport Container to CustomScrollView

We'll use GeometryReader to grab the parent view's size (this will be our scrollable viewport). Then we'll wrap your content in a clipped container, and control its initial position to align perfectly with the top of the viewport.

2. Fix Initial Content Position

We'll add a state variable for the scroll offset in CustomScrollView (instead of keeping it in ContentView—it makes more sense to encapsulate scroll logic in the custom component). Initially, this offset will be 0, ensuring the content starts at the top.

3. Ensure Proper Content Size Reading

Your ChildSizeReader looks mostly good, but we'll fix the PreferenceKey's reduce method to actually capture the content's size correctly.

Here's the revised code with these fixes:

import SwiftUI

struct ContentView: View {
    var body: some View {
        CustomScrollView {
            ForEach(0..<100) { i in
                Text("\(i)")
                    .frame(maxWidth: .infinity)
                    .background(Color.green)
            }
        }
        // Optional: Add a border to visualize the scroll view bounds
        .border(Color.red)
    }
}

struct CustomScrollView<Content: View>: View {
    let content: Content
    @State private var contentSize: CGSize = .zero
    @State private var scrollOffset: CGFloat = 0
    
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    
    var body: some View {
        GeometryReader { viewportProxy in
            // Clipped container defines our scrollable viewport
            ScrollableContentContainer(viewportSize: viewportProxy.size) {
                content
                    .size(size: $contentSize)
            }
            .offset(y: scrollOffset)
        }
    }
    
    // Helper to constrain content to viewport bounds
    private struct ScrollableContentContainer<Content: View>: View {
        let viewportSize: CGSize
        let content: Content
        
        var body: some View {
            content
                .frame(minHeight: viewportSize.height) // Ensure container matches viewport height at minimum
                .clipped() // Clip content that extends outside the viewport
        }
    }
}

//==================================================
// For extension
//==================================================
struct ChildSizeReader<Content: View>: View {
    @Binding var size: CGSize
    let content: () -> Content
    
    var body: some View {
        content()
            .background(
                GeometryReader { proxy in
                    Color.clear
                        .preference(key: SizePreferenceKey.self, value: proxy.size)
                }
            )
            .onPreferenceChange(SizePreferenceKey.self) { newSize in
                self.size = newSize
            }
    }
}

struct SizePreferenceKey: PreferenceKey {
    typealias Value = CGSize
    static var defaultValue: Value = .zero
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value = nextValue() // Fixed: actually update with the latest measured size
    }
}

extension View {
    func size(size: Binding<CGSize>) -> some View {
        ChildSizeReader(size: size) {
            self
        }
    }
}

Key Fixes Explained:

  • Encapsulated Scroll State: Moved scrollOffset and contentSize into CustomScrollView to keep scroll logic self-contained and clean.
  • Viewport Boundaries: The GeometryReader gives us the exact size of the scroll area, and the ScrollableContentContainer ensures content is clipped to this space so it doesn't overflow unexpectedly.
  • Fixed Preference Key: Updated the reduce method to actually capture the content's size—your original code was ignoring the new measurement, which could have caused incorrect size readings.

Next Steps for Drag Scroll:

Once the top content displays correctly, you can add a DragGesture to CustomScrollView to update scrollOffset. You'll also want to add bounds checking to prevent over-scrolling:

  • Don't let scrollOffset go above 0 (so content doesn't scroll past the top)
  • Don't let scrollOffset go below viewportSize.height - contentSize.height (so content doesn't scroll past the bottom)

内容的提问来源于stack exchange,提问作者shingo.nakanishi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 05:12:41