Search code examples
swiftuitextcore-animationcombine

How can I create a ChatGPT print Animation in SwiftUI?


I have created a small ChatGPT clone using SwiftUI to communicate with his/her API. How I can create a print-style animation when the response comes from the API?

struct ContentView: View {
    @ObservedObject var viewModel = ViewModel()
    @State var text = ""
    @State var models = [String]()

    var body: some View {

            VStack(alignment: .leading) {
                ForEach($models, id: \.self) { $string in
                    Text(string)
                }
                Spacer()
                HStack {
                    TextField("type here ...", text: $text)
                    Button("Send") {
                        send()
                    }
                }
            }
            .onAppear {
                viewModel.setup()
            }
            .padding()
    }

    private func send() {
        guard !text.trimmingCharacters(in: .whitespaces).isEmpty else { return }
        models.append("Me: \(text)")
        viewModel.send(text: text) { response in
            DispatchQueue.main.async {
                self.models.append("ChatGPT: " + response)
                self.text = ""
            }
        }
    }
}

Solution

  • You can split text by words or letters, and then add it one by one. Here is an example.

    Demo

    struct ContentView: View {
    
        @State private var letters: [String] = []
        @State private var text: String = "I have created a small ChatGPT clone using SwiftUI to communicate with his/her API. How can I create a print-style animation when the response come from the API?"
        @State private var displayText: String = ""
        @State private var animating: Bool = false
    
        var body: some View {
            VStack {
                Text(displayText)
                    .frame(height: 100)
                    .frame(maxWidth: .infinity, alignment: .leading)
                Button(action: animate) {
                    Text("Animate")
                        .frame(maxWidth: .infinity)
                        .font(.headline)
                        .foregroundColor(.white)
                        .padding()
                        .background(animating ? .black.opacity(0.75) : .black)
                        .cornerRadius(12)
                }
                .disabled(animating)
            }
            .padding()
        }
    
        private func animate() {
            animating = true
            displayText = ""
            letters = []
            letters = Array(text).map { String($0) }
            loop()
        }
    
        private func loop() {
            let pace = 0.05
    
            if !letters.isEmpty {
                DispatchQueue.main.asyncAfter(deadline: .now() + pace) {
                    displayText += letters.removeFirst()
                    loop()
                }
            } else {
                animating = false
            }
    
        }
    }