TypingText 만들기 (접근성 대응 포함)TL;DR
Task와Task.sleep으로 한 글자씩 출력하는 타이핑 애니메이션 컴포넌트.
시스템의 동작 줄이기(Reduce Motion) 가 켜지면 애니메이션을 건너뛰고 전체 텍스트를 즉시 표시합니다.
끝의 커서(|) 깜빡임도 포함됩니다.
|) 깜빡임 text 값이 바뀌면 자동으로 다시 타이핑 startDelay), 글자당 지연(perChar), 폰트(font), 커서 표시(showsCursor) 커스터마이즈import SwiftUI
struct TypingText: View {
let text: String
var font: Font = .pretendardFontFamily(family: .semiBold, size: 20)
var perChar: Double = 0.05 // 글자당 지연(초)
var startDelay: Double = 0.0 // 시작 지연(초)
var showsCursor: Bool = true // 커서 표시 여부
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var displayed: String = ""
@State private var blink = false
@State private var task: Task<Void, Never>?
var body: some View {
HStack(spacing: 0) {
Text(displayed).font(font)
if showsCursor {
Text("|")
.font(font)
.opacity(blink ? 0 : 1)
.animation(.easeInOut(duration: 0.6).repeatForever(), value: blink)
.accessibilityHidden(true)
}
}
.onAppear { startTyping() }
// iOS 17+: old/new 제공
.onChange(of: text) { _, _ in startTyping() }
.onDisappear { task?.cancel() }
}
private func startTyping() {
task?.cancel()
guard !reduceMotion else {
displayed = text
blink.toggle()
return
}
displayed = ""
blink = false
task = Task {
if startDelay > 0 {
try? await Task.sleep(nanoseconds: UInt64(startDelay * 1_000_000_000))
}
for ch in text {
displayed.append(ch)
try? await Task.sleep(nanoseconds: UInt64(perChar * 1_000_000_000))
}
await MainActor.run { blink = true }
}
}
}
iOS 16 이하 호환
아래 한 줄로 교체하세요.
.onChange(of: text) { _ in startTyping() }
struct DemoView: View {
@State private var title = "안녕하세요 1조입니다! 👋"
var body: some View {
VStack(spacing: 16) {
// 기본
TypingText(text: title)
// 커스터마이즈
TypingText(
text: "우리 팀의 궁극적인 목표",
font: .system(.title3, weight: .semibold),
perChar: 0.06,
startDelay: 0.15,
showsCursor: false
)
Button("텍스트 바꾸기") {
title = ["환영합니다 🙌", "함께 만들어가요 🚀", "집중! ✨"].randomElement()!
}
}
.padding()
}
}
여러 줄을 순차적으로 등장시키고 싶다면 startDelay를 다르게 주면 됩니다.
struct IntroList: View {
let lines: [String] = [
"다양성 존중",
"창의적 사고",
"따뜻한 소통",
"목표지향"
]
var body: some View {
VStack(alignment: .leading, spacing: 12) {
ForEach(Array(lines.enumerated()), id: \.offset) { index, line in
TypingText(
text: "• \(line)",
font: .system(.body),
perChar: 0.05,
startDelay: 0.12 + 0.08 * Double(index),
showsCursor: false
)
}
}
}
}
perChar는 0.03~0.08s 구간이 자연스럽습니다. 너무 길면 지루하고, 너무 짧으면 효과가 흐려집니다. text가 바뀌어야 onChange가 호출됩니다. 같은 값을 다시 재생하려면 별도의 trigger 상태를 두고 onChange(of: trigger)에서 startTyping()을 호출하는 패턴을 고려하세요. task?.cancel()로 백그라운드 작업을 종료합니다.