You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI问题:父视图withAnimation引发子视图意外动画

问题分析与解决方案

问题根源

  1. 父视图ContentView中通过withAnimation更新label时,会触发整个视图树重绘,包括弹窗里的PresetsView——尽管PresetsView自身状态没有变化。
  2. SwiftUI的动画会通过环境传递到子视图,导致LazyVGrid在无配置动画的情况下出现错位动画。
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 01:13:09