ChatUI is a Swift Package Manager library providing reusable SwiftUI components for building chat interfaces. It has no third-party dependencies.
- Language: Swift 6.0 (strict concurrency enforced)
- Platforms: macOS 14+, iOS 17+, watchOS 8+
- Framework: SwiftUI, Combine, Foundation
- Package manager: Swift Package Manager (SPM)
swift buildswift testThe test suite uses Swift Testing (import Testing), not XCTest. Filter by function name:
swift test --filter "example"For XCTest-style filter syntax:
swift test --filter ChatUITests/exampleswift package cleanswift package resolveswift package generate-xcodeprojNo linting toolchain (SwiftLint, etc.) is currently configured. Follow the style guidelines below manually.
ChatUI/
├── Package.swift # SPM manifest
├── Sources/
│ └── ChatUI/
│ ├── ChatUI.swift # Module entry point (stub)
│ ├── ChatMessageService.swift # Core protocols and data types
│ ├── ChatView.swift # Top-level view + ViewModel
│ ├── CollapsibleMessageView.swift
│ ├── MessageComposerView.swift
│ ├── MessageListView.swift
│ └── View+Extensions.swift # Shared view helpers
└── Tests/
└── ChatUITests/
└── ChatUITests.swift
- Indentation: 4 spaces (no tabs)
- Braces: Opening brace on the same line as the declaration
- Semicolons: Never used
- Line length: No hard limit, but prefer readable line breaks over long lines
- File headers: Every file begins with a comment header:
// FileName.swift // ChatUI // // Created by Reid Chatham on MM/DD/YY. //
- System/Apple frameworks first, then platform-conditional imports in
#ifblocks - No blank lines between standard imports; platform guards are isolated:
import SwiftUI import Combine #if os(iOS) import UIKit #endif
- No third-party imports exist; do not add external dependencies without discussion
| Category | Convention | Examples |
|---|---|---|
| Types (struct, class, enum, protocol) | PascalCase | ChatView, MessageComposerView, ChatAlertInfo |
| Functions and methods | camelCase | sendMessage(), scrollToBottom(scrollProxy:) |
| Properties and variables | camelCase | isMessageSending, voiceInputHandler, localInput |
| Private backing storage | underscore prefix | _settingsView, _messageContent |
| Generic type parameters | PascalCase | MessageService, Message |
| File names | PascalCase matching primary type | ChatView.swift |
| Extension files | TypeName+Category.swift |
View+Extensions.swift |
- Swift 6 strict concurrency is required. All code must compile without concurrency warnings.
- Annotate all ViewModel classes and UI-mutating functions with
@MainActor:@MainActor public class ViewModel: ObservableObject { ... }
- Use
nonisolatedfor properties that must be accessed off the main actor:nonisolated private let messageService: any ChatMessageService
- Mark protocol types and conforming types
Sendablewhere required by the concurrency model. - Use
any Protocol(existential syntax) for protocol-typed references — required in Swift 6:private var voiceInputHandler: (any VoiceInputHandler)?
- Prefer
@Published+ObservableObjectfor state management (not@Observablemacro). - Use
AnyViewfor injected view closures to avoid unbounded generic propagation:private var _messageContent: ((MessageService.ChatMessage) -> AnyView)?
- Use
@ViewBuilderfor optional view-customization parameters in public initializers.
- Define public-facing behavior through protocols with associated types:
public protocol ChatMessageService: Sendable, ObservableObject { associatedtype ChatMessage: ChatMessageInfo }
- Expose generic views parameterized on protocol conformances:
public struct ChatView<MessageService: ChatMessageService>: View { ... }
- Place ViewModel types as inner classes inside
extensionon their owning view:extension ChatView { @MainActor public class ViewModel: ObservableObject { ... } }
- Route errors through a protocol method that returns optional
ChatAlertInfo?; if nil, fall back to a generic error alert:func handleError(_ error: any Error) { if let alertInfo = messageService.handleError(error: error) { self.alertInfo = alertInfo self.showAlert = true } else { self.alertInfo = ChatAlertInfo(title: "Error!", ...) self.showError = true } }
- Wrap async throwing calls with
do/catchand route tohandleError:do { try await messageService.send(message: text, stream: true) } catch { handleError(error) }
- Restore UI state on failure (e.g., restore input text if a send fails):
catch { handleError(error) Task { @MainActor in input = sentText } }
- Use
try?only for genuinely non-critical operations (e.g.,Task.sleep).
- Wrap all platform-specific code in
#if os(iOS)/#else/#endifguards. - Keep conditional blocks focused and minimal — extract to helpers where blocks grow large.
- Use
print(...)for debug output. No dedicated logger abstraction currently exists. - Remove or gate debug prints before merging to main.
- Framework: Swift Testing (
import Testing) — not XCTest. - Tests live in
Tests/ChatUITests/. - Test functions are
async throwsto support async code under test. - Use
#expect(...)for assertions (Swift Testing API). - Import the module under test with
@testable import ChatUI. - Name test functions descriptively in camelCase:
@Test func sendsMessageOnSubmit() async throws.
- Branch naming:
feature/,fix/,refactor/prefixes (e.g.,feature/voice-input-stt) - Commit messages: Use conventional commits format (
feat:,fix:,refactor:,test:,docs:) - Never force-push to
main - Ensure
swift buildandswift testpass before opening a PR