I'm trying to give a UITextView a typewriter effect by having its text contents be written out one character at a time.
I've found a solution that works, but I've noticed the larger the text it needs to write, the more stuttered and "out of sync" it becomes.
See the below video for an example. You can see the first line comes out as expected, but the remaining go a bit wonky.
At first I thought it was because I was using the delay as the current time, so as the loop would continue the latter characters would be using an initial time that is later than the start, but changing that did not fix it.
The probably is definitely in my understanding and logic of the delays I'm using (I'm new to Swift). Would anyone know how to solve this problem?
Here is my Typewriter class:
class TypewriterTextView : UITextView {
let delay: CGFloat = 0.1
let delayForComplete: CGFloat = 0.5
var typingComplete: Bool = false
required init?(coder: NSCoder) {
super.init(coder: coder)
}
func Write(typewriterText: String) {
text = ""
typingComplete = false
var count = 1.0
var delayInSeconds: CGFloat = 0.0
let timeAtStartOfTypewriting = DispatchTime.now()
for i in typewriterText {
delayInSeconds = count * delay
DispatchQueue.main.asyncAfter(deadline: timeAtStartOfTypewriting + delayInSeconds) {
// Put your code which should be executed with a delay here
self.text! += "\(i)"
print(self.text!)
}
count += 1
}
delayInSeconds = delayInSeconds + delayForComplete
DispatchQueue.main.asyncAfter(deadline: timeAtStartOfTypewriting + delayInSeconds) {
// Put your code which should be executed with a delay here
self.typingComplete = true
print("done")
}
}
}
And for testing purposes I am simply calling the Write function on click of the view:
IntroTextView.Write(typewriterText: "Welcome!\nI am testing a lot of words\nFor some reason the longer this text is it starts to stutter and be weird.\nI have no idea why.")
You are building up a lot of events on the main dispatch queue -- which is not how you want to do this.
Instead, you should implement a repeating Timer and update the text on each repeat.
Give this a try:
func Write(typewriterText: String) {
typingComplete = false
var count = 0
let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { t in
if count <= typewriterText.count {
self.text = String(typewriterText.prefix(count))
count += 1
} else {
t.invalidate()
self.typingComplete = true
print("Done")
}
}
}
If you end up wanting to "interrupt" the typing, setup timer
as a class property / var so you can .invalidate()
it outside of this func.
Edit - here is a slightly modified version. It implements your original delayForComplete
, and it prevents starting a new "type this string" while it is currently "typing a string":
class TypewriterTextView : UITextView {
let delay: TimeInterval = 0.1
let delayForComplete: TimeInterval = 0.5
var typingComplete: Bool = true
func write(typewriterText: String) {
// if we're already "typing" a string, don't start another one
if !typingComplete { return }
typingComplete = false
var count = 0
// we don't need to save the timer instance, so we can use underscore-equals to ignore it
_ = Timer.scheduledTimer(withTimeInterval: delay, repeats: true) { t in
if count <= typewriterText.count {
self.text = String(typewriterText.prefix(count))
count += 1
} else {
// stop the timer
t.invalidate()
print("Typing Done")
// a little delay before calling it fully "complete"
DispatchQueue.main.asyncAfter(deadline: .now() + self.delayForComplete, execute: {
self.typingComplete = true
print("Complete Delay Done")
})
}
}
}
}