Skip to content

Commit

Permalink
Use CADisplayLink
Browse files Browse the repository at this point in the history
  • Loading branch information
juyan committed Dec 26, 2024
1 parent 02f16c7 commit e680e7c
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 53 deletions.
6 changes: 5 additions & 1 deletion Sources/FadeInText/FadeInText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ public struct FadeInText: View {

@StateObject var controller: FadeInTextController

public init(text: String, color: Color) {
self.init(text: text, color: color, tokenizer: DefaultTokenizer(), interpolator: LinearInterpolator(config: .defaultValue))
}

public init(text: String, color: Color, tokenizer: Tokenizer, interpolator: Interpolator) {
self._controller = StateObject(
wrappedValue: FadeInTextController(
Expand Down Expand Up @@ -43,7 +47,7 @@ struct ControlledView: View {
.padding(16)
.onAppear(perform: {
Task {
try await Task.sleep(nanoseconds: 5000000000)
try await Task.sleep(nanoseconds: 2000000000)
self.show = true
}
})
Expand Down
62 changes: 29 additions & 33 deletions Sources/FadeInText/FadeInTextController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ public class FadeInTextController: ObservableObject {
private let rawText: String
private let interpolator: Interpolator

public static let refreshDuration = 0.01667

private var displayLink: CADisplayLink?
private var startTime: Double?
private var chunks: [String] = []

init(rawText: String, color: Color, tokenizer: Tokenizer, interpolator: Interpolator) {
self.color = UIColor(color)
let str = AttributedString(stringLiteral: rawText)
Expand All @@ -26,38 +28,32 @@ public class FadeInTextController: ObservableObject {
}

func startAnimation() {
let chunks = tokenizer.chunks(text: rawText)

Task {
var currentTime = 0.0
var interpolationResult = InterpolationResult(opacities: Array(repeating: 0, count: chunks.count), shouldAnimationFinish: false)

while true {
do {
try await Task.sleep(nanoseconds: UInt64(Self.refreshDuration * 1000 * 1000 * 1000))
} catch {}
currentTime += Self.refreshDuration
let newResult = interpolator.interpolate(time: currentTime, previousResult: interpolationResult)
if newResult.shouldAnimationFinish {
break
} else {
interpolationResult = newResult
}
var updatedString = AttributedString()
for (i, chunk) in chunks.enumerated() {
let container = AttributeContainer([
NSAttributedString.Key.foregroundColor: self.color.withAlphaComponent(newResult.opacities[i])
])
let str = AttributedString(chunk, attributes: container)
updatedString.append(str)
}
await updateText(newText: updatedString)
}
}
self.chunks = tokenizer.chunks(text: rawText)
self.displayLink = CADisplayLink(target: self, selector: #selector(onFrameUpdate))
self.displayLink?.preferredFrameRateRange = CAFrameRateRange(minimum: 30, maximum: 60, preferred: 60)
self.displayLink?.add(to: RunLoop.main, forMode: .common)
startTime = CACurrentMediaTime()
}

@MainActor
private func updateText(newText: AttributedString) {
self.text = newText
@objc
private func onFrameUpdate(_ displayLink: CADisplayLink) {
guard let startTime, !self.chunks.isEmpty else {
return
}
let time = CACurrentMediaTime() - startTime
let newResult = interpolator.interpolate(currentTime: time, numberOfChunks: self.chunks.count)
var updatedString = AttributedString()
for (i, chunk) in chunks.enumerated() {
let container = AttributeContainer([
NSAttributedString.Key.foregroundColor: self.color.withAlphaComponent(newResult.opacities[i])
])
let str = AttributedString(chunk, attributes: container)
updatedString.append(str)
}
self.text = updatedString
if newResult.shouldAnimationFinish {
self.displayLink?.invalidate()
self.displayLink = nil
}
}
}
8 changes: 7 additions & 1 deletion Sources/FadeInText/Interpolator/Interpolator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import Foundation

/// Interpolator that specify the animation specs with regarding to how fast each chunk of text should fade in, and how fast each chunk of text should appear.
public protocol Interpolator {
func interpolate(time: Double, previousResult: InterpolationResult) -> InterpolationResult

/// Calculate the interpolation result.
/// - Parameters:
/// - currentTime: The current time, relative to the animation start time.
/// - numberOfChunks: The total number of text chunks.
/// - Returns: The interpolation result
func interpolate(currentTime: Double, numberOfChunks: Int) -> InterpolationResult
}

public struct InterpolationResult {
Expand Down
28 changes: 10 additions & 18 deletions Sources/FadeInText/Interpolator/LinearInterpolator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,19 @@ public final class LinearInterpolator: Interpolator {
self.config = config
}

public func interpolate(time: Double, previousResult: InterpolationResult) -> InterpolationResult {
guard previousResult.opacities.contains(where: { $0 < 1.0 }) else {
return InterpolationResult(opacities: [], shouldAnimationFinish: true)
public func interpolate(currentTime: Double, numberOfChunks: Int) -> InterpolationResult {
if currentTime > config.appearanceDuration + config.fadeInDuration {
return InterpolationResult(
opacities: Array(repeating: 1.0, count: numberOfChunks),
shouldAnimationFinish: true
)
}

let opacityStep = min(1, FadeInTextController.refreshDuration / config.fadeInDuration)

var newOpacities = [Double]()
for i in 0..<previousResult.opacities.count {
let prevOpacity = previousResult.opacities[i]
if prevOpacity == 0 {
let startTime = Double(i) * (config.appearanceDuration / Double(previousResult.opacities.count))
if time > startTime {
let newOpacity = min(1.0, (time - startTime) / config.fadeInDuration)
newOpacities.append(newOpacity)
} else {
newOpacities.append(0)
}
} else {
newOpacities.append(min(1.0, prevOpacity + opacityStep))
}
for i in 0..<numberOfChunks {
let startTime = Double(i) * (config.appearanceDuration / Double(numberOfChunks))
let newOpacity = max(min(1.0, (currentTime - startTime) / config.fadeInDuration), 0.0)
newOpacities.append(newOpacity)
}
return InterpolationResult(opacities: newOpacities, shouldAnimationFinish: false)
}
Expand Down

0 comments on commit e680e7c

Please sign in to comment.