feat(chat-room) 타이핑 중인 것을 알려주는 애니메이션 아이템 추가
This commit is contained in:
		@@ -6,13 +6,79 @@
 | 
				
			|||||||
//
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import SwiftUI
 | 
					import SwiftUI
 | 
				
			||||||
 | 
					import Kingfisher
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct TypingIndicatorItemView: View {
 | 
					struct TypingIndicatorItemView: View {
 | 
				
			||||||
 | 
					    var dotCount: Int = 3
 | 
				
			||||||
 | 
					    var size: CGFloat = 6
 | 
				
			||||||
 | 
					    var spacing: CGFloat = 6
 | 
				
			||||||
 | 
					    var color: Color = .secondary
 | 
				
			||||||
 | 
					    var period: Double = 1.2   // 초
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let characterName: String
 | 
				
			||||||
 | 
					    let characterProfileUrl: String
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    var body: some View {
 | 
					    var body: some View {
 | 
				
			||||||
        Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
 | 
					        HStack(alignment: .top, spacing: 9) {
 | 
				
			||||||
 | 
					            KFImage(URL(string: characterProfileUrl))
 | 
				
			||||||
 | 
					                .placeholder {
 | 
				
			||||||
 | 
					                    Image(systemName: "person.crop.circle")
 | 
				
			||||||
 | 
					                        .resizable()
 | 
				
			||||||
 | 
					                        .scaledToFit()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                .resizable()
 | 
				
			||||||
 | 
					                .frame(width: 30, height: 30)
 | 
				
			||||||
 | 
					                .clipShape(Circle())
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            VStack(alignment: .leading, spacing: 4) {
 | 
				
			||||||
 | 
					                HStack(spacing: 4) {
 | 
				
			||||||
 | 
					                    Text(characterName)
 | 
				
			||||||
 | 
					                        .font(.custom(Font.preRegular.rawValue, size: 12))
 | 
				
			||||||
 | 
					                        .foregroundColor(.white)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                HStack(spacing: 10) {
 | 
				
			||||||
 | 
					                    TimelineView(.animation) { context in
 | 
				
			||||||
 | 
					                        let t = context.date.timeIntervalSinceReferenceDate
 | 
				
			||||||
 | 
					                        HStack(spacing: spacing) {
 | 
				
			||||||
 | 
					                            ForEach(0..<dotCount, id: \.self) { i in
 | 
				
			||||||
 | 
					                                Circle()
 | 
				
			||||||
 | 
					                                    .fill(color)
 | 
				
			||||||
 | 
					                                    .frame(width: size, height: size)
 | 
				
			||||||
 | 
					                                    .opacity(opacity(for: i, time: t))
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        // 레이아웃이 미세하게 흔들리지 않도록 애니메이션은 투명도에만 적용
 | 
				
			||||||
 | 
					                        .animation(.easeInOut(duration: period / Double(dotCount)).repeatForever(autoreverses: true), value: context.date)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                .padding(.horizontal, 10)
 | 
				
			||||||
 | 
					                .padding(.vertical, 8)
 | 
				
			||||||
 | 
					                .background(
 | 
				
			||||||
 | 
					                    Color.black.opacity(0.1)
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                .clipShape(AiMessageBubbleShape())
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .frame(maxWidth: .infinity, alignment: .leading)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /// 각 점마다 위상(phase)을 어긋나게 해서 순차적으로 밝아지도록 함
 | 
				
			||||||
 | 
					    private func opacity(for index: Int, time t: TimeInterval) -> Double {
 | 
				
			||||||
 | 
					        // 0...1 구간에서 부드럽게 오르내리는 값(사인파 기반)
 | 
				
			||||||
 | 
					        let base = (t.truncatingRemainder(dividingBy: period)) / period
 | 
				
			||||||
 | 
					        let phase = base + Double(index) / Double(max(1, dotCount))
 | 
				
			||||||
 | 
					        let wave = 0.5 + 0.5 * sin(2 * .pi * phase)
 | 
				
			||||||
 | 
					        // 최소/최대 투명도 범위
 | 
				
			||||||
 | 
					        return 0.25 + 0.75 * wave
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#Preview {
 | 
					#Preview {
 | 
				
			||||||
    TypingIndicatorItemView()
 | 
					    TypingIndicatorItemView(
 | 
				
			||||||
 | 
					        characterName: "보라",
 | 
				
			||||||
 | 
					        characterProfileUrl: "https://picsum.photos/1000"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .padding()
 | 
				
			||||||
 | 
					    .background(Color.black)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user