Velox:Miguel de Icaza 将 Tauri 移植到 Swift 的项目
Velox: A Port of Tauri to Swift by Miguel de Icaza

原始链接: https://github.com/velox-apps/velox

## Velox:为 Swift 开发者提供的 Tauri Velox 将 Tauri 的桌面应用构建能力带给 Swift 开发者,允许使用 HTML 前端和 Swift 后端创建桌面应用程序。它专为熟悉 Swift 但觉得 Rust 繁琐的开发者设计。 主要特性包括一个 Swift 包插件,可自动构建必要的 Rust 组件,一个 CLI 工具 (`velox`) 用于项目创建 (`velox init`) 和开发 (`velox dev` 支持热重载),以及生产构建 (`velox build`)。Velox 支持直接提供静态资源,也支持代理到现代 Web 开发服务器(如 Vite),以实现热模块替换等功能。 配置通过 `velox.json` 文件管理,类似于 Tauri 的 `tauri.conf.json`,并支持平台特定的覆盖。Swift 与 webview 之间的 IPC 通过自定义协议处理,可以使用便捷的 `@VeloxCommand` 宏或更手动化的 DSL 实现。Velox 还提供强大的窗口和 webview 控制 API。 该项目利用 Rust FFI 层,提供构建针对已发布 crates 或本地修补版本的选项,用于开发和测试。提供了示例来演示各种功能。

黑客新闻 新的 | 过去的 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 Velox:Tauri 的 Swift 移植,作者 Miguel de Icaza (github.com/velox-apps) 8 分,wahnfrieden 发表于 2 小时前 | 隐藏 | 过去的 | 收藏 | 讨论 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

Bringing Tauri to Swift developers.

I love Rust as much as the next security paranoid person, but I do not love it to write apps, it gets in my way, and I am too old to develop an appreciation for poetry or Rust. So this is a port of Tauri to Swift so I can both build desktop apps using HTML with Swift backends and fill me with joy.

Discord: invite

Documentation and Tutorials

The Swift package declares a build-tool plugin that automatically compiles the Rust FFI crate whenever VeloxRuntimeWryFFI is built. Simply run:

The plugin will invoke cargo build with the correct configuration (debug or release) and emit libraries into runtime-wry-ffi/target. If you prefer to build the Rust crate manually you can still run cargo build or cargo build --release inside runtime-wry-ffi/.

By default the plugin runs Cargo in offline mode to avoid sandboxed network access and ensures velox/.cargo/config.toml patch overrides are picked up. If you need Cargo to fetch from the network, set VELOX_CARGO_ONLINE=1 when building.

Use the create-velox-app command to create a new blank project, starting from one of the built-in templates.

Velox includes a CLI tool for development workflow, similar to Tauri's CLI.

swift build --product velox

The CLI binary will be available at .build/debug/velox.

velox init - Initialize a New Project

Initialize Velox in a new or existing directory:

# Initialize with defaults (derives name from directory)
velox init

# Specify product name and identifier
velox init --name "MyApp" --identifier "com.example.myapp"

# Overwrite existing files
velox init --force

This creates:

your-project/
├── Package.swift       # Swift package manifest
├── Sources/
│   └── YourApp/
│       └── main.swift  # App entry point with IPC handlers
├── assets/
│   └── index.html      # Frontend UI template
└── velox.json          # Velox configuration

velox dev - Development Mode

Run the app in development mode with hot reloading:

# Run with auto-detected target
velox dev

# Specify a target explicitly
velox dev MyApp

# Run in release mode
velox dev --release

# Disable file watching
velox dev --no-watch

# Override dev server port
velox dev --port 3000

Features:

  • Executes beforeDevCommand from velox.json (e.g., npm run dev)
  • Waits for dev server at devUrl if configured
  • Builds and runs the Swift app with VELOX_DEV_URL set
  • Dev server proxy: When devUrl is set, the app:// protocol proxies requests to your dev server, enabling HMR from tools like Vite
  • Watches for Swift file changes and rebuilds automatically
  • Smart reload: Frontend-only changes trigger a quick restart without rebuild
  • Graceful shutdown with Ctrl+C

velox build - Production Build

Build the app for production:

# Release build (default)
velox build

# Debug build
velox build --debug

# Create macOS app bundle (.app)
velox build --bundle

# Specify target
velox build MyApp

# Debug build with app bundle
velox build --debug --bundle

The --bundle flag creates a complete macOS app bundle:

.build/release/MyApp.app/
├── Contents/
│   ├── Info.plist      # Generated from velox.json
│   ├── MacOS/
│   │   └── MyApp       # Executable
│   └── Resources/
│       └── assets/     # Frontend files (from frontendDist)

You can also set bundle.active: true in velox.json to enable bundling without the CLI flag. For full macOS bundling details (signing, DMG, notarization), see Sources/VeloxRuntime/VeloxRuntime.docc/Articles/Bundling.md.

The CLI uses settings from velox.json:

{
  "productName": "MyApp",
  "version": "1.0.0",
  "identifier": "com.example.myapp",
  "build": {
    "devUrl": "http://localhost:5173",
    "beforeDevCommand": "npm run dev",
    "beforeBuildCommand": "npm run build",
    "beforeBundleCommand": "npm run prepare-bundle",
    "frontendDist": "dist",
    "env": {
      "API_URL": "https://api.example.com",
      "DEBUG": "true"
    }
  },
  "bundle": {
    "active": true,
    "targets": ["app", "dmg"],
    "icon": "icons/AppIcon.icns",
    "resources": ["extra-assets"],
    "macos": {
      "minimumSystemVersion": "13.0",
      "infoPlist": "Info.plist",
      "entitlements": "entitlements.plist",
      "signingIdentity": "Developer ID Application: Example (ABCDE12345)",
      "hardenedRuntime": true,
      "dmg": {
        "enabled": true,
        "name": "MyApp",
        "volumeName": "MyApp"
      },
      "notarization": {
        "keychainProfile": "AC_NOTARY",
        "wait": true,
        "staple": true
      }
    }
  }
}
Field Description
devUrl Dev server URL; enables proxy mode (see below)
beforeDevCommand Command to run before velox dev (e.g., start Vite)
beforeBuildCommand Command to run before velox build (e.g., build frontend)
beforeBundleCommand Command to run before creating app bundle
frontendDist Directory containing frontend assets for bundling
env Environment variables to inject into build and dev processes

Velox supports environment variable injection from multiple sources:

  1. .env - Base environment file
  2. .env.development or .env.production - Mode-specific overrides
  3. .env.local - Local overrides (gitignored)
  4. velox.json build.env - Configuration-defined variables

Priority (highest to lowest): system env > velox.json > .env.local > .env.[mode] > .env

Example .env file:

API_URL=https://api.example.com
DEBUG=true
# Comments are supported
MULTILINE="line1\nline2"

Frontend Development Modes

Velox offers two approaches for serving frontend assets during development. Choose based on your project's complexity and tooling preferences.

Option 1: Local Asset Serving (Simple Projects)

When to use: Static HTML/CSS/JS without a build step, simple projects, or when you want the fastest possible reload cycle.

{
  "build": {
    "frontendDist": "assets"
  }
}

How it works:

  • velox dev serves files directly from the frontendDist directory (e.g., assets/)
  • File watcher monitors both Swift sources AND frontend files
  • When you edit index.html, styles.css, or app.js, the app restarts instantly (no rebuild)
  • Swift file changes trigger a full rebuild

Pros:

  • Zero configuration - just put HTML files in assets/
  • No Node.js or npm required
  • Fastest restart for simple frontend changes
  • Great for prototyping and learning

Cons:

  • No transpilation (TypeScript, JSX, etc.)
  • No module bundling
  • No Hot Module Replacement (page fully reloads)
  • Manual browser refresh via app restart

Option 2: Dev Server Proxy (Modern Web Tooling)

When to use: Projects using Vite, webpack, or other modern frontend toolchains that provide HMR.

{
  "build": {
    "devUrl": "http://localhost:5173",
    "beforeDevCommand": "npm run dev",
    "frontendDist": "dist"
  }
}

How it works:

  1. beforeDevCommand starts your frontend dev server (e.g., Vite)
  2. Velox waits for devUrl to respond before launching the app
  3. The VELOX_DEV_URL environment variable is passed to your Swift app
  4. The app:// protocol proxies all requests to the dev server
  5. File watcher only monitors Swift sources (frontend HMR is handled by Vite)

Pros:

  • Hot Module Replacement (HMR) - instant updates without page reload
  • Full modern toolchain support (TypeScript, React, Vue, Tailwind, etc.)
  • Source maps for debugging
  • Consistent app:// protocol between dev and production
  • CORS-free development

Cons:

  • Requires Node.js and npm
  • More configuration
  • Slightly slower initial startup (waiting for dev server)
Feature Local Assets Dev Server Proxy
Setup complexity Minimal Requires npm project
Frontend tooling None (vanilla JS) Full (Vite, webpack, etc.)
TypeScript/JSX Not supported Fully supported
Hot Module Replacement No (app restart) Yes (instant)
Page reload on change Full restart Partial/none (HMR)
Swift change handling Rebuild + restart Rebuild + restart
Production build Copy files Run build command

To switch from proxy mode to local asset serving, simply remove or comment out devUrl:

{
  "build": {
    // "devUrl": "http://localhost:5173",  // Commented out = local mode
    // "beforeDevCommand": "npm run dev",   // Not needed without devUrl
    "frontendDist": "assets"
  }
}

The same frontendDist directory is used for both development (local mode) and production builds.

The repository includes several example applications demonstrating Velox capabilities. Examples are located in the Examples/ directory.

Build and run any example using Swift Package Manager:

# Build all examples
swift build

# Run a specific example
swift run HelloWorld
swift run HelloWorld2
swift run MultiWindow
swift run State
swift run Splashscreen
swift run Streaming
swift run RunReturn
let loop = VeloxRuntimeWry.EventLoop()
let proxy = loop?.makeProxy()
let window = loop?.makeWindow(configuration: .init(width: 800, height: 600, title: "Velox"))
let webview = window?.makeWebview(configuration: .init(url: "https://tauri.app"))

loop?.pump { event in
  switch event {
  case .loopDestroyed, .userExit:
    return .exit
  default:
    return .poll
  }
}

proxy?.requestExit()

This demonstrates the bridging between Swift and the underlying Tao/Wry event loop, window, and webview primitives exposed by the Rust shim. Event callbacks now deliver structured metadata (via JSON) which the Swift layer normalises into strongly-typed VeloxRuntimeWry.Event values.

VeloxRuntimeWry.Event exposes rich keyboard, pointer, focus, DPI, and file-drop information so Swift applications can respond to Tao/Wry input without having to touch the underlying JSON payloads.

Window & Webview Controls

The Swift API now includes helpers to:

  • configure window titles, fullscreen state, sizing constraints, z-order, and visibility;
  • request redraws or reposition windows without touching tao directly;
  • drive Wry webviews via navigation, reload, JavaScript evaluation, zoom control, visibility toggles, and browsing-data clearing.
  • toggle advanced window capabilities including decorations, always-on-bottom/workspace visibility, content protection, focus/focusable state, cursor controls, drag gestures, and attention requests.

Velox now ships a nascent VeloxRuntime module that defines the Swift-first protocols mirroring Tauri's runtime traits. VeloxRuntimeWry.Runtime remains a stub while the native implementation is completed; the event-loop based APIs remain the primary entry point until the dedicated Swift runtime is feature-complete.

The repository includes several example applications demonstrating Velox capabilities. Examples are located in the Examples/ directory.

Build and run any example using Swift Package Manager:

# Build all examples
swift build

# Run a specific example
swift run HelloWorld
swift run HelloWorld2
swift run MultiWindow
swift run State
swift run Splashscreen
swift run Streaming
swift run RunReturn
swift run Commands
swift run CommandsManual
swift run CommandsManualRegistry
swift run Resources
swift run WindowControls
swift run MultiWebView
swift run DynamicHTML
swift run Events
swift run Tray

Velox supports two approaches for loading web content, mirroring Tauri's flexibility:

1. Self-Contained (Inline HTML)

The simplest approach embeds HTML directly in Swift code. This is ideal for simple UIs or when you want a single-binary deployment with no external dependencies.

Example: HelloWorld

let html = """
<!doctype html>
<html>
  <body>
    <h1>Hello from Velox!</h1>
  </body>
</html>
"""

let appProtocol = VeloxRuntimeWry.CustomProtocol(scheme: "app") { _ in
  VeloxRuntimeWry.CustomProtocol.Response(
    status: 200,
    headers: ["Content-Type": "text/html"],
    body: Data(html.utf8)
  )
}

Pros:

  • Single binary, no external files needed
  • Simple deployment
  • Good for small UIs

Cons:

  • HTML/CSS/JS changes require recompilation
  • Less suitable for complex UIs
  • No separation of concerns

2. Bundled Assets (External Files)

For larger applications, keep HTML, CSS, and JavaScript as separate files loaded at runtime. This mirrors Tauri's asset bundling approach.

Example: HelloWorld2

Examples/HelloWorld2/
├── main.swift          # Swift entry point with AssetBundle
└── assets/
    ├── index.html      # HTML markup
    ├── styles.css      # Stylesheet
    └── app.js          # JavaScript

The AssetBundle struct discovers and serves files from the assets directory:

struct AssetBundle {
  let basePath: String

  func loadAsset(path: String) -> (data: Data, mimeType: String)? {
    // Load file and detect MIME type
  }
}

let appProtocol = VeloxRuntimeWry.CustomProtocol(scheme: "app") { request in
  guard let url = URL(string: request.url),
        let asset = assets.loadAsset(path: url.path) else {
    return notFoundResponse()
  }
  return VeloxRuntimeWry.CustomProtocol.Response(
    status: 200,
    headers: ["Content-Type": asset.mimeType],
    body: asset.data
  )
}

Pros:

  • Separation of concerns (Swift logic vs web UI)
  • Edit HTML/CSS/JS without recompiling Swift
  • Better for complex UIs and larger teams
  • Familiar web development workflow

Cons:

  • Requires bundling assets with the binary
  • Slightly more complex deployment
Example Description Asset Approach
HelloWorld Basic window with inline HTML and IPC Self-contained
HelloWorld2 Same functionality with external assets Bundled assets
MultiWindow Multiple windows running simultaneously Self-contained
State Shared state across IPC calls Self-contained
Splashscreen Splash window before main window Self-contained
Streaming Server-sent events from Swift to webview Self-contained
RunReturn Manual event loop control Self-contained
Commands @VeloxCommand macro for cleanest command definitions Bundled assets
CommandsManual Manual IPC routing with switch statement Bundled assets
CommandsManualRegistry Type-safe command DSL with automatic JSON decoding Bundled assets
Resources Resource bundling and path resolution Bundled assets
WindowControls Comprehensive window/webview API demonstration Self-contained
MultiWebView Multiple child webviews: local app + GitHub, tauri.app, Twitter Mixed
DynamicHTML Swift-rendered dynamic HTML with counter, todos, and themes Self-contained
Events Event system: backend-to-frontend and frontend-to-backend events Self-contained
Tray System tray icon with context menu (macOS) Self-contained

Configuration (velox.json)

Velox supports a configuration file (velox.json) similar to Tauri's tauri.conf.json. This allows declarative app configuration:

{
  "$schema": "https://velox.dev/schema/velox.schema.json",
  "productName": "MyApp",
  "version": "1.0.0",
  "identifier": "com.example.myapp",
  "app": {
    "windows": [
      {
        "label": "main",
        "title": "My Application",
        "width": 800,
        "height": 600,
        "url": "app://localhost/",
        "create": true,
        "visible": true,
        "resizable": true,
        "devtools": true,
        "customProtocols": ["app", "ipc"]
      }
    ],
    "macOS": {
      "activationPolicy": "regular"
    }
  },
  "build": {
    "frontendDist": "assets"
  }
}

Devtools: devtools defaults to true in debug builds and false in release. On macOS, enabling devtools uses private WebKit APIs, so avoid setting it to true for release builds.

Use VeloxAppBuilder to create your app from configuration:

import VeloxRuntime
import VeloxRuntimeWry

let config = try VeloxConfig.load(from: URL(fileURLWithPath: "path/to/app"))
let eventLoop = VeloxRuntimeWry.EventLoop()!

let app = VeloxAppBuilder(config: config)
  .registerProtocol("app") { request in
    // Serve assets
  }
  .registerProtocol("ipc") { request in
    // Handle IPC commands
  }
  .build(eventLoop: eventLoop)

Platform-Specific Overrides: Create velox.macos.json, velox.ios.json, etc. to override settings per platform using RFC 7396 JSON Merge Patch.

Both approaches use custom protocols for IPC between Swift and the webview. Velox injects a window.Velox.invoke helper that supports both immediate and deferred command responses:

// JavaScript: invoke a Swift command (preferred)
const message = await window.Velox.invoke('greet', { name: 'World' });

If you need a custom helper, you can still use fetch for immediate responses:

async function invoke(command, args = {}) {
  const response = await fetch(`ipc://localhost/${command}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(args)
  });
  return (await response.json()).result;
}

To return after the IPC handler completes (for modal dialogs or long tasks), defer the response and resolve it later. window.Velox.invoke will await the final result automatically:

struct DelayedEchoArgs: Codable, Sendable { let message: String; let delayMs: Int? }

commands.register("delayed_echo", args: DelayedEchoArgs.self, returning: DeferredCommandResponse.self) { args, ctx in
  let deferred = try ctx.deferResponse()
  let delay = max(0, args.delayMs ?? 500)
  DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(delay)) {
    deferred.responder.resolve(args.message)
  }
  return deferred.pending
}
const reply = await window.Velox.invoke('delayed_echo', { message: 'Hello later', delayMs: 800 });

@VeloxCommand Macro (Recommended)

Velox provides the @VeloxCommand macro for the cleanest command definitions, similar to Tauri's #[tauri::command]:

import VeloxMacros
import VeloxRuntimeWry

// Define response types
struct GreetResponse: Codable, Sendable {
  let message: String
}

// Commands must be in a container (enum/struct) for macro expansion
enum Commands {
  @VeloxCommand
  static func greet(name: String) -> GreetResponse {
    GreetResponse(message: "Hello, \(name)!")
  }

  @VeloxCommand
  static func divide(numerator: Double, denominator: Double) throws -> MathResponse {
    guard denominator != 0 else {
      throw CommandError(code: "DivisionByZero", message: "Cannot divide by zero")
    }
    return MathResponse(result: numerator / denominator)
  }

  // Access state via CommandContext parameter
  @VeloxCommand
  static func increment(context: CommandContext) -> CounterResponse {
    let state: AppState = context.requireState()
    return CounterResponse(value: state.increment())
  }

  // Custom command name
  @VeloxCommand("get_counter")
  static func getCounter(context: CommandContext) -> CounterResponse {
    let state: AppState = context.requireState()
    return CounterResponse(value: state.counter)
  }
}

// Register macro-generated commands
let registry = commands {
  Commands.greetCommand      // Generated by @VeloxCommand
  Commands.divideCommand
  Commands.incrementCommand
  Commands.getCounterCommand
}

See Examples/Commands for a complete demonstration of the @VeloxCommand macro.

For cases where you prefer explicit control, use the command DSL directly:

import VeloxRuntime
import VeloxRuntimeWry

// Define typed arguments and responses
struct GreetArgs: Codable, Sendable {
  let name: String
}

struct GreetResponse: Codable, Sendable {
  let message: String
}

// Register commands using the DSL
let registry = commands {
  command("greet", args: GreetArgs.self, returning: GreetResponse.self) { args, _ in
    GreetResponse(message: "Hello, \(args.name)!")
  }

  command("divide", args: DivideArgs.self, returning: MathResponse.self) { args, _ in
    guard args.denominator != 0 else {
      throw CommandError(code: "DivisionByZero", message: "Cannot divide by zero")
    }
    return MathResponse(result: args.numerator / args.denominator)
  }

  command("increment", returning: CounterResponse.self) { context in
    let state: AppState = context.requireState()
    return CounterResponse(value: state.increment())
  }
}

// Create IPC handler and register protocol
let stateContainer = StateContainer().manage(AppState())
let ipcHandler = createCommandHandler(registry: registry, stateContainer: stateContainer)
let ipcProtocol = VeloxRuntimeWry.CustomProtocol(scheme: "ipc", handler: ipcHandler)

See Examples/CommandsManualRegistry for a complete demonstration of the type-safe command DSL.

For simpler cases, you can handle IPC requests manually:

// Swift: handle IPC requests manually
let ipcProtocol = VeloxRuntimeWry.CustomProtocol(scheme: "ipc") { request in
  let command = URL(string: request.url)?.path.trimmingCharacters(in: .init(charactersIn: "/"))

  switch command {
  case "greet":
    let name = parseArgs(request.body)["name"] as? String ?? "World"
    return jsonResponse(["result": "Hello \(name)!"])
  default:
    return errorResponse("Unknown command")
  }
}

The Small Print set in H2

  • Package.swift: Swift Package definition exposing the VeloxRuntimeWry library target.
  • Sources/VeloxRuntimeWry: Swift surface area that mirrors the Tauri runtime concepts with Velox naming.
  • Sources/VeloxRuntimeWryFFI: Lightweight C target that bridges into the Rust static library.
  • runtime-wry-ffi: Rust crate producing a velox_runtime_wry_ffi static/dynamic library that re-exports selected pieces of tao, wry, and tauri-runtime-wry.

Velox supports two build modes for the Rust FFI crate:

Standard Build (crates.io)

Uses published versions of tao and wry from crates.io. This is the default for clean checkouts and CI:

# Remove local patches if present
rm -f .cargo/config.toml
swift build

Uses locally patched tao and wry with additional testing features. Requires sibling checkouts of these repositories with the velox-testing feature added to tao's default features.

# Ensure .cargo/config.toml exists with patches (at package root, not runtime-wry-ffi)
# Then build with local-dev feature:
VELOX_LOCAL_DEV=1 swift build

The .cargo/config.toml (in the package root) patches crates.io dependencies with local paths:

[patch.crates-io]
tao = { path = "../tao" }
wry = { path = "../wry" }

Requirements for Local Dev:

  • Local tao version must match crates.io (currently 0.34.5)
  • Local tao must have velox-testing in its default features in Cargo.toml
  • Local wry version must match crates.io (currently 0.53.5)
联系我们 contact @ memedata.com