如何让SwiftUI ScrollView默认收起可折叠日历视图
默认收起的可折叠日历视图实现方案
方案一:用ScrollViewReader直接定位(推荐,无需计算高度)
不用纠结ScrollView总高度,给日历容器加锚点ID,视图加载时直接滚动到折叠状态的位置,同时默认将日历设为收起:
struct ContentView: View { // 默认收起日历 @State private var isCalendarCollapsed = true // 自定义日历展开/收起的高度 let expandedHeight: CGFloat = 320 let collapsedHeight: CGFloat = 60 var body: some View { ScrollViewReader { proxy in ScrollView { // 日历容器 VStack(spacing: 0) { // 顶部触发栏(点击切换展开/收起) HStack { Text("选择日期") .font(.headline) Spacer() Image(systemName: isCalendarCollapsed ? "chevron.down" : "chevron.up") } .padding() .onTapGesture { withAnimation(.easeInOut) { isCalendarCollapsed.toggle() } } // 日历内容(展开时显示) if !isCalendarCollapsed { CalendarView() .frame(height: expandedHeight - collapsedHeight) } } .frame(height: isCalendarCollapsed ? collapsedHeight : expandedHeight) .background(Color.gray.opacity(0.1)) .id("calendarCollapsedAnchor") // 锚点标记 // 下方数据展示区 VStack(spacing: 12) { ForEach(1...25, id: \.self) { index in Text("当日数据 \(index)") .padding() .frame(maxWidth: .infinity) .background(Color.white) .cornerRadius(8) .padding(.horizontal) } } } .onAppear { // 初始滚动到日历锚点,实现默认收起的视觉效果 withAnimation(.easeInOut) { proxy.scrollTo("calendarCollapsedAnchor", anchor: .top) } } // 监听滚动偏移,自动展开/收起(iOS 17+) .onScrollGeometryChange { geometry in let offset = geometry.contentOffset.y // 下滑到一定程度(offset为负)时展开日历 withAnimation(.easeInOut) { isCalendarCollapsed = offset > -60 } } } } } // 你的自定义日历视图 struct CalendarView: View { var body: some View { // 替换成你的日历实现(比如DatePicker、自定义日历组件) Text("日历内容区域") .frame(maxWidth: .infinity) .background(Color.blue.opacity(0.2)) } }
方案二:通过GeometryReader获取总高度适配defaultScrollAnchor
如果坚持要用defaultScrollAnchor,可以用GeometryReader包裹滚动内容,获取总高度后计算锚点位置:
struct ContentView: View { @State private var isCalendarCollapsed = true let expandedHeight: CGFloat = 320 let collapsedHeight: CGFloat = 60 @State private var totalScrollHeight: CGFloat = 0 var body: some View { ScrollView { GeometryReader { geo in VStack(spacing: 0) { // 日历容器(同方案一) VStack(spacing: 0) { HStack { Text("选择日期") .font(.headline) Spacer() Image(systemName: isCalendarCollapsed ? "chevron.down" : "chevron.up") } .padding() .onTapGesture { withAnimation(.easeInOut) { isCalendarCollapsed.toggle() } } if !isCalendarCollapsed { CalendarView() .frame(height: expandedHeight - collapsedHeight) } } .frame(height: isCalendarCollapsed ? collapsedHeight : expandedHeight) .background(Color.gray.opacity(0.1)) // 数据展示区 VStack(spacing: 12) { ForEach(1...25, id: \.self) { index in Text("当日数据 \(index)") .padding() .frame(maxWidth: .infinity) .background(Color.white) .cornerRadius(8) .padding(.horizontal) } } } .onAppear { // 获取滚动内容的总高度 totalScrollHeight = geo.size.height } } } // 计算锚点:滚动到内容顶部下方(总高度 - 日历收起高度)的位置 .defaultScrollAnchor(totalScrollHeight > 0 ? UnitPoint(x: 0, y: (totalScrollHeight - collapsedHeight)/totalScrollHeight) : .top) // 低版本iOS滚动监听(替换iOS17+的onScrollGeometryChange) .coordinateSpace(name: "scrollSpace") .overlay( GeometryReader { geo in Color.clear .preference(key: ScrollOffsetKey.self, value: geo.frame(in: .named("scrollSpace")).origin.y) } ) .onPreferenceChange(ScrollOffsetKey.self) { offset in withAnimation(.easeInOut) { isCalendarCollapsed = offset > -60 } } } } // 自定义偏好键(低版本iOS监听滚动偏移用) struct ScrollOffsetKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {} }
关键注意点
- 两个方案都将
isCalendarCollapsed初始值设为true,确保日历默认收起 - 方案一无需计算高度,逻辑更简洁,兼容性更好
- 若需兼容iOS16及以下,用方案二中的偏好键方式监听滚动偏移,替换iOS17+的
onScrollGeometryChange
内容的提问来源于stack exchange,提问作者Thiến Bảo Lê
相关产品推荐
相关产品推荐

