Search code examples
swiftuikituitextview

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


Solution

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