Skip to content

Instantly share code, notes, and snippets.

@Koshimizu-Takehito
Last active June 21, 2024 08:31
Show Gist options
  • Save Koshimizu-Takehito/7486f95975a08d6720d49d6519170b8b to your computer and use it in GitHub Desktop.
Save Koshimizu-Takehito/7486f95975a08d6720d49d6519170b8b to your computer and use it in GitHub Desktop.
Hacker Text Effect - SwiftUI - iOS 16 & iOS 17
import SwiftUI
@main
struct App: SwiftUI.App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
import SwiftUI
struct ContentView: View {
@State var trigger = 0
@State var text = sampeles[0]
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HackerTextEffectView(
text: text,
trigger: trigger,
transition: .numericText(),
speed: 0.06
)
.font(.largeTitle.bold())
.lineLimit(nil)
Button(
action: {
trigger += 1
text = sampeles[trigger % sampeles.count]
},
label: {
Text("Trigger")
.fontWeight(.semibold)
.padding(.horizontal, 16)
.padding(.vertical, 2)
}
)
.buttonStyle(.borderedProminent)
.buttonBorderShape(.capsule)
.frame(maxWidth: .infinity)
.padding(.top, 30)
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
private let sampeles = [
"Hello, World!",
"Made With SwiftUI\n By Foo Bar Baz",
"This is Animated\nText View."
]
#Preview {
ContentView()
}
import SwiftUI
import Observation
@Observable
final class HackerTextEffect {
let actualText: String
private(set) var displayText = ""
private let duration: Double
private let speed: Double
init(text: String, duration: Double, speed: Double) {
self.actualText = text
self.duration = duration
self.speed = speed
}
func update(reset: Bool) {
if reset {
resetDisplayText()
}
updateDisplayText()
}
private func updateDisplayText() {
for index in actualText.indices {
let delay = Double.random(in: 0...duration)
var timerDuration = 0.0
let timer = Timer.scheduledTimer(withTimeInterval: speed, repeats: true) { [weak self] timer in
guard let self else {
timer.invalidate()
return
}
timerDuration += speed
if timerDuration >= delay {
displayText.replace(at: index, charcter: actualText[index])
timer.invalidate()
} else {
displayText.replace(at: index, charcter: .randomASCII)
}
}
timer.fire()
}
}
private func resetDisplayText() {
displayText = actualText
for index in displayText.indices {
displayText.replace(at: index, charcter: .randomASCII)
}
}
}
private extension String {
mutating func replace(at index: String.Index, charcter: Character) {
guard
indices.contains(index),
String(self[index]).trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
else {
return
}
replaceSubrange(index...index, with: String(charcter))
}
}
extension Character {
private static let ascii: [Character] = (33...126).map {
Character(UnicodeScalar($0))
}
static var randomASCII: Self {
ascii.randomElement()!
}
}
import SwiftUI
struct HackerTextEffectView<Trigger: Equatable>: View {
var trigger: Trigger
var transition = ContentTransition.interpolate
let model: HackerTextEffect
init(
text: String,
trigger: Trigger,
transition: ContentTransition = .interpolate,
duration: Double = 1.0,
speed: Double = 0.05
) {
self.trigger = trigger
self.transition = transition
self.model = HackerTextEffect(text: text, duration: duration, speed: speed)
}
var body: some View {
Text(model.displayText)
.fontDesign(.monospaced)
.truncationMode(.tail)
.contentTransition(transition)
.animation(.easeIn(duration: 0.1), value: model.displayText)
.onAppear {
model.update(reset: true)
}
.onChange(of: trigger) { _, _ in
model.update(reset: false)
}
.onChange(of: model.actualText) { _, _ in
model.update(reset: true)
}
}
}
#Preview {
ContentView()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment