Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

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

Morty Proxy This is a proxified and sanitized view of the page, visit original site.