Re-creating ChatGPT iOS App using OpenAI API (2)

After connecting to API, I then decided to add a little more fidelity to my prototype. I had a couple of things that I wanted to enable - such as dynamic scrolling as bot response comes in, nicer animation (typewriter effect) and adding loading state. I'm planning to dive deeper to enable more things but these 3 things were something I wanted to learn so that I can figure out how this interaction with API works.

Make Chat animated

This one was actually very easy. I just asked ChatGPT to make a function like this.

"can I render text in sequenced animation? like paragraph as if it's streamed"

GPT just created a code snippet that I can use. I then tried to understand how it works - for example. I could understand that it's going through fullText (actual bot response) and then go through them one by one and add them to new Text variable (which will be used for rendering animation). This makes an effect as if you're typing the text in real time(Not exactly the same as streaming since it's more dynamic and depends on latency).

func startTypingAnimation() {
        timer?.invalidate()
        currentIndex = 0
        displayedText = ""
        timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { _ in
            if currentIndex < fullText.count {
                let index = fullText.index(fullText.startIndex, offsetBy: currentIndex)
                displayedText.append(fullText[index])
                currentIndex += 1
            } else {
                timer?.invalidate()
            }
        }
  }

And I just add this function to .onAppear so that it can automatically triggered as view starts to be rendered.

Adding Loading State

But the problem was, there was a quite amount of latency before render anything, especially when the prompt was more complex so it needed time to fully fetch the response. But how do I know which is actually latency duration? First thing that came to my mind was the sendNewMessage() event. I can trigger something or right after getBotReply() is triggered. I chose to do on getBotReply() but I don't think it'll make any differences.

So I can trigger loading animation, how do I render it? I was thinking about firing up a function but that felt bit too complicated so resorted with Bool toggle. I set up a Bool variable called isLoading and then flip it true when I trigger getBotReply(). Something like this. It is rendered and used whenever variable turns true. Scrappy way to do it but it works.

if chatController.isLoading == true {
  Circle()
    .frame(width:20,height:20)
    .scaleEffect(loaderscale)
    .onAppear {
        loaderscale = 0
        withAnimation(.spring().repeatForever(autoreverses: true)) {
        loaderscale = 1
        }
      }
    }

Scrolling

This one was the thing that I had the most trouble with. The thing that was most interesting for ChatGPT app was how it dynamically renders the text as the data streams in. Definitely conveying that generative feel or something is created right in front of me. Typing animation would do a little bit of it but eventually it'll cut off and you need to scroll for more contents. So I wanted to move the view a little bit as the bot response gets longer.

I tried to tie this initially - basically pushing the view down as bot response size grew :

GeometryReader { geometry -> Color in
  DispatchQueue.main.async {
    let contentHeight = geometry.frame(in: .global).height
        if contentHeight > previousContentHeight {
              scrollViewProxy.scrollTo(messages.last?.id, anchor: .bottom)
             }
         previousContentHeight = contentHeight
  }
}

Then quickly realized that I can't cancel the animation when bot response is too long, got bored and can do nothing but staring at the screen. This is just keep pushing the view down and you can do nothing else while this animation is being triggered. There might be a different way to enable cancellable animation but I just didn't know how to do it with SwiftUI. So I gave it a rest for a couple of days, occasionally revisit and try a couple of different thing with the help of ChatGPT.

First thing that I tried was listening if user start dragging - that wasn't very successful since it won't listen or react as I wanted. Sometimes it worked but mostly it just didn't react at all. Secondly, I tried to put this GeometryReader in different UI element. So far it was within ScrollView and wasn't very performant either since view kept grew and scrolled at the same time.

I then somehow put this on ScrollView background, and I realized now I can scroll while animation is being triggered. I tried to make sense of it, my guess was since it's just listening to background size, and then just scroll to the bottom of the page instead of individual message elements, it now can listen to general scroll interaction event…?

But then when response got really long, it was painful to watch until everything scrolled / rendered. I noticed iOS ChatGPT app actually stops scrolling at some point and show a button so that user can choose to scroll to the bottom. Amount of information that you can absorb in certain amount of time is limited. So that felt like a good move.

This was rather simple. I just set up stopScroll as Bool variable and toggled it as the message height grew past certain threshold and entire view size is initially over device viewport. Because a) the button doesn't need to be seen when initial viewport is not filled b) I wanted initial response scroll a certain amount (in this case, 360pt vertically). All based on my personal preferences and experience.

if id == message.id { //registering the latest message height
  delta = c.frame(in: .named(message.id)).size.height
} 

if delta > 360 && previousContentHeight > 800 { //comparing view size (800 for simplicity) and message height
  scrollStop = true
}

And then I rendered a simple button that shows up at the bottom.

This concludes my exploration on simple text-based GPT wrapper. It took me the longest time to tune but also I was able to learn a lot in terms of understanding logic and fetching data, triggering animation etc. I paid $10 to sign up for API and after all these exploration I still have $9.62 as a balance. I might want to try some more until I fully use the balance.

If you're interested, you can download the final version on Github : https://github.com/radiofun/ChatGPT-wrapper-swiftui


Re-creating ChatGPT iOS App using OpenAI API (2)

After connecting to API, I then decided to add a little more fidelity to my prototype. I had a couple of things that I wanted to enable - such as dynamic scrolling as bot response comes in, nicer animation (typewriter effect) and adding loading state. I'm planning to dive deeper to enable more things but these 3 things were something I wanted to learn so that I can figure out how this interaction with API works.

Make Chat animated

This one was actually very easy. I just asked ChatGPT to make a function like this.

"can I render text in sequenced animation? like paragraph as if it's streamed"

GPT just created a code snippet that I can use. I then tried to understand how it works - for example. I could understand that it's going through fullText (actual bot response) and then go through them one by one and add them to new Text variable (which will be used for rendering animation). This makes an effect as if you're typing the text in real time(Not exactly the same as streaming since it's more dynamic and depends on latency).

func startTypingAnimation() {
        timer?.invalidate()
        currentIndex = 0
        displayedText = ""
        timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { _ in
            if currentIndex < fullText.count {
                let index = fullText.index(fullText.startIndex, offsetBy: currentIndex)
                displayedText.append(fullText[index])
                currentIndex += 1
            } else {
                timer?.invalidate()
            }
        }
  }

And I just add this function to .onAppear so that it can automatically triggered as view starts to be rendered.

Adding Loading State

But the problem was, there was a quite amount of latency before render anything, especially when the prompt was more complex so it needed time to fully fetch the response. But how do I know which is actually latency duration? First thing that came to my mind was the sendNewMessage() event. I can trigger something or right after getBotReply() is triggered. I chose to do on getBotReply() but I don't think it'll make any differences.

So I can trigger loading animation, how do I render it? I was thinking about firing up a function but that felt bit too complicated so resorted with Bool toggle. I set up a Bool variable called isLoading and then flip it true when I trigger getBotReply(). Something like this. It is rendered and used whenever variable turns true. Scrappy way to do it but it works.

if chatController.isLoading == true {
  Circle()
    .frame(width:20,height:20)
    .scaleEffect(loaderscale)
    .onAppear {
        loaderscale = 0
        withAnimation(.spring().repeatForever(autoreverses: true)) {
        loaderscale = 1
        }
      }
    }

Scrolling

This one was the thing that I had the most trouble with. The thing that was most interesting for ChatGPT app was how it dynamically renders the text as the data streams in. Definitely conveying that generative feel or something is created right in front of me. Typing animation would do a little bit of it but eventually it'll cut off and you need to scroll for more contents. So I wanted to move the view a little bit as the bot response gets longer.

I tried to tie this initially - basically pushing the view down as bot response size grew :

GeometryReader { geometry -> Color in
  DispatchQueue.main.async {
    let contentHeight = geometry.frame(in: .global).height
        if contentHeight > previousContentHeight {
              scrollViewProxy.scrollTo(messages.last?.id, anchor: .bottom)
             }
         previousContentHeight = contentHeight
  }
}

Then quickly realized that I can't cancel the animation when bot response is too long, got bored and can do nothing but staring at the screen. This is just keep pushing the view down and you can do nothing else while this animation is being triggered. There might be a different way to enable cancellable animation but I just didn't know how to do it with SwiftUI. So I gave it a rest for a couple of days, occasionally revisit and try a couple of different thing with the help of ChatGPT.

First thing that I tried was listening if user start dragging - that wasn't very successful since it won't listen or react as I wanted. Sometimes it worked but mostly it just didn't react at all. Secondly, I tried to put this GeometryReader in different UI element. So far it was within ScrollView and wasn't very performant either since view kept grew and scrolled at the same time.

I then somehow put this on ScrollView background, and I realized now I can scroll while animation is being triggered. I tried to make sense of it, my guess was since it's just listening to background size, and then just scroll to the bottom of the page instead of individual message elements, it now can listen to general scroll interaction event…?

But then when response got really long, it was painful to watch until everything scrolled / rendered. I noticed iOS ChatGPT app actually stops scrolling at some point and show a button so that user can choose to scroll to the bottom. Amount of information that you can absorb in certain amount of time is limited. So that felt like a good move.

This was rather simple. I just set up stopScroll as Bool variable and toggled it as the message height grew past certain threshold and entire view size is initially over device viewport. Because a) the button doesn't need to be seen when initial viewport is not filled b) I wanted initial response scroll a certain amount (in this case, 360pt vertically). All based on my personal preferences and experience.

if id == message.id { //registering the latest message height
  delta = c.frame(in: .named(message.id)).size.height
} 

if delta > 360 && previousContentHeight > 800 { //comparing view size (800 for simplicity) and message height
  scrollStop = true
}

And then I rendered a simple button that shows up at the bottom.

This concludes my exploration on simple text-based GPT wrapper. It took me the longest time to tune but also I was able to learn a lot in terms of understanding logic and fetching data, triggering animation etc. I paid $10 to sign up for API and after all these exploration I still have $9.62 as a balance. I might want to try some more until I fully use the balance.

If you're interested, you can download the final version on Github : https://github.com/radiofun/ChatGPT-wrapper-swiftui


Re-creating ChatGPT iOS App using OpenAI API (2)

After connecting to API, I then decided to add a little more fidelity to my prototype. I had a couple of things that I wanted to enable - such as dynamic scrolling as bot response comes in, nicer animation (typewriter effect) and adding loading state. I'm planning to dive deeper to enable more things but these 3 things were something I wanted to learn so that I can figure out how this interaction with API works.

Make Chat animated

This one was actually very easy. I just asked ChatGPT to make a function like this.

"can I render text in sequenced animation? like paragraph as if it's streamed"

GPT just created a code snippet that I can use. I then tried to understand how it works - for example. I could understand that it's going through fullText (actual bot response) and then go through them one by one and add them to new Text variable (which will be used for rendering animation). This makes an effect as if you're typing the text in real time(Not exactly the same as streaming since it's more dynamic and depends on latency).

func startTypingAnimation() {
        timer?.invalidate()
        currentIndex = 0
        displayedText = ""
        timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { _ in
            if currentIndex < fullText.count {
                let index = fullText.index(fullText.startIndex, offsetBy: currentIndex)
                displayedText.append(fullText[index])
                currentIndex += 1
            } else {
                timer?.invalidate()
            }
        }
  }

And I just add this function to .onAppear so that it can automatically triggered as view starts to be rendered.

Adding Loading State

But the problem was, there was a quite amount of latency before render anything, especially when the prompt was more complex so it needed time to fully fetch the response. But how do I know which is actually latency duration? First thing that came to my mind was the sendNewMessage() event. I can trigger something or right after getBotReply() is triggered. I chose to do on getBotReply() but I don't think it'll make any differences.

So I can trigger loading animation, how do I render it? I was thinking about firing up a function but that felt bit too complicated so resorted with Bool toggle. I set up a Bool variable called isLoading and then flip it true when I trigger getBotReply(). Something like this. It is rendered and used whenever variable turns true. Scrappy way to do it but it works.

if chatController.isLoading == true {
  Circle()
    .frame(width:20,height:20)
    .scaleEffect(loaderscale)
    .onAppear {
        loaderscale = 0
        withAnimation(.spring().repeatForever(autoreverses: true)) {
        loaderscale = 1
        }
      }
    }

Scrolling

This one was the thing that I had the most trouble with. The thing that was most interesting for ChatGPT app was how it dynamically renders the text as the data streams in. Definitely conveying that generative feel or something is created right in front of me. Typing animation would do a little bit of it but eventually it'll cut off and you need to scroll for more contents. So I wanted to move the view a little bit as the bot response gets longer.

I tried to tie this initially - basically pushing the view down as bot response size grew :

GeometryReader { geometry -> Color in
  DispatchQueue.main.async {
    let contentHeight = geometry.frame(in: .global).height
        if contentHeight > previousContentHeight {
              scrollViewProxy.scrollTo(messages.last?.id, anchor: .bottom)
             }
         previousContentHeight = contentHeight
  }
}

Then quickly realized that I can't cancel the animation when bot response is too long, got bored and can do nothing but staring at the screen. This is just keep pushing the view down and you can do nothing else while this animation is being triggered. There might be a different way to enable cancellable animation but I just didn't know how to do it with SwiftUI. So I gave it a rest for a couple of days, occasionally revisit and try a couple of different thing with the help of ChatGPT.

First thing that I tried was listening if user start dragging - that wasn't very successful since it won't listen or react as I wanted. Sometimes it worked but mostly it just didn't react at all. Secondly, I tried to put this GeometryReader in different UI element. So far it was within ScrollView and wasn't very performant either since view kept grew and scrolled at the same time.

I then somehow put this on ScrollView background, and I realized now I can scroll while animation is being triggered. I tried to make sense of it, my guess was since it's just listening to background size, and then just scroll to the bottom of the page instead of individual message elements, it now can listen to general scroll interaction event…?

But then when response got really long, it was painful to watch until everything scrolled / rendered. I noticed iOS ChatGPT app actually stops scrolling at some point and show a button so that user can choose to scroll to the bottom. Amount of information that you can absorb in certain amount of time is limited. So that felt like a good move.

This was rather simple. I just set up stopScroll as Bool variable and toggled it as the message height grew past certain threshold and entire view size is initially over device viewport. Because a) the button doesn't need to be seen when initial viewport is not filled b) I wanted initial response scroll a certain amount (in this case, 360pt vertically). All based on my personal preferences and experience.

if id == message.id { //registering the latest message height
  delta = c.frame(in: .named(message.id)).size.height
} 

if delta > 360 && previousContentHeight > 800 { //comparing view size (800 for simplicity) and message height
  scrollStop = true
}

And then I rendered a simple button that shows up at the bottom.

This concludes my exploration on simple text-based GPT wrapper. It took me the longest time to tune but also I was able to learn a lot in terms of understanding logic and fetching data, triggering animation etc. I paid $10 to sign up for API and after all these exploration I still have $9.62 as a balance. I might want to try some more until I fully use the balance.

If you're interested, you can download the final version on Github : https://github.com/radiofun/ChatGPT-wrapper-swiftui