SwiftUI问题:父视图withAnimation引发子视图意外动画
问题分析与解决方案
问题根源
- 父视图
ContentView中通过withAnimation更新label时,会触发整个视图树重绘,包括弹窗里的PresetsView——尽管PresetsView自身状态没有变化。 - SwiftUI的动画会通过环境传递到子视图,导致
LazyVGrid在无配置动画的情况下出现错位动画。 PresetsView未做重绘优化,无法避免不必要的刷新。
解决方案步骤
1. 阻断动画传递到Sheet
在ContentView的sheet修饰符中添加transaction,禁用动画继承,避免父视图的动画影响弹窗内的布局:
.sheet(isPresented: $showPresets) { PresetsView(onDismiss: presetsViewDidDismiss, buttonAction: saveMessage) .presentationDetents([.fraction(0.05), .fraction(0.6), .large], selection: $currentDetent) .presentationDragIndicator(.hidden) .transaction { $0.disablesAnimations = true } // 阻止父动画传递 }
2. 让PresetsView支持Equatable优化重绘
让PresetsView遵循Equatable协议,仅在自身状态变化时才重绘:
struct PresetsView: View, Equatable { static func == (lhs: PresetsView, rhs: PresetsView) -> Bool { // 仅比较自身可变状态,忽略闭包(因为闭包逻辑固定) lhs.selectedPresetCatagoriesArray == rhs.selectedPresetCatagoriesArray } // 原属性与body代码保持不变 }
然后在sheet中使用时添加.equatable()修饰符:
PresetsView(onDismiss: presetsViewDidDismiss, buttonAction: saveMessage) .equatable() // 其他修饰符...
3. 缩小动画作用范围(可选)
将动画仅作用于需要动效的Text,避免全局动画触发不必要的重绘:
// 修改ContentView中的Text Text(label) .animation(.easeInOut, value: label) // 仅Text变化时触发动画 // 修改onReceive逻辑,移除全局withAnimation .onReceive(viewModel.$text, perform: { text in label = text })
修改后的完整代码
class MessageModel: ObservableObject{ @Published var text = "" func setText(message: String){ text = message } } struct ContentView: View { @State private var label = "" @State private var showPresets = false @State private var currentDetent: PresentationDetent = .fraction(0.05) @StateObject var viewModel = MessageModel() var body: some View { VStack { Text(label) .animation(.easeInOut, value: label) // 仅Text动画 Button { showPresets = true } label: { Image(systemName: "circle.fill") } Spacer() } .padding() .onReceive(viewModel.$text, perform: { text in label = text // 移除全局withAnimation }) .sheet(isPresented: $showPresets) { PresetsView(onDismiss: presetsViewDidDismiss, buttonAction: saveMessage) .equatable() // 启用Equatable优化 .presentationDetents([.fraction(0.05), .fraction(0.6), .large], selection: $currentDetent) .presentationDragIndicator(.hidden) .transaction { $0.disablesAnimations = true } // 阻断动画传递 } } private func presetsViewDidDismiss() { print("Presets view was dismissed") } private func saveMessage(message: String) { viewModel.setText(message: message) } } struct PresetsView: View, Equatable { static func == (lhs: PresetsView, rhs: PresetsView) -> Bool { lhs.selectedPresetCatagoriesArray == rhs.selectedPresetCatagoriesArray } @State private var selectedPresetCatagoriesArray = ["Topic1", "Topic2", "Topic3"] @State private var setMessage = "" var onDismiss: () -> Void let buttonAction: (String) -> Void let presets = Presets() var body: some View { let columns = [ GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()) ] ScrollView { ForEach(selectedPresetCatagoriesArray, id: \.self) { category in Text(category.uppercased()) LazyVGrid(columns: columns, spacing: 0) { ForEach(presets.getPresets(for: category.lowercased()), id: \.self) { preset in Button(action: { setMessage = preset.message Task { buttonAction(setMessage) } }) { VStack() { Circle() .frame(width: 55, height: 55) .overlay { Image(systemName: preset.image) .font(.system(size: 25)) .foregroundColor(.white) } Text(preset.label) .font(.footnote) Spacer() } } } } Divider() .background(Color.gray) } .padding(.horizontal, 10) } .defaultScrollAnchor(.top) .onDisappear() { onDismiss() } } } struct Preset: Codable, Hashable, Identifiable { var id = UUID() let label: String var image: String let message: String } struct Presets { let presetsTopic1 = [ Preset(label: "1", image: "1.circle.fill", message: "One"), Preset(label: "2", image: "2.circle.fill", message: "Two"), Preset(label: "3", image: "3.circle.fill", message: "Three") ] let presetsTopic2 = [ Preset(label: "1", image: "1.circle.fill", message: "One"), Preset(label: "2", image: "2.circle.fill", message: "Two"), Preset(label: "3", image: "3.circle.fill", message: "Three") ] let presetsTopic3 = [ Preset(label: "1", image: "1.circle.fill", message: "One"), Preset(label: "2", image: "2.circle.fill", message: "Two"), Preset(label: "3", image: "3.circle.fill", message: "Three") ] func getPresets(for type: String) -> [Preset] { switch type { case "topic1": return presetsTopic1 case "topic2": return presetsTopic2 case "topic3": return presetsTopic3 default: return [] } } }
内容的提问来源于stack exchange,提问作者Rhythmus
相关产品推荐
相关产品推荐

