AI Chat Interface with Voice Input

Complete chat module with message list, streaming-ready input bar, voice-to-text via VolcEngine ASR, and AI conversation UI patterns (iOS only)

Free
Free with a ShipSwift account.

Copies a short instruction with your private download link. Paste it into your agent: it downloads the whole skill (the source code, the setup steps and the mistakes we already made for you) and builds the screen.

Works with

+70

What it does

Provides a production-ready AI chat interface with an auto-scrolling message list (no CPU spikes during streaming), a text input bar with optional voice-to-text via VolcEngine ASR, and composable bubble styling — ready to connect to any AI backend.

Permission timing strategy: the ASR module uses the use-site request pattern — microphone authorization is requested when startRecording() is called. For an overview of all three permission patterns (use-site / onboarding prefetch / deferred), see Permission Prefetch Pattern.

What you get

  • ✓1,805 lines of source code, ready to paste
  • ✓Architecture
  • ✓Dependencies
  • ✓Implementation
  • ✓Integration Checklist
  • ✓Common Customizations
  • ✓Known Pitfalls
chatAIvoice inputASRstreamingconversationSwiftUI

Architecture

iOS App
  SWChatView (all-in-one chat component)
    |
    |--- SWMessageList (auto-scrolling message list)
    |       |--- SWMessageBubble (alignment wrapper)
    |       |--- ScrollViewReader + throttleScroll (anchors scroll to bottom)
    |
    |--- SWChatInputView (text + voice input bar)
    |       |--- TextField (expandable, 1-5 lines)
    |       |--- Microphone button (hidden when asrConfig is nil)
    |       |--- SWAudioWaveformView (animated bars during recording)
    |       |--- Send button
    |       |
    |       |--- SWVolcEngineASRService (speech-to-text)
    |               |--- AVAudioEngine (microphone capture)
    |               |--- NWConnection (WebSocket to VolcEngine)
    |               |--- Binary protocol (gzip compressed)
    |               |--- Callbacks: onTranscriptionUpdate / onTranscriptionComplete / onError
    |
    |--- SWChatMessage (Identifiable message model)
    |
    v
  Your AI Backend (OpenAI, Claude, custom, etc.)

Data flow:

  1. User types or speaks a message
  2. SWChatView appends the user message to the binding automatically
  3. onSend callback fires with the text — you forward it to your AI backend
  4. You append the AI response to the same messages array
  5. The auto-scrolling list keeps the newest message visible with no layout issues

Integration Checklist

  • Add microphone permission to Info.plist:
  <key>NSMicrophoneUsageDescription</key>
  <string>Microphone access is needed for voice input</string>

Required only if you use the voice input feature (asrConfig is non-nil).

  • Obtain VolcEngine ASR credentials (voice input only):
  • Sign up at VolcEngine Console
  • Enable the Streaming ASR service
  • Create an application to get appId and accessToken
  • Choose the correct cluster for your region (default: volcengine_streaming_common)

The accessToken you pass to SWASRConfig ships inside your app. Don't hard-code a long-lived token in source control; prefer fetching it from your backend at runtime so you can rotate it.

  • Add the files to your Xcode project:
  • SWChatView+iOS.swift
  • SWChatInputView+iOS.swift
  • SWMessageList+iOS.swift
  • SWVolcEngineASRService+iOS.swift
  • SWDebugLog.swift — skip it if your project already has ShipSwift's SWUtil or the paywall-kit skill

If your app also builds for macOS, limit the four +iOS files to iOS in Build Phases > Compile Sources.

  • Connect to your AI backend in the onSend callback:
  SWChatView(messages: $messages) { text in
      Task {
          let response = await yourAIService.chat(text)
          messages.append(SWChatMessage(content: response, isUser: false))
      }
  }
  • Set deployment target to iOS 18.0 or later.

Known Pitfalls

1. Microphone permission must be requested at runtime

iOS requires runtime permission for microphone access. SWVolcEngineASRService.startRecording() handles this automatically, but if the user denies permission, it throws SWASRError.microphonePermissionDenied. Always handle this error gracefully (e.g., show an alert directing the user to Settings).

2. Do NOT use ScrollView + LazyVStack for chat

ScrollView + LazyVStack causes 100% CPU usage during streaming updates due to infinite layout calculation loops. SWMessageList uses List with ScrollViewReader and throttled auto-scroll instead, providing smooth bottom-anchoring without coordinate system inversion. See the doc comments in SWMessageList+iOS.swift for details.

3. Audio session configuration on iOS

The ASR service sets the audio session to .playAndRecord with .defaultToSpeaker and .allowBluetoothA2DP, and deactivates it (with .notifyOthersOnDeactivation, so music from other apps resumes) when recording stops or is cancelled. If your app plays its own audio (e.g., media playback or text-to-speech), set the category you need again before playing:

// After stopRecording()
try AVAudioSession.sharedInstance().setCategory(.playback)

4. VolcEngine WebSocket uses a custom binary protocol

The VolcEngine ASR API does not use standard WebSocket text frames. It uses a custom binary protocol with gzip compression. The SWVolcEngineASRService handles all binary encoding/decoding internally. If you need to debug, look at the buildFullClientRequest and handleServerResponse methods.

5. Throttled scroll timing

The auto-scroll uses a 400ms leading throttle with a 450ms trailing guarantee. During very fast streaming (many tokens per second), the scroll fires at most once per 400ms to prevent jank, then a final trailing scroll ensures the list ends at the correct position. These values work well for typical AI streaming speeds — adjust only if you have unusual update patterns.

6. SWChatMessage is immutable by design

SWChatMessage uses let properties for thread safety. For streaming responses, replace the entire message object in the array rather than mutating properties. See the "Support streaming AI responses" section above.

7. Keyboard dismissal behavior

SWMessageList uses .scrollDismissesKeyboard(.immediately) to dismiss the keyboard when scrolling. The send button also dismisses focus before calling onSend() to prevent the keyboard from popping back up after sending.

8. ASR service lifecycle

Each recording session creates a new SWVolcEngineASRService instance and a new WebSocket connection. The service is not designed to be reused across multiple recording sessions. This is by design -- each session needs a fresh connection with a unique request ID. SWChatInputView cancels the previous session before starting a new one, and cancels a running session when the input bar leaves the screen, so the microphone and the connection are always released. onTranscriptionComplete is called once when the stream ends, even if nothing was recognized (with an empty string), and the microphone is released if the connection drops while still recording; after cancelRecording() no further callbacks are delivered.

More in Main screens

Browse places to stay: a search that opens into where, when and who, category tabs, and big listing cards whose photo zooms into the full listing.
Stay explorer
Pro
Browse places to stay: a search that opens into where, when and who, category tabs, and big listing cards whose photo zooms into the full listing.
A daily brief, live talent signals, and an AI agent that sources candidates for you in chat.
AI daily brief
Pro
A daily brief, live talent signals, and an AI agent that sources candidates for you in chat.
Hundreds of items as stickers with real physics. Filter by room and they fall, switch views, or spin them into a 3D sphere.
Physics sticker wall
Pro
Hundreds of items as stickers with real physics. Filter by room and they fall, switch views, or spin them into a 3D sphere.
Swipe through candidates, shortlist the best, and send a note your AI agent drafts for you.
Swipe and shortlist
Pro
Swipe through candidates, shortlist the best, and send a note your AI agent drafts for you.
Compare finalists side by side: a skills radar, scores and key facts. Swap people in and it all updates.
Side-by-side compare
Pro
Compare finalists side by side: a skills radar, scores and key facts. Swap people in and it all updates.
Everything you own, totalled by category and by room. Tap a room to drill in.
Totals dashboard
Pro
Everything you own, totalled by category and by room. Tap a room to drill in.
Local tastemakers and the spots they love.
Curator feed
Pro
Local tastemakers and the spots they love.
Top picks on pinned order slips, creators' notes on a ring, and the place screen behind them.
Creator city guide
Pro
Top picks on pinned order slips, creators' notes on a ring, and the place screen behind them.
Every trip at a glance. Open one and check items off as you pack.
Trips and checklists
Pro
Every trip at a glance. Open one and check items off as you pack.
Every recommended spot, clustered on the map.
Clustered map
Pro
Every recommended spot, clustered on the map.