Search code examples
swiftanimationswiftuitextwebapi

Issue with animation prompt and appending string


I have created chatGPT clone. I have one issue with printing the responses when I type and send it will loop my question and cutoff API response.

code

import SwiftUI
import OpenAISwift

final class ViewModel: ObservableObject{
    init(){}
    
    private var client : OpenAISwift?
    
    func setup(){
        client = OpenAISwift(authToken: "sk-")
    }
    
    func send(text:String,completion:@escaping (String) -> Void ){
        self.client?.sendCompletion(with: text,
                                    maxTokens:500,completionHandler: {result in
            switch result {
            case .success(let model):
                let output  =  model.choices.first?.text ?? ""
                completion(output)
            case .failure:
                break
            }
        })
    }
}

struct ContentView: View {
    @ObservedObject var viewModel = ViewModel()
    @State var messageText = ""
    @State var messages = [String]()
    @State private var animating: Bool = false

    var body: some View {
            VStack(alignment: .leading) {
                
                AnimatedTextView(text: messages.joined(separator: "\n"))
                    .frame(height: 100)
                    .frame(maxWidth: .infinity, alignment: .leading)

                Spacer()
                HStack {
                    TextField("type here ...", text: $messageText)
                    Button("Send") {
                        send()
                    }
                }
            }
            .onAppear {
                viewModel.setup()
            }
            .padding()
    }

    private func send() {
        guard !messageText.trimmingCharacters(in: .whitespaces).isEmpty else { return }
        messages.append(messageText)
        viewModel.send(text: messageText) { response in
            DispatchQueue.main.async {
                self.messages.append("ChatGPT: " + response)
                animating = true
            }
        }
    }
}
struct AnimatedTextView: View {
    let text: String

    @State private var letters: [String] = []
    @State private var displayText: String = ""

    var body: some View {
        Text(displayText)
            .font(.system(size: 20))
            .frame(height: 100)
            .frame(maxWidth: .infinity, alignment: .leading)
            .onChange(of: text) { _ in
                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()
            }
        }
    }
}



struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

images enter image description here


Solution

  • First use a StateObject instead of ObservedObject :

    struct ContentView: View {
        @StateObject var viewModel = ViewModel() // Here
        @State var messageText = ""
        @State var messages = [String]()
        @State var lastMessage: String = "" // save last entered message
        @State private var animating: Bool = false
    ...
                    AnimatedTextView(text: $lastMessage) // Binding will auto update
    ...
        private func send() {
            guard !messageText.trimmingCharacters(in: .whitespaces).isEmpty else { return }
            messages.append(messageText)
            lastMessage = messageText + "\n" // Save last message
    

    To animate the text you should use Binding in AnimatedTextView and pass only the last message :

    struct AnimatedTextView: View {
        @Binding var text: String
    

    With this you only send the last question to the AnimatedTextView and update it accordingly.