不使用SwiftUI ScrollView实现自定义可拖拽滚动组件:内容顶部缺失问题求助
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
scrollOffsetandcontentSizeintoCustomScrollViewto keep scroll logic self-contained and clean. - Viewport Boundaries: The
GeometryReadergives us the exact size of the scroll area, and theScrollableContentContainerensures content is clipped to this space so it doesn't overflow unexpectedly. - Fixed Preference Key: Updated the
reducemethod 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
scrollOffsetgo above 0 (so content doesn't scroll past the top) - Don't let
scrollOffsetgo belowviewportSize.height - contentSize.height(so content doesn't scroll past the bottom)
内容的提问来源于stack exchange,提问作者shingo.nakanishi

