Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,15 @@ site/

# Python virtual environment
.venv/

# Claude Code
.claude/

# Xcode user data
xcuserdata/

# Internal docs
docs/guides/competitive-analysis-pr729.md

# Misc
samples/client/lit/component_gallery/package-lock.json
19 changes: 19 additions & 0 deletions renderers/swiftui/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Xcode
DerivedData/
*.xcuserstate
xcuserdata/

# Swift Package Manager
.build/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.swiftpm/xcode/xcuserdata/

# macOS
.DS_Store

# IDE
*.xcworkspace/xcuserdata/

# Development notes (not part of the library)
DEVELOPMENT_PLAN.md
PR_REVIEW.md
46 changes: 46 additions & 0 deletions renderers/swiftui/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// swift-tools-version: 5.9

// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import PackageDescription

let package = Package(
name: "A2UI",
platforms: [
.iOS(.v17),
.macOS(.v14),
.tvOS(.v17),
.watchOS(.v10),
.visionOS(.v1),
],
products: [
.library(
name: "A2UI",
targets: ["A2UI"]
),
],
targets: [
.target(
name: "A2UI",
path: "Sources/A2UI"
),
.testTarget(
name: "A2UITests",
dependencies: ["A2UI"],
path: "Tests/A2UITests",
resources: [.copy("TestData")]
),
]
)
179 changes: 179 additions & 0 deletions renderers/swiftui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# A2UI SwiftUI Renderer

A native SwiftUI renderer for the [A2UI](https://github.com/google/A2UI) protocol.
Renders agent-generated JSON into native iOS and macOS interfaces using SwiftUI.

## Requirements

- iOS 17.0+ / macOS 14.0+
- Swift 5.9+
- Xcode 15+

## Installation

Since the `Package.swift` lives in the `renderers/swiftui/` subdirectory (not the
repository root), use a local path reference:

**In `Package.swift`:**

```swift
dependencies: [
.package(path: "../path/to/A2UI/renderers/swiftui"),
]
```

**In Xcode:** File → Add Package Dependencies → Add Local… → select the
`renderers/swiftui` directory.

## Quick Start

```swift
import A2UI

// 1. Load A2UI messages (from a JSON file, network response, etc.)
let data = try Data(contentsOf: jsonFileURL)
let messages = try JSONDecoder().decode([ServerToClientMessage].self, from: data)

// 2. Render the surface
A2UIRendererView(messages: messages)
```

### Live Agent Streaming

```swift
import A2UI

// Stream messages from an A2A agent
A2UIRendererView(stream: messageStream, onAction: { action in
print("User triggered: \(action.name)")
})
```

### JSONL Stream Parsing

```swift
import A2UI

let parser = JSONLStreamParser()
let manager = SurfaceManager()

// Parse from async byte stream (e.g. URLSession)
let (bytes, _) = try await URLSession.shared.bytes(for: request)
for try await message in parser.messages(from: bytes) {
try manager.processMessage(message)
}

// Render in SwiftUI — View only observes, no stream logic
A2UIRendererView(manager: manager)
```

## Supported Components

All 18 standard A2UI components are implemented:

| Category | Components |
|----------|-----------|
| Display | Text, Image, Icon, Video, AudioPlayer, Divider |
| Layout | Row, Column, List, Card, Tabs, Modal |
| Input | Button, TextField, CheckBox, DateTimeInput, Slider, MultipleChoice |

### Component Mapping

| A2UI Component | SwiftUI Implementation |
|---------------|----------------------|
| Text | `SwiftUI.Text` with usageHint → font mapping (h1–h6) |
| Image | `AsyncImage` with usageHint variants (avatar, icon, feature, header) |
| Icon | `Image(systemName:)` with Material → SF Symbol mapping |
| Video | `AVPlayerViewController` (iOS) / `VideoPlayer` (macOS) |
| AudioPlayer | `AVPlayer` with custom play/pause controls |
| Row | `HStack` with distribution and alignment |
| Column | `VStack` with distribution and alignment |
| List | `LazyVStack` / `LazyHStack` with template support |
| Card | Rounded-corner container with shadow |
| Tabs | Segmented tab bar with content switching |
| Modal | `.sheet` presentation |
| Button | Primary / secondary styles with action callbacks |
| TextField | `SwiftUI.TextField` / `TextEditor` with two-way binding |
| CheckBox | `Toggle` |
| DateTimeInput | `DatePicker` |
| Slider | `SwiftUI.Slider` |
| MultipleChoice | Checkbox list or chips (FlowLayout) with filtering |
| Divider | `SwiftUI.Divider` |

## Architecture

```
Sources/A2UI/
├── Models/ Codable data models (Messages, Components, Primitives)
├── Processing/ SurfaceViewModel (state) + JSONLStreamParser (streaming)
├── Views/ A2UIComponentView (recursive renderer)
├── Styling/ A2UIStyle + SwiftUI Environment integration
├── Networking/ A2AClient (JSON-RPC over HTTP)
└── A2UIRenderer.swift Public API entry point
```

The renderer uses `@Observable` (Observation framework) for property-level
reactivity, matching the Signal-based approach used by the official Lit and
Angular renderers.

## Running Tests

```bash
cd renderers/swiftui
swift test
```

84 tests across 5 test files cover message decoding, component parsing, data
binding, path resolution, template rendering, catalog functions, validation,
JSONL streaming, incremental updates, and Codable round-trips.

## Demo App

The demo app is located at `samples/client/swiftui/A2UIDemoApp/` in the
repository root. It demonstrates both offline sample rendering and live A2A
agent integration.

Open `samples/client/swiftui/A2UIDemoApp/A2UIDemoApp.xcodeproj` in Xcode and run on a
simulator or device.

## Known Limitations

- Requires iOS 17+ / macOS 14+ (uses `@Observable` from the Observation framework).
- Custom (non-standard) component types are decoded but not rendered.
- Video playback uses `UIViewControllerRepresentable` on iOS; macOS uses a
`VideoPlayer` fallback.
- No built-in Content Security Policy enforcement for image/video URLs —
applications should validate URLs from untrusted agents.

## Security

**Important:** The sample code provided is for demonstration purposes and
illustrates the mechanics of A2UI and the Agent-to-Agent (A2A) protocol. When
building production applications, it is critical to treat any agent operating
outside of your direct control as a potentially untrusted entity.

All operational data received from an external agent — including its AgentCard,
messages, artifacts, and task statuses — should be handled as untrusted input.
For example, a malicious agent could provide crafted data in its fields (e.g.,
name, skills.description) that, if used without sanitization to construct
prompts for a Large Language Model (LLM), could expose your application to
prompt injection attacks.

Similarly, any UI definition or data stream received must be treated as
untrusted. Malicious agents could attempt to spoof legitimate interfaces to
deceive users (phishing), inject malicious scripts via property values (XSS),
or generate excessive layout complexity to degrade client performance (DoS). If
your application supports optional embedded content (such as iframes or web
views), additional care must be taken to prevent exposure to malicious external
sites.

**Developer Responsibility:** Failure to properly validate data and strictly
sandbox rendered content can introduce severe vulnerabilities. Developers are
responsible for implementing appropriate security measures — such as input
sanitization, Content Security Policies (CSP), strict isolation for optional
embedded content, and secure credential handling — to protect their systems and
users.

## License

Apache 2.0 — see [LICENSE](LICENSE).
76 changes: 76 additions & 0 deletions renderers/swiftui/Sources/A2UI/A2UIRenderer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import SwiftUI

/// The main entry point for rendering A2UI surfaces in SwiftUI.
///
/// This view is purely declarative — it observes a `SurfaceManager` and renders
/// its surfaces. Message processing, stream consumption, and error handling
/// belong in the app layer.
///
/// Usage:
/// ```swift
/// // App layer: manage stream + errors
/// let manager = SurfaceManager()
/// for try await message in parser.messages(from: bytes) {
/// try manager.processMessage(message)
/// }
///
/// // View layer: just render
/// A2UIRendererView(manager: manager, onAction: { action in
/// Task { try await client.sendAction(action, surfaceId: "main") }
/// })
/// ```
public struct A2UIRendererView: View {
private let manager: SurfaceManager
private let onAction: ((ResolvedAction) -> Void)?

public init(
manager: SurfaceManager,
onAction: ((ResolvedAction) -> Void)? = nil
) {
self.manager = manager
self.onAction = onAction
}

public var body: some View {
Group {
if manager.orderedSurfaceIds.isEmpty {
ContentUnavailableView(
"No Surface",
systemImage: "rectangle.dashed",
description: Text("Waiting for A2UI messages…")
)
} else {
VStack(spacing: 0) {
ForEach(manager.orderedSurfaceIds, id: \.self) { surfaceId in
if let vm = manager.surfaces[surfaceId],
let rootNode = vm.componentTree {
ScrollView {
A2UIComponentView(
node: rootNode, viewModel: vm
)
.padding()
}
.tint(vm.a2uiStyle.primaryColor)
.environment(\.a2uiStyle, vm.a2uiStyle)
}
}
}
}
}
.environment(\.a2uiActionHandler, onAction)
}
}
Loading