iOS
Serve a WKWebView (iOS and macOS) from a .wvb bundle using the Swift bindings generated from the Rust core.
On iOS (and macOS), Webview Bundle ships as a Swift library generated from the Rust core with
UniFFI. You build a BundleSource
and serve files into a WKWebView through a BundleUrlHandler wired to a WKURLSchemeHandler.
A working test app lives in
packages/ffi/apple, and the
integration tests in
TestRunner.swift exercise the full Swift
API.
The Apple bindings are built from this repository (no published release artifact yet). The steps
below build the xcframework and add the local Swift package.
What the library contains
WebViewBundleFFI.xcframework— the native static libraries forios-arm64,ios-arm64_x86_64-simulator, andmacos-arm64_x86_64, plus C headers.WebViewBundleLibrary— the generated Swift API (WebViewBundleLibrary.swift) that wraps it.
The Swift package (packages/ffi/apple/Package.swift) ties them together and links
SystemConfiguration, Security, and CoreFoundation. Minimum targets are iOS 14 / macOS 12.
1. Build and add the package
From the repo, build the Apple bindings (this compiles the Rust core for each Apple platform and generates the Swift bindings + xcframework):
# from packages/ffi
yarn build-ffi-apple # → node ./cli/main.ts build apple --profile=releaseThen add packages/ffi/apple as a local Swift package dependency in Xcode (File → Add Package
Dependencies → Add Local…) and link the WebViewBundleLibrary product to your app target. In a
Package.swift-based project:
.package(path: "path/to/webview-bundle/packages/ffi/apple")
// …then add "WebViewBundleLibrary" to your target's dependencies.2. Ship your bundles as resources
Pack your web build and add the .wvb plus its manifest.json to your app bundle as resources,
following the source layout:
npx wvb pack ./dist --outfile assets/bundles/builtin/app/app_1.0.0.wvbassets/bundles/builtin/
├── app/
│ └── app_1.0.0.wvb
└── manifest.jsonAt runtime, resolve the builtin directory from the app bundle, and use a writable directory (e.g.
under Application Support or a temp dir) for downloaded updates:
let builtinDir = Bundle.main.resourceURL!
.appendingPathComponent("assets/bundles/builtin")
let remoteDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("bundles/remote")
try? FileManager.default.createDirectory(at: remoteDir, withIntermediateDirectories: true)3. Build a BundleSource
import WebViewBundleLibrary
let source = BundleSource(config: BundleSourceConfig(
builtinDir: builtinDir.path,
remoteDir: remoteDir.path,
builtinManifestFilepath: nil, // defaults to manifest.json in the dir
remoteManifestFilepath: nil
))
// Query what's available:
let bundle = try await source.fetch(bundleName: "app") // load into memory
let html = try bundle.getData(path: "/index.html") // Data? — bytes or nil4. Serve a WKWebView from the bundle
WKWebView lets you handle a custom URL scheme with a WKURLSchemeHandler. Register a scheme
(it cannot be a built-in one like https), then forward each request to a BundleUrlHandler:
import WebKit
import WebViewBundleLibrary
final class BundleSchemeHandler: NSObject, WKURLSchemeHandler {
private let handler: BundleUrlHandler
init(source: BundleSource) { self.handler = BundleUrlHandler(source: source) }
func webView(_ webView: WKWebView, start task: WKURLSchemeTask) {
let request = task.request
let method: HttpMethod = (request.httpMethod == "HEAD") ? .head : .get
let headers = request.allHTTPHeaderFields
Task {
do {
let resp = try await handler.handle(
method: method,
uri: request.url!.absoluteString,
headers: headers
)
let contentType = resp.headers["content-type"] ?? "application/octet-stream"
let response = HTTPURLResponse(
url: request.url!,
statusCode: Int(resp.status),
httpVersion: "HTTP/1.1",
headerFields: resp.headers
)!
task.didReceive(response)
task.didReceive(resp.body)
task.didFinish()
} catch {
task.didFailWithError(error)
}
}
}
func webView(_ webView: WKWebView, stop task: WKURLSchemeTask) {}
}
// Register the scheme and load your bundle:
let config = WKWebViewConfiguration()
config.setURLSchemeHandler(BundleSchemeHandler(source: source), forURLScheme: "app")
let webView = WKWebView(frame: .zero, configuration: config)
webView.load(URLRequest(url: URL(string: "app://app.wvb/index.html")!))The handler resolves the bundle name from the first host label (app.wvb → bundle app) and treats
a trailing slash or extension-less path as index.html. It returns 404 for unknown paths and
supports GET/HEAD. handle(...) is async, so call it from a Task.
Development against a dev server
For hot reload during development, use LocalUrlHandler instead, mapping a host to your dev server:
let local = LocalUrlHandler(hosts: ["app.wvb": "http://localhost:5173"])5. Build a bundle in-process (optional)
The same API can create bundles, which is handy for tests or tooling:
let builder = BundleBuilder(version: .v1)
_ = try builder.insertEntry(path: "/index.html", data: Data("<!DOCTYPE html>".utf8),
contentType: "text/html", headers: nil)
let bundle = try builder.build(options: nil)
let bytes = try writeBundleToBytes(bundle: bundle) // serialize to Data
let loaded = try readBundleFromBytes(data: bytes) // round-trip
_ = try await writeBundle(bundle: bundle, filepath: path) // or write to a fileOver-the-air updates
BundleSource exposes writeRemoteBundle(...) and updateVersion(...) to install a downloaded
bundle and flip the current version. The download/verify orchestration mirrors the desktop
updater; on iOS you typically fetch the bundle bytes yourself and call
writeRemoteBundle to install them, then reload the WKWebView.