SwiftCUI
SwiftCUI is a framework for building character user interfaces in Swift with a declarative style inspired by SwiftUI.
Instead of driving the UI through an interactive terminal session, a SwiftCUI app runs as a TCP server and is controlled through request/response commands. That makes it a good fit for both human operators and AI agents that need deterministic, scriptable interactions.
Claude Code Plugin
This repository now ships both a plugin manifest and a marketplace manifest under .claude-plugin.
- Plugin name:
swiftcui
- Marketplace name:
koher
To add the marketplace from GitHub and install the plugin:
/plugin marketplace add koher/swift-cui
/plugin install swiftcui@koher
Why SwiftCUI
- Declarative screen definitions with Swift syntax
- Text-first interaction model that works well for automation
- A thin CUI layer that can share ViewModels with SwiftUI or other frontends
- Async lifecycle hooks for loading and background work
- A companion
scui script in the SwiftCUI skill for inspecting and operating a running app
Core Concepts
- A screen is a
View
- A
View renders a Component
body returns a Component, not another View
- User operations are modeled as
Action values
- Actions are encoded as JSON and sent through the
scui script
- Views update state in
perform(_:)
- Navigation uses
navigator.present(), navigator.dismiss(), and navigator.dismissAll()
- Top-level tab switching uses
TabActionHandler and navigator.activateStack(_:)
- An app starts from
Application
Minimal Example
import SwiftCUI
struct SearchView: View {
@Environment(\.navigator) var navigator
let viewModel = SearchViewModel()
var body: some Component {
"# Search"
""
if viewModel.isLoading {
"Searching..."
} else if let error = viewModel.errorMessage {
"Error: \(error)"
} else if viewModel.items.isEmpty {
"No results. Use the search action."
} else {
for (index, item) in viewModel.items.enumerated() {
"\(index). \(item.name)"
}
}
}
enum Action: ActionProtocol {
case search(query: String)
case detail(index: Int)
var hint: String {
switch self {
case .search: "Search items"
case .detail: "Open detail"
}
}
}
var actions: [Action] {
var result: [Action] = [.search(query: "...")]
if !viewModel.items.isEmpty {
result.append(.detail(index: 0))
}
return result
}
func onAppear() async {
await viewModel.loadInitialData()
}
func perform(_ action: Action) async {
switch action {
case .search(let query):
viewModel.query = query
await viewModel.search()
case .detail(let index):
guard viewModel.items.indices.contains(index) else { return }
navigator.present(DetailView(id: viewModel.items[index].id))
}
}
}
Lifecycle Model
Use the View lifecycle hooks according to their role:
onAppear(): work required before the first rendered response
task(): long-running background work after the response is returned
onDisappear(): cleanup when the view leaves the stack
In practice, initial data loading usually belongs in onAppear(). Use task() for observation, polling, or subscriptions that should continue in the background.
Actions
Actions typically use a Codable enum that conforms to ActionProtocol.
enum Action: ActionProtocol {
case refresh
case search(query: String)
case detail(index: Int)
var hint: String {
switch self {
case .refresh: "Refresh data"
case .search: "Search items"
case .detail: "Open detail"
}
}
}
Only return actions that are actually available in the current UI state. SwiftCUI renders each action as JSON plus its hint, so the user or agent can invoke it directly.
Navigation
Get the navigator from the environment:
@Environment(\.navigator) var navigator
Then navigate with:
navigator.present(DetailView(id: item.id))
navigator.dismiss()
navigator.dismissAll()
Tabs
For multiple top-level screens, define a TabActionHandler.
import SwiftCUI
struct Tabs: TabActionHandler {
@Environment(\.navigator) var navigator
enum Action: String, ActionProtocol, Hashable {
case search, trending, user
var hint: String {
switch self {
case .search: "Go to Search"
case .trending: "Go to Trending"
case .user: "Go to User"
}
}
}