Skip to content

Text Animation

Song edited this page Aug 4, 2021 · 6 revisions

2021-08-03

by Song

  • 다른 View 변경으로 인해 멈추지 않는 애니메이션 구현
  • Main Thread 외의 Thread 활용

Step 1 - 한글자씩 나타나는 애니메이션

기존 사용하던 GhostTypeWriter 라이브러리는 Main Thread에서 실행됨

  • 콜렉션 뷰 스크롤 시 멈추어 개선 필요
  • CATextLayer를 활용하여 Custom하게 제작
  • UIView를 subclassing한 TypeWriterView에 CATextLayer를 활용한 애니메이션 메소드 구현
  • 각 글자마다 타이머 딜레이를 다르게 주어 1글자씩 나타나게 함
func startTyping(text fullText: String, duration: Double) {
    let totalCount = fullText.count
    let delayPerLetter = duration / Double(totalCount)
    let letters = fullText.map { String($0) }
    
    for count in 0..<totalCount {
        let currentDelay = delayPerLetter * Double(count)
        Timer.scheduledTimer(withTimeInterval: currentDelay, repeats: false) { [weak self] _ in
            self?.changeText(for: count, with: letters)
        }
    }
}

private func changeText(for count: Int, with letters: [String]) {
    let currentText = text(for: count, with: letters)
    textLayer.string = currentText
}

private func text(for currentCount: Int, with letters: [String]) -> String {
    return (0...currentCount).map { letters[$0] }.joined()
}
  • CATextLayer를 View의 중앙에 위치시키는 메소드 구현
func startTyping(text fullText: String, duration: Double) {
    adjustTextLayerFrameToCenter(for: fullText)
    // 애니메이션 코드 생략
}

private func adjustTextLayerFrameToCenter(for fullText: String) {
    let lineCount = fullText.components(separatedBy: "\n").count
    let fontHeight = font.capHeight
    let totalTextHeight = fontHeight * CGFloat(lineCount)
    let yPosition = (layer.bounds.size.height-totalTextHeight*1.5)/2 // Position이 아닌 Origin이기 때문에 전체 텍스트 높이의 반만큼 더 위로 올려 줌
    let newOrigin = CGPoint(x: 0, y: yPosition)
    textLayer.frame = CGRect(origin: newOrigin, size: textLayer.frame.size)
}

결과

-> 그러나 Thread 문제는 해결하지 못함

  • CATextLayer의 String Property는 Animatable하지 않다
  • Layer의 텍스트 변경을 위해서는 Main Thread를 거쳐갈 수밖에 없음 -> String 프로퍼티 변경해서..? 명확한 원인 파악 필요 <<<
  • Timer를 background에서 돌리더라도 String 변경 시 Main thread를 거쳐야 함

Step 2 - Fade In되어 나타나는 텍스트 애니메이션

애니메이션과 동시에 유저 인터랙션이 발생할 가능성이 있는 ItemViewController에 한해 Animatable한 속성을 활용한 것으로 변경

  • Opacity 변경 애니메이션 구현
func show(text fullText: String, duration: Double=0.8) {
    textLayer.add(newFadeInAnimation(for: duration), forKey: opacityKey)
}

private func newFadeInAnimation(for duration: Double) -> CAKeyframeAnimation {
    let animation = CAKeyframeAnimation(keyPath: opacityKey)
    animation.duration = duration
    animation.values = [0.0, 1.0]
    animation.timingFunction = CAMediaTimingFunction.init(name: .easeOut)
    return animation
}
  • 텍스트 길이에 따라 font size 조정
func show(text fullText: String, duration: Double=0.8) {
    setTextLayer(with: fullText)
    // 애니메이션 코드 생략
}

private func setTextLayer(with text: String) {
    textLayer.string = text
    textLayer.fontSize = newFontSize(for: text)
}

private func newFontSize(for text: String) -> CGFloat {
    let letterCountsPerLine = text.components(separatedBy: "\n").map { $0.count }
    let maxLetterCount = letterCountsPerLine.max() ?? 15
    let newFontSize = textLayer.bounds.width / CGFloat(maxLetterCount)
    return newFontSize
}

결과

Step 3 - Refactoring

Text Layer Setting을 담당하는 TextPresentView에서 각 class 분화

  • 하나의 CATextLayer를 가지고 있는 TextPresentView
  • 파라미터로 받은 텍스트를 Layer를 통해 보여주고, 글자 크기 및 레이어 위치를 조정함
class TextPresentView: UIView {
    
    private(set) lazy var textLayer: CATextLayer = {
        let textLayer = CATextLayer()
        textLayer.font = UIFont(name: Font.joystix, size: 16)
        textLayer.alignmentMode = .center
        textLayer.frame = CGRect(origin: .zero, size: layer.bounds.size)
        textLayer.foregroundColor = defaultTextColor.cgColor
        layer.addSublayer(textLayer)
        return textLayer
    }()
    
    private let defaultTextColor = UIColor(named: "digitalgreen") ?? UIColor.green
    private let defaultLetterCount = 15
    private let lineSeparator = "\n"
    
    func show(text fullText: String) {
        setTextLayer(with: fullText)
    }
    
    private func setTextLayer(with text: String) {
        textLayer.string = text
        textLayer.fontSize = newFontSize(for: text)
        adjustTextLayerFrameToCenter(for: text)
    }
    
    private func newFontSize(for text: String) -> CGFloat {
        // 상세 코드 생략
    }
    
    private func adjustTextLayerFrameToCenter(for fullText: String) {
        // 상세 코드 생략
    }
}
  • 위의 class를 subclassing하여 각 애니메이션 뷰 생성
final class TypeWriterView: TextPresentView {

    private let delayPerLetter: Double = 0.08
    
    override func show(text fullText: String) {
        super.show(text: fullText)
        startTyping(text: fullText)
    }
    
    private func startTyping(text fullText: String) {
        // 상세 코드 생략
    }
}
final class FadeInTextView: TextPresentView {

    private let duration: Double = 0.8
    private let opacityKey = #keyPath(CALayer.opacity)

    override func show(text fullText: String) {
        super.show(text: fullText)
        textLayer.add(newFadeInAnimation(for: duration), forKey: opacityKey)
    }

    private func newFadeInAnimation(for duration: Double) -> CAKeyframeAnimation {
        // 상세 코드 생략
    }
}
Clone this wiki locally