SwiftUI 로 TypingText 애니메이션 만들기

Ios_Roy·2025년 8월 12일
0

TIL

목록 보기
19/25
post-thumbnail

SwiftUI로 타자 치듯 글자가 나타나는 TypingText 만들기 (접근성 대응 포함)

TL;DR
TaskTask.sleep으로 한 글자씩 출력하는 타이핑 애니메이션 컴포넌트.
시스템의 동작 줄이기(Reduce Motion) 가 켜지면 애니메이션을 건너뛰고 전체 텍스트를 즉시 표시합니다.
끝의 커서(|) 깜빡임도 포함됩니다.


기능 요약

  • 텍스트를 한 글자씩 출력하는 타이핑 효과
  • 커서(|) 깜빡임
  • text 값이 바뀌면 자동으로 다시 타이핑
  • 접근성: Reduce Motion 활성화 시 애니메이션 생략
  • 시작 지연(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()을 호출하는 패턴을 고려하세요.
  • 접근성 고려: Reduce Motion 사용자는 애니메이션 없이 전체 텍스트를 즉시 읽을 수 있어야 합니다. 본 컴포넌트는 이를 자동으로 지원합니다.
  • 리소스 관리: 화면 전환 시 task?.cancel()로 백그라운드 작업을 종료합니다.

profile
iOS 개발자 공부하는 Roy

0개의 댓글