LAST CHANCE: Save 50% on all my Swift books and bundles! >>

Animating a paragraph onto screen word-by-word

Forums > SwiftUI

I just finished the animation section of the 100 days SwiftUI course, and now i'm trying to animate a paragraph onto screen word-by-word (almost like the computer is typing it out and "thinking" live, similar to chat.openai.com)

What's the best approach to take here? So far all my approaches have failed...

Thanks! J

2      

You can try something like this:

import SwiftUI

class Paragrapher: ObservableObject {
    //what is the full text when we are done?
    //NOTE: this is stored as an array of Strings
    //      that we will join element-by-element
    //      when the time comes
    let fullText: [String]
    //how long between showing each element?
    let delay: Double

    //the text we display to the outside world
    @Published var partialText = ""

    //are we splitting the fullText by letters or words?
    enum ParagraphSplit {
        case letters
        case words
    }
    private let splitBy: ParagraphSplit

    //used when we rejoin our split text
    private let separator: String

    //how many elements have we shown so far
    private var showTextCount = 0

    //reference to our timer
    private var timer = Timer()

    var textComplete: Bool {
        showTextCount == fullText.count
    }

    init(with fullText: String,
         splitBy: ParagraphSplit = .letters,
         delayedBy: Double = 0.2) {

        //how do we want to split our fullText?
        self.splitBy = splitBy

        if splitBy == .letters {
            //store each letter in our fullText array
            self.fullText = fullText.map { String($0) }
            //we want no separator between the letters
            //when we rejoin
            self.separator = ""
        } else {
            //store each word in our fullText array
            self.fullText = fullText.components(separatedBy: .whitespaces)
            //rejoin them with a space in between each word
            self.separator = " "
        }

        //how long do we want to wait in between showing each element?
        self.delay = delayedBy
    }

    //start displaying elements on our schedule
    func start() {
        timer = Timer.scheduledTimer(withTimeInterval: delay, repeats: true) { [self] _ in
            //every time the Timer fires...
            //increment how many elements we are showing
            showTextCount += 1
            //create our partialText by joining the appropriate
            //number of elements
            partialText = fullText[0..<showTextCount].joined(separator: separator)
            //if we've got all elements, stop the timer
            if showTextCount == fullText.count {
                self.stop()
            }
        }
    }

    //stop displaying elements
    //use: clearText == false to later resume where we left off
    //     clearText == true to start over again
    func stop(clearText: Bool = false) {
        timer.invalidate()
        //if we want to clear the text, erase our progress
        if clearText {
            showTextCount = 0
            partialText = ""
        }
    }

    //convenience function for calling stop(clearText: true)
    func reset() {
        stop(clearText: true)
    }
}

struct ParagrapherView: View {
    @StateObject var letterParagrapher = Paragrapher(with: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin dictum eros urna, a eleifend orci aliquam nec. Vivamus non iaculis quam. Morbi ipsum dolor, venenatis.")
    @StateObject var wordParagrapher = Paragrapher(with: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin dictum eros urna, a eleifend orci aliquam nec. Vivamus non iaculis quam. Morbi ipsum dolor, venenatis.", splitBy: .words)

    @State private var timerStopped = false

    var stopStartButton: some View {
        Button {
            if timerStopped {
                letterParagrapher.start()
                wordParagrapher.start()
            } else {
                letterParagrapher.stop()
                wordParagrapher.stop()
            }
            timerStopped.toggle()
        } label: {
            Text("\(timerStopped ? "Start" : "Stop") Timer")
        }
    }

    var stopStartClearButton: some View {
        Button {
            if timerStopped {
                letterParagrapher.start()
                wordParagrapher.start()
            } else {
                letterParagrapher.reset()
                wordParagrapher.reset()
            }
            timerStopped.toggle()
        } label: {
            Text("\(timerStopped ? "Start" : "Stop and Clear") Timer")
        }
    }

    var body: some View {
        VStack(spacing: 20) {
            Text(letterParagrapher.partialText)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .border(letterParagrapher.textComplete ? Color.green : Color.red, width: 1)
                .onAppear {
                    letterParagrapher.start()
                }

            stopStartButton
            stopStartClearButton

            Text(wordParagrapher.partialText)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .border(wordParagrapher.textComplete ? Color.green : Color.red, width: 1)
                .onAppear {
                    wordParagrapher.start()
                }
        }
    }
}

4      

You might want try this out (taking from PRO SWIFT ). It does not give you word-by-word but letter by letter!

struct TypeWriterText: View, Animatable {
    var string: String
    var count = 0

    var animatableData: Double {
        get { Double(count) }
        set { count = Int(max(0, newValue)) }
    }

    var body: some View {
        let stringToShow = String(string.prefix(count))
        Text(stringToShow)
    }
}

and then you can use it

struct ContentView: View {
    @State private var value = 0

    let message = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin dictum eros urna, a eleifend orci aliquam nec. Vivamus non iaculis quam. Morbi ipsum dolor, venenatis."

    var body: some View {
        ZStack {
            VStack {
                TypeWriterText(string: message, count: value)
                    .frame(maxWidth: .infinity, alignment: .leading)

                Spacer()
            }
            .padding()
        }
        .onAppear {
            withAnimation(.linear(duration: 5)) {
                value = message.count
            }
        }
    }
}

2      

thank you both!!

2      

Hacking with Swift is sponsored by Essential Developer.

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until July 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.