impo
impo

Reputation: 789

UITextView typewriter effect text update with delay lags with too many characters

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.")

The typewriter effect showing the lagging

Upvotes: 1

Views: 46

Answers (1)

DonMag
DonMag

Reputation: 77672

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")
                })
            }
            
        }
        
    }
    
}

Upvotes: 1

Related Questions