SwiftUI ScrollView 模式参考
目录
- 用于编程式滚动的 ScrollViewReader
- 滚动位置跟踪
- 滚动过渡与效果
- 滚动目标行为
- 汇总清单
用于编程式滚动的 ScrollViewReader
使用ScrollViewReader实现滚动到顶部、滚动到底部和基于锚点的跳转。
structChatView:View{@Stateprivatevarmessages:[Message]=[]privateletbottomID="bottom"varbody:someView{ScrollViewReader{proxyinScrollView{LazyVStack{ForEach(messages){messageinMessageRow(message:message).id(message.id)}Color.clear.frame(height:1).id(bottomID)}}.onChange(of:messages.count){_,_inwithAnimation{proxy.scrollTo(bottomID,anchor:.bottom)}}.onAppear{proxy.scrollTo(bottomID,anchor:.bottom)}}}}滚动到顶部模式
structFeedView:View{@Stateprivatevaritems:[Item]=[]@StateprivatevarscrollToTop=falseprivatelettopID="top"varbody:someView{ScrollViewReader{proxyinScrollView{LazyVStack{Color.clear.frame(height:1).id(topID)ForEach(items){iteminItemRow(item:item)}}}.onChange(of:scrollToTop){_,shouldScrollinifshouldScroll{withAnimation{proxy.scrollTo(topID,anchor:.top)}scrollToTop=false}}}}}原因:ScrollViewReader提供带稳定锚点的编程式滚动控制。始终使用稳定的 ID 和显式动画。
滚动位置跟踪
基本滚动位置
避免- 直接存储滚动位置会在每个滚动帧触发视图更新:
// ❌ 坏实践 - 导致不必要的重新渲染structContentView:View{@StateprivatevarscrollPosition:CGFloat=0varbody:someView{ScrollView{content.background(GeometryReader{geometryinColor.clear.preference(key:ScrollOffsetPreferenceKey.self,value:geometry.frame(in:.named("scroll")).minY)})}.coordinateSpace(name:"scroll").onPreferenceChange(ScrollOffsetPreferenceKey.self){valueinscrollPosition=value}}}推荐- 检查滚动位置并根据阈值更新标志,以获得更平滑、更高效的滚动:
// ✅ 好实践 - 仅在跨越阈值时更新状态structContentView:View{@StateprivatevarstartAnimation:Bool=falsevarbody:someView{ScrollView{content.background(GeometryReader{geometryinColor.clear.preference(key:ScrollOffsetPreferenceKey.self,value:geometry.frame(in:.named("scroll")).minY)})}.coordinateSpace(name:"scroll").onPreferenceChange(ScrollOffsetPreferenceKey.self){valueinifvalue<-100{startAnimation=true}else{startAnimation=false}}}}structScrollOffsetPreferenceKey:PreferenceKey{staticvardefaultValue:CGFloat=0staticfuncreduce(value:inoutCGFloat,nextValue:()->CGFloat){value=nextValue()}}基于滚动的头部可见性
structContentView:View{@StateprivatevarshowHeader=truevarbody:someView{VStack(spacing:0){ifshowHeader{HeaderView().transition(.move(edge:.top))}ScrollView{content.background(GeometryReader{geometryinColor.clear.preference(key:ScrollOffsetPreferenceKey.self,value:geometry.frame(in:.named("scroll")).minY)})}.coordinateSpace(name:"scroll").onPreferenceChange(ScrollOffsetPreferenceKey.self){offsetinifoffset<-50{// 向下滚动withAnimation{showHeader=false}}elseifoffset>50{// 向上滚动withAnimation{showHeader=true}}}}}}滚动过渡与效果
iOS 17+:本节中的所有 API 需要 iOS 17 或更高版本。
基于滚动的不透明度
structParallaxView:View{varbody:someView{ScrollView{LazyVStack(spacing:20){ForEach(items){iteminItemCard(item:item).visualEffect{content,geometryinletframe=geometry.frame(in:.scrollView)letdistance=min(0,frame.minY)returncontent.opacity(1+distance/200)}}}}}}视差效果
structParallaxHeader:View{varbody:someView{ScrollView{VStack(spacing:0){Image("hero").resizable().aspectRatio(contentMode:.fill).frame(height:300).visualEffect{content,geometryinletoffset=geometry.frame(in:.scrollView).minYreturncontent.offset(y:offset>0?-offset*0.5:0)}.clipped()ContentView()}}}}滚动目标行为
iOS 17+:本节中的所有 API 需要 iOS 17 或更高版本。
分页 ScrollView
structPagingView:View{varbody:someView{ScrollView(.horizontal){LazyHStack(spacing:0){ForEach(pages){pageinPageView(page:page).containerRelativeFrame(.horizontal)}}.scrollTargetLayout()}.scrollTargetBehavior(.paging)}}吸附到项目
structSnapScrollView:View{varbody:someView{ScrollView(.horizontal){LazyHStack(spacing:16){ForEach(items){iteminItemCard(item:item).frame(width:280)}}.scrollTargetLayout()}.scrollTargetBehavior(.viewAligned).contentMargins(.horizontal,20)}}汇总清单
- 使用带稳定 ID 的
ScrollViewReader进行编程式滚动 - 始终使用显式动画配合
scrollTo() - 基于滚动的视觉变化使用
.visualEffect - 分页行为使用
.scrollTargetBehavior(.paging) - 吸附到项目行为使用
.scrollTargetBehavior(.viewAligned) - 用阈值门控频繁的滚动位置更新
- 自定义滚动位置跟踪使用偏好键