We’ve all seen it: that mesmerizing "typing" effect when ChatGPT or Claude generates a response. It’s not just a fancy animation; it’s a real-time stream of data called tokens. For developers, replicating this experience in a native app presents a unique set of challenges. How do you keep the UI responsive while receiving a firehose of data? How do you ensure the view automatically scrolls to the bottom as the text grows?
In this guide, we’ll dive into the architecture of a high-performance TokenStreamView. By leveraging Swift’s modern concurrency model and SwiftUI’s ScrollViewReader, you can build a seamless, AI-driven interface that feels fluid and professional.
The Challenge of the "Live" UI
Traditional request-response patterns are easy: you wait for the data, then you show it. Generative AI flips this script. Tokens arrive asynchronously and rapidly. This creates four primary hurdles:
- Asynchronous Data Flow: The UI must stay active while the AI thinks and streams.
- Rapid State Updates: Every new word is a state change. If not handled correctly, your app will flicker or lag.
- Content Growth: As the text expands, the user shouldn't have to manually scroll to see the latest word.
- Thread Safety: Updating the UI from a background AI inference task is a recipe for crashes without proper synchronization.
The Architecture: Actors and @observable
To handle these challenges, we need a robust backend. Swift’s Actors are the perfect tool for isolating the AI's state. By using an actor, we ensure that as tokens stream in from a background task, they are gathered safely before being passed to the UI.
On the UI side, the @observable macro (introduced in iOS 17) is a game-changer. Unlike the older ObservableObject, @Observable provides fine-grained tracking. SwiftUI only re-renders the specific text views that change, which is vital when you're updating the screen 20 times per second.
The Token Stream ViewModel
Here is how you set up a thread-safe ViewModel to manage the incoming stream:
import SwiftUI
import Observation
@Observable
class TokenStore {
var tokens: [String] = []
// Computed property to provide the full string
var fullResponse: String {
tokens.joined()
}
@MainActor
func append(_ token: String) {
tokens.append(token)
}
@MainActor
func reset() {
tokens = []
}
}
Implementing the Auto-Scroll with ScrollViewReader
The "magic" of a chat interface is the automatic scrolling. In SwiftUI, we achieve this using ScrollViewReader. This component provides a proxy that allows us to programmatically move the scroll position to a specific view ID.
A common "pro-tip" for developers is the Invisible Anchor Pattern. Instead of trying to scroll to the last dynamic token ID (which can be finicky), we place a tiny, invisible view at the very bottom of our list and scroll to it whenever the token count changes.
Building the TokenStreamView
struct TokenStreamView: View {
@State private var store = TokenStore()
private let bottomID = "bottomOfStream"
var body: some View {
ScrollViewReader { proxy in
ScrollView {
VStack(alignment: .leading, spacing: 10) {
ForEach(0..<store.tokens.count, id: \.self) { index in
Text(store.tokens[index])
.transition(.opacity)
}
// The invisible anchor
Color.clear
.frame(height: 1)
.id(bottomID)
}
.padding()
}
.onChange(of: store.tokens.count) { _, _ in
// Smoothly scroll to the bottom anchor when a new token arrives
withAnimation(.easeInOut(duration: 0.1)) {
proxy.scrollTo(bottomID, anchor: .bottom)
}
}
}
}
}
Why This Works: Apple’s Design Philosophy
This implementation aligns perfectly with Apple’s modern development pillars:
- Reactive Programming: We define what the view looks like based on the state, and SwiftUI handles the when of the updates.
- Concurrency for Responsiveness: By using
async/await, the heavy lifting of AI inference never blocks the Main Actor, keeping the UI buttery smooth. - Declarative Control:
ScrollViewReaderreplaces complex offset calculations with a simple, declarative intent: "Scroll to this ID."
Conclusion
Building a real-time AI token stream is about more than just displaying text; it's about managing high-frequency data safely and providing a user experience that feels alive. By combining Swift Actors for safety, @Observable for performance, and ScrollViewReader for programmatic control, you can create a top-tier AI interface that stands up to the best apps on the App Store.
Let's Discuss
- How do you handle user-initiated scrolling? If a user scrolls up to read a previous answer, should the app "force" them back to the bottom when a new token arrives, or stay put?
- Have you experimented with
TextFlowor custom layouts to make the token transitions feel more like natural typing and less like "popping" into existence?
The concepts and code demonstrated here are drawn directly from the comprehensive roadmap laid out in the ebook
SwiftUI for AI Apps. Building reactive, intelligent interfaces that respond to model outputs, stream tokens, and visualize AI predictions in real time. You can find it here: Leanpub.com or Amazon.
Check also all the other programming ebooks on python, typescript, c#, swift: Leanpub.com or Amazon.
Top comments (0)