Katkat

Implement A Mobile Chat Interface In An Existing Codebase

OpenAIiOS Engineer · Technical Screen

Last 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

  1. Message State Machine: Model messages with explicit statuses: sending, streaming, completed, and failed.
  2. Streaming Execution: When sendMessage(content:) is called:
    • Append the user message (completed).
    • Append an initial assistant message (streaming with empty content).
    • Consume chunks from ChatService.stream(prompt:), appending each chunk to the active assistant message.
    • Transition the assistant message to completed once the stream finishes.
  3. Cancellation: Support cancelCurrentStream(), which terminates the active task and leaves the assistant message marked as completed with whatever content was collected so far.
  4. Failure & Retry: If the network stream throws an error, transition the message status to failed. Provide a retryFailedMessage(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 sendMessage while 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 sendMessage before 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?