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

SwiftUI动画故障无法消除,求解决方案

SwiftUI 动画异常故障修复方案

问题现象

当DialogView首次加载、修改代码后预览,或点击Test按钮切换气泡列表(空→填充/填充→空)时,会出现不符合预期的动画故障。


原问题代码

Bubble.swift
// Bubble.swift
import SwiftUI

struct Bubble: Identifiable {
    var id = UUID()
    var sender: Sender
    var text: String
    var value: String?
    var inputType: String?
}

enum Sender {
    case user
    case bot
    case system
}


struct BubbleView: View {
    @State var bubble: Bubble
    @State var showBubble = false
    
    var body: some View {
        Section {
            if self.showBubble {
                switch bubble.sender {
                    case .user, .bot:
                        Text(bubble.text)
                            .padding(10)
                            .foregroundColor(bubble.sender == Sender.user ? .white : .black)
                            .background(bubble.sender == Sender.user ? .accentColor : Color(UIColor.systemGray5))
                            .clipShape(RoundedRectangle(cornerRadius: 20))
                            .frame(maxWidth: .infinity, alignment: bubble.sender == Sender.user ? .trailing : .leading)
                            .padding(.vertical, bubble.sender == Sender.user ? 10 : 0)
                            .transition(.move(edge: .bottom))
                    case .system:
                        Text(bubble.text.uppercased())
                            .padding(10)
                            .foregroundColor(.secondary)
                            .font(.caption)
                            .fontWeight(.semibold)
                            .frame(maxWidth: .infinity)
                            .transition(.move(edge: .bottom))
                }
            } else {
                Text("")
            }
        }.onAppear {
            withAnimation {
                self.showBubble.toggle()
            }
        }
    }
}

#Preview {
    VStack(alignment: .leading) {
        BubbleView(bubble: Bubble(sender: .bot, text: "It's-a me, Mario!"))
        BubbleView(bubble: Bubble(sender: .user, text: "And it's-a me, Luigi!"))
        BubbleView(bubble: Bubble(sender: .system, text: "10:30"))
    }
}
Dialog.swift
// Dialog.swift
import SwiftUI

struct DialogView: View {
    @Binding var dialog: [Bubble]
    
    func scrollDown(proxy: ScrollViewProxy) {
        if let lastID = dialog.last?.id {
            withAnimation {
                proxy.scrollTo(lastID)
            }
        }
    }
    
    var body: some View {
        ScrollViewReader { proxy in
            ScrollView {
                VStack(alignment: .leading) {
                    ForEach(dialog) { bubble in
                        BubbleView(bubble: bubble)
                    }
                    .onChange(of: dialog.count, initial: false) { _,_  in
                        scrollDown(proxy: proxy)
                    }
                }
                .frame(maxHeight: .infinity)
                .padding()
                Spacer()
            }
        }
    }
}


#Preview("Dialog view") {
    DialogView(dialog: .constant([
        Bubble(sender: .bot, text: "Bubble 1"),
        Bubble(sender: .bot, text: "Bubble 2"),
        Bubble(sender: .bot, text: "Bubble 3")
    ]))
}
ContentView.swift
// ContentView.swift
import SwiftUI

struct ContentView: View {
    @State var bubbles: [Bubble] = []
    
    var body: some View {
        DialogView(dialog: $bubbles)
        Button {
            if self.bubbles.count == 0 {
                self.bubbles = [
                    Bubble(sender: .bot, text: "Bubble 1"),
                    Bubble(sender: .bot, text: "Bubble 2"),
                    Bubble(sender: .bot, text: "Bubble 3")
                ]
            } else {
                self.bubbles = []
            }
        } label: {
            Text("Test")
        }
    }
}

#Preview {
    ContentView()
}

问题原因分析

  1. 空Text占位干扰布局:showBubble为false时的空Text("")会占据布局空间,导致气泡出现/消失时布局计算异常。
  2. 不必要的Section容器:Section用于Form/List分组,普通VStack中使用会引入额外布局层级,干扰动画执行。
  3. 动画与滚动触发冲突:批量添加气泡时,BubbleView的onAppear动画与ScrollView滚动动画叠加,引发异常。

修复方案

1. 重构BubbleView

移除Section和空Text占位,调整参数类型与动画逻辑:

// Bubble.swift 修改后
import SwiftUI

struct Bubble: Identifiable {
    var id = UUID()
    var sender: Sender
    var text: String
    var value: String?
    var inputType: String?
}

enum Sender {
    case user
    case bot
    case system
}


struct BubbleView: View {
    let bubble: Bubble // 改为let,无需内部状态管理
    @State private var showBubble = false
    
    var body: some View {
        Group {
            switch bubble.sender {
            case .user, .bot:
                Text(bubble.text)
                    .padding(10)
                    .foregroundColor(bubble.sender == .user ? .white : .black)
                    .background(bubble.sender == .user ? .accentColor : Color(UIColor.systemGray5))
                    .clipShape(RoundedRectangle(cornerRadius: 20))
                    .frame(maxWidth: .infinity, alignment: bubble.sender == .user ? .trailing : .leading)
                    .padding(.vertical, bubble.sender == .user ? 10 : 0)
            case .system:
                Text(bubble.text.uppercased())
                    .padding(10)
                    .foregroundColor(.secondary)
                    .font(.caption)
                    .fontWeight(.semibold)
                    .frame(maxWidth: .infinity)
            }
        }
        .opacity(showBubble ? 1 : 0)
        .offset(y: showBubble ? 0 : 20)
        .onAppear {
            withAnimation(.easeOut(duration: 0.3)) {
                showBubble = true
            }
        }
        .onDisappear {
            showBubble = false
        }
    }
}

#Preview {
    VStack(alignment: .leading) {
        BubbleView(bubble: Bubble(sender: .bot, text: "It's-a me, Mario!"))
        BubbleView(bubble: Bubble(sender: .user, text: "And it's-a me, Luigi!"))
        BubbleView(bubble: Bubble(sender: .system, text: "10:30"))
    }
}

2. 优化DialogView滚动逻辑

调整监听对象与滚动时机,避免动画冲突:

// Dialog.swift 修改后
import SwiftUI

struct DialogView: View {
    @Binding var dialog: [Bubble]
    
    func scrollDown(proxy: ScrollViewProxy) {
        guard let lastID = dialog.last?.id else { return }
        // 延迟滚动,确保气泡动画完成后执行
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            withAnimation(.easeOut) {
                proxy.scrollTo(lastID, anchor: .bottom)
            }
        }
    }
    
    var body: some View {
        ScrollViewReader { proxy in
            ScrollView {
                VStack(alignment: .leading, spacing: 8) { // 控制气泡间距
                    ForEach(dialog) { bubble in
                        BubbleView(bubble: bubble)
                    }
                }
                .padding()
            }
            .onChange(of: dialog) { _, _ in
                scrollDown(proxy: proxy)
            }
        }
    }
}


#Preview("Dialog view") {
    DialogView(dialog: .constant([
        Bubble(sender: .bot, text: "Bubble 1"),
        Bubble(sender: .bot, text: "Bubble 2"),
        Bubble(sender: .bot, text: "Bubble 3")
    ]))
}

3. 可选优化ContentView布局

让按钮固定在底部:

// ContentView.swift 可选优化
import SwiftUI

struct ContentView: View {
    @State var bubbles: [Bubble] = []
    
    var body: some View {
        VStack {
            DialogView(dialog: $bubbles)
            Spacer()
            Button {
                if bubbles.isEmpty {
                    bubbles = [
                        Bubble(sender: .bot, text: "Bubble 1"),
                        Bubble(sender: .bot, text: "Bubble 2"),
                        Bubble(sender: .bot, text: "Bubble 3")
                    ]
                } else {
                    bubbles = []
                }
            } label: {
                Text("Test")
                    .padding()
                    .background(.blue)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }
            .padding()
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

#Preview {
    ContentView()
}

修复效果

  • 气泡加载时从下方平滑淡入上移,无异常跳动
  • 清空气泡时平滑消失
  • ScrollView在气泡动画完成后自动滚动到底部,无冲突动画

内容的提问来源于stack exchange,提问作者TheJof

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 21:35:54