如何解决SwiftUI自定义Picker切换时的动画冲突问题
自定义Picker隐藏时动画卡顿问题解决
问题描述
开发了一个自定义Picker,点击后展开显示选项列表。在另一个视图中使用两个该类型的Picker,设置仅显示用户当前交互的Picker、隐藏另一个,但隐藏Picker时出现动画卡顿:会先停顿片刻再完全消失。
相关代码
ConceptOptionsView
struct ConceptOptionsView: View { @State private var aspectRatio: AspectRatio = .sixteenbynine @State private var imageModel: ImageModel = .flux(.schnell) @State private var resolution: ImageResolution = .seventwenty @State private var activePicker: ActivePicker? = nil enum ActivePicker { case model case resolution } var body: some View { VStack(spacing: 0) { Rectangle() .fill(.gray.opacity(0.3)) .frame(height: 0.5) .frame(maxWidth: .infinity) VStack(alignment: .leading, spacing: 20) { HStack { if activePicker == nil || activePicker == .model { PillSelectorView( title: "Model", icon: "sparkle", selection: $imageModel, items: ImageModel.pickerItems, didBeginSelection: { activePicker = .model }, didEndSelection: { activePicker = nil } ) .transition(.move(edge: .leading)) } Spacer() if activePicker == nil || activePicker == .resolution { PillSelectorView( title: "Resolution", icon: "camera.metering.center.weighted.average", selection: $resolution, items: ImageResolution.pickerItems, didBeginSelection: { activePicker = .resolution }, didEndSelection: { activePicker = nil } ) .transition(.move(edge: .trailing)) } } Text("The options you select will be the starting point when you want to generate a new image") .font(.footnote) .foregroundStyle(.secondary) .multilineTextAlignment(.center) .padding(.horizontal, 20) } .padding() Spacer() } .background(.ultraThinMaterial) .animation(.snappy, value: activePicker) } }
PillSelectorView 及相关结构
struct PillSelectorItem<SelectionValue: Hashable>: Identifiable { let id = UUID() let value: SelectionValue let title: String init( value: SelectionValue, title: String ) { self.value = value self.title = title } } struct PillSelectorView<SelectionValue: Hashable>: View { let title: String let icon: String @Binding var selection: SelectionValue let items: [PillSelectorItem<SelectionValue>] var didBeginSelection: () -> Void var didEndSelection: () -> Void @State private var isSelecting: Bool = false var body: some View { VStack(alignment: .leading, spacing: 5) { Text(title) .font(.caption.weight(.medium)) .foregroundStyle(.secondary) .padding(.leading, 10) HStack { Button { withAnimation { isSelecting.toggle() } } label: { HStack { Image(systemName: icon) .imageScale(.small) if !isSelecting { Text(items.first(where: { $0.value == selection })?.title ?? "Item") .font(.subheadline.weight(.medium)) } } .padding(.horizontal) .padding(.vertical, 12) .frame(maxWidth: isSelecting ? 40 : .infinity) .background(.gray.opacity(0.1), in: .rect(cornerRadius: 20)) } .buttonStyle(.plain) if isSelecting { ScrollView(.horizontal) { HStack { ForEach(items) { item in Button { withAnimation { selection = item.value isSelecting = false } } label: { Text(item.title) .font(.footnote.weight(.medium)) .foregroundStyle( item.value == selection ? .primary : .secondary ) .padding(.vertical, 10) .padding(.horizontal, 10) .overlay { Capsule() .fill( item.value == selection ? .gray.opacity(0.1) : .clear ) .stroke( item.value == selection ? .primary : Color.gray.opacity(0.3), lineWidth: 1.0 ) } } .buttonStyle(.plain) } } } .scrollIndicators(.hidden) .transition(.move(edge: .trailing)) } } } .onChange(of: isSelecting) { oldValue, newValue in newValue ? didBeginSelection() : didEndSelection() } } }
解决方案
1. 分离动画作用域,避免父子动画冲突
父视图的全局动画会与子视图内部的动画叠加,导致过渡卡顿。移除父视图的全局动画,改为在切换activePicker时使用显式动画:
修改ConceptOptionsView中Picker的回调部分:
// Model Picker的回调 didBeginSelection: { withAnimation(.snappy) { activePicker = .model } }, didEndSelection: { withAnimation(.snappy) { activePicker = nil } } // Resolution Picker的回调 didBeginSelection: { withAnimation(.snappy) { activePicker = .resolution } }, didEndSelection: { withAnimation(.snappy) { activePicker = nil } }
同时移除父视图底部的.animation(.snappy, value: activePicker)。
2. 优化Picker按钮的布局稳定性
原按钮的frame(maxWidth: isSelecting ? 40 : .infinity)会导致宽度突变,配合过渡动画容易引发布局计算延迟。修改为固定最小宽度,并给文本添加淡入淡出过渡:
修改PillSelectorView的按钮label:
Button { withAnimation { isSelecting.toggle() } } label: { HStack { Image(systemName: icon) .imageScale(.small) if !isSelecting { Text(items.first(where: { $0.value == selection })?.title ?? "Item") .font(.subheadline.weight(.medium)) .transition(.opacity) } } .padding(.horizontal) .padding(.vertical, 12) .frame(minWidth: 40, maxWidth: .infinity) .background(.gray.opacity(0.1), in: .rect(cornerRadius: 20)) }
3. 隔离ScrollView的动画影响
父视图的过渡动画会影响内部ScrollView的布局,给ScrollView添加动画禁用:
ScrollView(.horizontal) { // 原有内容 } .scrollIndicators(.hidden) .transition(.move(edge: .trailing)) .animation(nil, value: isSelecting)
说明
以上修改通过分离动画作用域避免冲突,优化布局减少计算延迟,隔离滚动容器的动画影响,从而解决隐藏Picker时的卡顿问题。
内容的提问来源于stack exchange,提问作者Tobi Omotayo
相关产品推荐
相关产品推荐

