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

SwiftUI中如何正确定位AI聊天应用的气泡动画?

AI聊天应用气泡动画定位问题解决办法

问题描述

我在开发AI聊天应用时,遇到了气泡动画的定位问题——AI回复生成时的加载动画(类似iMessage的圆点跳动效果)功能正常,但始终显示在聊天窗口中央,而不是预期的AI回复左侧区域。我怀疑问题出在GeometryReader的使用上。

当前效果:
当前效果

相关代码:

import SwiftUI

struct ContentView: View {
    @State private var isTyping = false // to show animation
    
    var body: some View {
        VStack {
            // Chat messages
            Text("Chat messages")
            
            // Bubble animation
            GeometryReader { geometry in
                if isTyping {
                    AnimatedDotsView()
                        .position(x: geometry.size.width / 2, y: geometry.size.height / 2)
                }
            }
            
            // User input
            TextField("Send a message", text: .constant(""))
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
            
            // Button
            Button(action: {
                isTyping.toggle()
            }) {
                Text("Toggle Animation")
                    .foregroundColor(.blue)
            }
            .padding()
        }
        .padding()
    }
}

struct AnimatedDotsView: View {
    @State private var dot1Animating = false
    @State private var dot2Animating = false
    @State private var dot3Animating = false

    var body: some View {
        HStack(spacing: 3) {
            Circle()
                .foregroundColor(dot1Animating ? .gray : .green)
                .frame(width: 5, height: 5)
                .scaleEffect(dot1Animating ? 1 : 0.2)
                .onAppear {
                    withAnimation(Animation.easeInOut(duration: 0.8).repeatForever(autoreverses: true).delay(0.1)) {
                        dot1Animating = true
                    }
                }
            
            Circle()
                .foregroundColor(dot2Animating ? .gray : .green)
                .frame(width: 5, height: 5)
                .scaleEffect(dot2Animating ? 1 : 0.2)
                .onAppear {
                    withAnimation(Animation.easeInOut(duration: 0.8).repeatForever(autoreverses: true).delay(0.2)) {
                        dot2Animating = true
                    }
                }
            
            Circle()
                .foregroundColor(dot3Animating ? .gray : .green)
                .frame(width: 5, height: 5)
                .scaleEffect(dot3Animating ? 1 : 0.2)
                .onAppear {
                    withAnimation(Animation.easeInOut(duration: 0.8).repeatForever(autoreverses: true).delay(0.3)) {
                        dot3Animating = true
                    }
                }
        }
        .padding(.horizontal, 6)
        .padding(.vertical, 8)
        .background(
            RoundedRectangle(cornerRadius: 8)
                .foregroundColor(Color.gray.opacity(0.2))
        )
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

我尝试过以下调整,但没有解决问题:

GeometryReader { geometry in
    if isTyping {
        AnimatedDotsView()
            .position(x: geometry.size.width / 2, y: geometry.size.height / 2)
    }
}

问题原因

你用的GeometryReader会占据父视图的全部可用空间,再加上.position(x: geometry.size.width / 2, y: geometry.size.height / 2)直接把动画定位到了这个容器的正中央,所以才会出现当前的效果。要实现左侧对齐的AI回复样式,不需要用GeometryReader来定位,而是利用SwiftUI的布局系统来实现。

修改方案

调整后的完整代码

import SwiftUI

struct ContentView: View {
    @State private var isTyping = false // 控制动画显示
    
    var body: some View {
        VStack {
            // 聊天消息滚动区域
            ScrollView {
                // 示例AI消息
                HStack(alignment: .bottom) {
                    Text("AI回复示例")
                        .padding(.horizontal, 12)
                        .padding(.vertical, 8)
                        .background(Color.gray.opacity(0.2))
                        .cornerRadius(16)
                    Spacer()
                }
                .padding(.horizontal)
                
                // 加载动画:AI回复左侧区域
                if isTyping {
                    HStack(alignment: .bottom) {
                        AnimatedDotsView()
                        Spacer()
                    }
                    .padding(.horizontal)
                }
            }
            .frame(maxHeight: .infinity)
            
            // 用户输入区域
            TextField("Send a message", text: .constant(""))
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
            
            Button(action: {
                isTyping.toggle()
            }) {
                Text("Toggle Animation")
                    .foregroundColor(.blue)
            }
            .padding()
        }
        .padding()
    }
}

// AnimatedDotsView代码保持不变
struct AnimatedDotsView: View {
    @State private var dot1Animating = false
    @State private var dot2Animating = false
    @State private var dot3Animating = false

    var body: some View {
        HStack(spacing: 3) {
            Circle()
                .foregroundColor(dot1Animating ? .gray : .green)
                .frame(width: 5, height: 5)
                .scaleEffect(dot1Animating ? 1 : 0.2)
                .onAppear {
                    withAnimation(Animation.easeInOut(duration: 0.8).repeatForever(autoreverses: true).delay(0.1)) {
                        dot1Animating = true
                    }
                }
            
            Circle()
                .foregroundColor(dot2Animating ? .gray : .green)
                .frame(width: 5, height: 5)
                .scaleEffect(dot2Animating ? 1 : 0.2)
                .onAppear {
                    withAnimation(Animation.easeInOut(duration: 0.8).repeatForever(autoreverses: true).delay(0.2)) {
                        dot2Animating = true
                    }
                }
            
            Circle()
                .foregroundColor(dot3Animating ? .gray : .green)
                .frame(width: 5, height: 5)
                .scaleEffect(dot3Animating ? 1 : 0.2)
                .onAppear {
                    withAnimation(Animation.easeInOut(duration: 0.8).repeatForever(autoreverses: true).delay(0.3)) {
                        dot3Animating = true
                    }
                }
        }
        .padding(.horizontal, 6)
        .padding(.vertical, 8)
        .background(
            RoundedRectangle(cornerRadius: 8)
                .foregroundColor(Color.gray.opacity(0.2))
        )
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

关键修改点

  • 移除了导致居中的GeometryReader和position设置,改用ScrollView承载聊天消息和动画,模拟真实聊天窗口的滚动效果
  • 用HStack + Spacer的组合实现AI消息的左侧对齐:动画视图放在HStack开头,后面加Spacer填满剩余空间,自然实现靠左布局
  • 让聊天区域占满垂直方向的剩余空间,保证输入框固定在底部

这样修改后,动画就会显示在AI回复所在的左侧区域,和聊天消息的布局保持一致。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 20:24:54