Implement A Mobile Chat Interface In An Existing Codebase
OpenAIiOS Engineer · Technical ScreenLast updated
Tests your ability to design and implement a streaming chat interface state manager in an existing mobile codebase, focusing on async token streaming, state modeling, cancellation, and error handling.
Scenario
You are tasked with building the core chat screen logic for an existing iOS application. The project uses unidirectional data flow (UDF) or MVVM with async/await. An existing networking service provides a streaming endpoint that yields text chunks as an AsyncThrowingStream.
Your goal is to implement ChatSessionController, which manages sending messages, streaming assistant replies, handling partial chunk assembly, and supporting cancellation and retries.
Requirements
- Message State Machine: Model messages with explicit statuses:
sending,streaming,completed, andfailed. - Streaming Execution: When
sendMessage(content:)is called:- Append the user message (
completed). - Append an initial assistant message (
streamingwith empty content). - Consume chunks from
ChatService.stream(prompt:), appending each chunk to the active assistant message. - Transition the assistant message to
completedonce the stream finishes.
- Append the user message (
- Cancellation: Support
cancelCurrentStream(), which terminates the active task and leaves the assistant message marked ascompletedwith whatever content was collected so far. - Failure & Retry: If the network stream throws an error, transition the message status to
failed. Provide aretryFailedMessage(id:)method that restarts generation for that message.
Interface
enum MessageRole {
case user
case assistant
}
enum MessageStatus: Equatable {
case sending
case streaming
case completed
case failed(errorDescription: String)
}
struct Message: Identifiable, Equatable {
let id: UUID
let role: MessageRole
var content: String
var status: MessageStatus
}
protocol ChatStreamingService {
func stream(prompt: String) -> AsyncThrowingStream
}
@MainActor
class ChatSessionController: ObservableObject {
@Published private(set) var messages: [Message] = []
init(service: ChatStreamingService) { ... }
func sendMessage(_ text: String) async { ... }
func cancelCurrentStream() { ... }
func retryFailedMessage(id: UUID) async { ... }
}
Example
// Initial state: messages = []
await controller.sendMessage("Hello")
// Step 1: User message added, Assistant placeholder added
// messages[0] = Message(role: .user, content: "Hello", status: .completed)
// messages[1] = Message(role: .assistant, content: "", status: .streaming)
// Step 2: Stream yields "Hi", then " there!"
// messages[1].content -> "Hi there!"
// Step 3: Stream finishes normally
// messages[1].status -> .completed
Edge Cases
- Calling
sendMessagewhile another response is currently streaming (choose to either queue, cancel, or reject new sends). - Empty responses or network errors thrown on the very first token.
- Rapid cancellation right after calling
sendMessagebefore any chunk arrives.
Hints
Hint 1
Retain a reference to the active `Task` handle so you can cancel it deterministically in `cancelCurrentStream()`.
Hint 2
Ensure updates to `@Published` properties occur on the MainActor, especially when iterating over background stream chunks.
Hint 3
When retrying a failed message, replace or reset the failed assistant message's state rather than appending a duplicate entry to the conversation history.
Follow-up questions
- How would you throttle or batch token updates if the API yields dozens of tiny chunks per second to prevent UI layout thrashing?
- How should the UI distinguish between user-initiated scroll behavior and programmatic auto-scroll to the newest streaming token?
- If the app is backgrounded mid-stream, how would you preserve state or transition the connection to a background task?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.