diff --git a/Demo/App.swiftpm/Sources/App.swift b/Demo/App.swiftpm/Sources/App.swift index 34ec108..b999ac3 100644 --- a/Demo/App.swiftpm/Sources/App.swift +++ b/Demo/App.swiftpm/Sources/App.swift @@ -1,20 +1,32 @@ #if canImport(AndroidSwiftUI) import AndroidSwiftUI -import AndroidKit #else import SwiftUI #endif // The gallery screens are restored alongside the view types they exercise as the -// Compose-backed renderer is built up; until then this is a bare launch point. +// Compose-backed renderer is built up. + +struct ContentView: View { + + @State private var count = 0 + + var body: some View { + VStack(spacing: 12) { + Text("Count: \(count)") + Button("Increment") { count += 1 } + Toggle("Feature flag", isOn: .constant(true)) + } + } +} #if canImport(AndroidSwiftUI) /// App launch point, called from `MainActivity`. @_silgen_name("AndroidSwiftUIMain") func AndroidSwiftUIMain() { - let log = try! JavaClass() - _ = log.v("DemoApp", "Starting SwiftUI App") + AndroidSwiftUILog("Starting SwiftUI App") + AndroidSwiftUIApp.run(ContentView()) } #else @@ -24,7 +36,7 @@ struct DemoApp: App { var body: some Scene { WindowGroup { - Text("Demo") + ContentView() } } } diff --git a/Demo/app/build.gradle.kts b/Demo/app/build.gradle.kts index 8707008..50bbed5 100644 --- a/Demo/app/build.gradle.kts +++ b/Demo/app/build.gradle.kts @@ -67,6 +67,7 @@ android { } dependencies { + implementation(project(":swiftui")) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) diff --git a/Demo/desktop/build.gradle.kts b/Demo/desktop/build.gradle.kts index 59656cd..8c9d86c 100644 --- a/Demo/desktop/build.gradle.kts +++ b/Demo/desktop/build.gradle.kts @@ -14,11 +14,24 @@ kotlin { implementation(compose.desktop.currentOs) implementation(compose.material3) } + jvmTest.dependencies { + implementation(kotlin("test")) + implementation(compose.desktop.uiTestJUnit4) + } } } +// Absolute path to the Swift-built dylib; the loader loads its sibling +// libSwiftJava.dylib first (JNI_OnLoad lives there). +val swiftLibrary = rootDir.resolve("../.build/arm64-apple-macosx/debug/libSwiftUIDesktopDemo.dylib").canonicalPath + +tasks.withType().configureEach { + systemProperty("swiftui.library", swiftLibrary) +} + compose.desktop { application { mainClass = "com.pureswift.swiftui.desktop.MainKt" + jvmArgs += "-Dswiftui.library=$swiftLibrary" } } diff --git a/Demo/desktop/src/jvmMain/kotlin/com/pureswift/swiftui/desktop/Main.kt b/Demo/desktop/src/jvmMain/kotlin/com/pureswift/swiftui/desktop/Main.kt index 00c04dc..ad550c1 100644 --- a/Demo/desktop/src/jvmMain/kotlin/com/pureswift/swiftui/desktop/Main.kt +++ b/Demo/desktop/src/jvmMain/kotlin/com/pureswift/swiftui/desktop/Main.kt @@ -64,16 +64,35 @@ object Fixtures { ) } -fun main() = application { - Window(onCloseRequest = ::exitApplication, title = "AndroidSwiftUI desktop rig") { - MaterialTheme { - Surface { - RigContent() +fun main() { + val live = SwiftRuntime.load() + application { + Window(onCloseRequest = ::exitApplication, title = "AndroidSwiftUI desktop rig") { + MaterialTheme { + Surface { + if (live) { + LiveContent() + } else { + RigContent() + } + } } } } } +/// Live mode: the Swift dylib evaluates its root view and drives the store. +@Composable +private fun LiveContent() { + val store = remember { + TreeStore().also { + com.pureswift.swiftui.SwiftBridge.sink = com.pureswift.swiftui.SwiftCallbackSink() + SwiftRuntime().start(it) + } + } + store.root?.let { Render(it) } +} + @Composable private fun RigContent() { val store = remember { TreeStore() } diff --git a/Demo/desktop/src/jvmMain/kotlin/com/pureswift/swiftui/desktop/SwiftRuntime.kt b/Demo/desktop/src/jvmMain/kotlin/com/pureswift/swiftui/desktop/SwiftRuntime.kt new file mode 100644 index 0000000..9b3904b --- /dev/null +++ b/Demo/desktop/src/jvmMain/kotlin/com/pureswift/swiftui/desktop/SwiftRuntime.kt @@ -0,0 +1,29 @@ +package com.pureswift.swiftui.desktop + +import com.pureswift.swiftui.TreeStore + +// Entry point into the Swift dylib on the desktop JVM. Loading the library +// fires swift-java's JNI_OnLoad; `start` hands Swift the tree store, and the +// Swift side evaluates its root view and drives the store from then on. +class SwiftRuntime { + + external fun start(store: TreeStore) + + companion object { + + /// Loads the Swift libraries from `-Dswiftui.library=`. + /// swift-java's `JNI_OnLoad` lives in libSwiftJava, and the JVM only + /// fires it for explicitly loaded libraries — so the runtime library + /// (sibling `libSwiftJava.dylib`) loads first, then the app library. + /// Returns false (rig falls back to fixtures) when not configured. + fun load(): Boolean { + val path = System.getProperty("swiftui.library") ?: return false + val runtime = java.io.File(java.io.File(path).parentFile, "libSwiftJava.dylib") + if (runtime.exists()) { + System.load(runtime.absolutePath) + } + System.load(path) + return true + } + } +} diff --git a/Demo/desktop/src/jvmTest/kotlin/com/pureswift/swiftui/desktop/BridgeTests.kt b/Demo/desktop/src/jvmTest/kotlin/com/pureswift/swiftui/desktop/BridgeTests.kt new file mode 100644 index 0000000..372a567 --- /dev/null +++ b/Demo/desktop/src/jvmTest/kotlin/com/pureswift/swiftui/desktop/BridgeTests.kt @@ -0,0 +1,47 @@ +package com.pureswift.swiftui.desktop + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import com.pureswift.swiftui.Render +import com.pureswift.swiftui.SwiftBridge +import com.pureswift.swiftui.SwiftCallbackSink +import com.pureswift.swiftui.TreeStore +import org.junit.Assume.assumeTrue +import org.junit.Rule +import org.junit.Test + +/// The end-to-end bridge test: native Swift evaluates the view tree, JNI +/// materializes it into Kotlin nodes, Compose renders it, a click dispatches +/// back into Swift, Swift re-evaluates, and the UI shows the new state. +/// Runs headless on the host JVM — no emulator, no window. +class BridgeTests { + + @get:Rule + val compose = createComposeRule() + + @Test + fun counterRoundTripAcrossTheBridge() { + assumeTrue("swiftui.library not set — bridge test skipped", SwiftRuntime.load()) + + val store = TreeStore() + SwiftBridge.sink = SwiftCallbackSink() + SwiftRuntime().start(store) + + compose.setContent { + store.root?.let { Render(it) } + } + + // initial tree evaluated in Swift + compose.onNodeWithText("Count: 0").assertIsDisplayed() + + // Compose click → JNI → Swift @State write → re-evaluate → new tree + compose.onNodeWithText("Increment").performClick() + compose.onNodeWithText("Count: 1").assertIsDisplayed() + + // and again, proving the callback registry survives re-registration + compose.onNodeWithText("Increment").performClick() + compose.onNodeWithText("Count: 2").assertIsDisplayed() + } +} diff --git a/Demo/swiftui/build.gradle.kts b/Demo/swiftui/build.gradle.kts index f05b3e6..8e5fe6a 100644 --- a/Demo/swiftui/build.gradle.kts +++ b/Demo/swiftui/build.gradle.kts @@ -28,6 +28,15 @@ kotlin { implementation(compose.material3) implementation(libs.kotlinx.serialization.json) } + // `external fun` is JVM-only; both targets are JVM, so the bridge's + // Swift-implemented classes live in a source set they share. + val jvmShared by creating { + dependsOn(commonMain.get()) + } + androidMain.get().dependsOn(jvmShared) + val desktopMain by getting { + dependsOn(jvmShared) + } val desktopTest by getting { dependencies { implementation(kotlin("test")) diff --git a/Demo/swiftui/src/androidMain/kotlin/com/pureswift/swiftui/SwiftUIHostView.kt b/Demo/swiftui/src/androidMain/kotlin/com/pureswift/swiftui/SwiftUIHostView.kt new file mode 100644 index 0000000..c83588a --- /dev/null +++ b/Demo/swiftui/src/androidMain/kotlin/com/pureswift/swiftui/SwiftUIHostView.kt @@ -0,0 +1,32 @@ +package com.pureswift.swiftui + +import android.content.Context +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.ui.platform.ComposeView + +// The Android host: one Compose island rendering the whole Swift-evaluated +// tree. Swift constructs this, hands its store to the bridge runtime, and +// installs it as the activity's content view. +class SwiftUIHostView(context: Context) : FrameLayout(context) { + + val store = TreeStore() + + init { + SwiftBridge.sink = SwiftCallbackSink() + val composeView = ComposeView(context) + composeView.setContent { + MaterialTheme { + Surface { + store.root?.let { Render(it) } + } + } + } + addView( + composeView, + ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + ) + } +} diff --git a/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/ViewNode.kt b/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/ViewNode.kt index 9edd2e1..2b3cbdb 100644 --- a/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/ViewNode.kt +++ b/Demo/swiftui/src/commonMain/kotlin/com/pureswift/swiftui/ViewNode.kt @@ -2,10 +2,12 @@ package com.pureswift.swiftui import androidx.compose.runtime.Immutable import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.booleanOrNull import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.longOrNull /// One entry in a node's ordered modifier chain. Order is significant: the @@ -39,6 +41,33 @@ data class ViewNode( val itemProviderId: Long? = null, ) { + /// Bridge constructor: the Swift materializer builds nodes through this, + /// crossing JNI with flat arrays (one call per node, arrays as single + /// arguments). Prop and modifier-arg values arrive as JSON literals + /// ("\"text\"", "42", "true"), keeping the typed JsonObject model without + /// per-field JNI calls. Negative count/provider mean "absent". + constructor( + type: String, + id: String, + propKeys: Array, + propValues: Array, + modifierKinds: Array, + modifierArgs: Array, + children: Array, + count: Int, + itemProviderId: Long, + ) : this( + type = type, + id = id, + props = JsonObject(propKeys.indices.associate { propKeys[it] to Json.parseToJsonElement(propValues[it]) }), + modifiers = modifierKinds.indices.map { + ModifierNode(modifierKinds[it], Json.parseToJsonElement(modifierArgs[it]).jsonObject) + }, + children = children.toList(), + count = if (count >= 0) count else null, + itemProviderId = if (itemProviderId >= 0) itemProviderId else null, + ) + // Typed prop accessors used by the interpreter. fun string(key: String): String? = (props[key] as? JsonPrimitive)?.content diff --git a/Demo/swiftui/src/jvmShared/kotlin/com/pureswift/swiftui/SwiftCallbackSink.kt b/Demo/swiftui/src/jvmShared/kotlin/com/pureswift/swiftui/SwiftCallbackSink.kt new file mode 100644 index 0000000..5bad982 --- /dev/null +++ b/Demo/swiftui/src/jvmShared/kotlin/com/pureswift/swiftui/SwiftCallbackSink.kt @@ -0,0 +1,19 @@ +package com.pureswift.swiftui + +// The entire Kotlin→Swift bridge surface: five externals dispatching event +// callback ids into the Swift registry. The JNI symbol for each derives from +// the SWIFT @JavaImplementation signature — these declarations and the Swift +// counterparts in SwiftCallbackSink.swift must stay exactly in sync, and this +// class must never grow per-view methods. +class SwiftCallbackSink : CallbackSink { + + external override fun invokeVoid(id: Long) + + external override fun invokeBool(id: Long, value: Boolean) + + external override fun invokeDouble(id: Long, value: Double) + + external override fun invokeInt(id: Long, value: Int) + + external override fun invokeString(id: Long, value: String) +} diff --git a/Package.swift b/Package.swift index e098145..bfd8a74 100644 --- a/Package.swift +++ b/Package.swift @@ -13,18 +13,39 @@ let package = Package( .library( name: "AndroidSwiftUI", targets: ["AndroidSwiftUI"] + ), + .library( + name: "AndroidSwiftUIBridge", + targets: ["AndroidSwiftUIBridge"] + ), + // Desktop test-rig dylib: the bridge plus a demo root view, loaded by + // the Compose Desktop rig on the host JVM. + .library( + name: "SwiftUIDesktopDemo", + type: .dynamic, + targets: ["SwiftUIDesktopDemo"] ) ], dependencies: [ .package( url: "https://github.com/PureSwift/Android.git", branch: "master" - ) + ), + .package( + url: "https://github.com/swiftlang/swift-java.git", + branch: "main" + ), + .package(path: "AndroidSwiftUICore") ], targets: [ .target( name: "AndroidSwiftUI", dependencies: [ + "AndroidSwiftUIBridge", + .product( + name: "AndroidSwiftUICore", + package: "AndroidSwiftUICore" + ), .product( name: "AndroidKit", package: "Android" @@ -33,6 +54,29 @@ let package = Package( swiftSettings: [ .swiftLanguageMode(.v5) ] + ), + // JNI bridge between the evaluation core and the Compose interpreter. + // Platform-neutral: no android.* imports, so it builds for the desktop + // JVM (macOS dylib) and cross-compiles for Android identically. + .target( + name: "AndroidSwiftUIBridge", + dependencies: [ + .product(name: "AndroidSwiftUICore", package: "AndroidSwiftUICore"), + .product(name: "SwiftJava", package: "swift-java") + ], + swiftSettings: [ + .swiftLanguageMode(.v5) + ] + ), + .target( + name: "SwiftUIDesktopDemo", + dependencies: [ + "AndroidSwiftUIBridge", + .product(name: "AndroidSwiftUICore", package: "AndroidSwiftUICore") + ], + swiftSettings: [ + .swiftLanguageMode(.v5) + ] ) ] ) diff --git a/Sources/AndroidSwiftUI/AndroidSwiftUI.swift b/Sources/AndroidSwiftUI/AndroidSwiftUI.swift index 76da3a9..db51094 100644 --- a/Sources/AndroidSwiftUI/AndroidSwiftUI.swift +++ b/Sources/AndroidSwiftUI/AndroidSwiftUI.swift @@ -1,2 +1,12 @@ import Foundation import AndroidKit + +// The public SwiftUI API surface comes from the platform-neutral core. +@_exported import AndroidSwiftUICore + +/// Logs a message to logcat under the "SwiftUI" tag, so app code doesn't need +/// AndroidKit (whose `AndroidView.View` would collide with the core's `View`). +public func AndroidSwiftUILog(_ message: String) { + let log = try! JavaClass() + _ = log.v("SwiftUI", message) +} diff --git a/Sources/AndroidSwiftUI/SwiftUIHostView.swift b/Sources/AndroidSwiftUI/SwiftUIHostView.swift new file mode 100644 index 0000000..8ff8730 --- /dev/null +++ b/Sources/AndroidSwiftUI/SwiftUIHostView.swift @@ -0,0 +1,44 @@ +// +// SwiftUIHostView.swift +// AndroidSwiftUI +// +// Created by Alsey Coleman Miller on 7/22/26. +// + +import AndroidKit +import AndroidSwiftUIBridge +import AndroidSwiftUICore + +/// Binding for the Kotlin Compose host view. +@JavaClass("com.pureswift.swiftui.SwiftUIHostView") +open class SwiftUIHostView: AndroidView.View { + + @JavaMethod + @_nonoverride public convenience init(_ context: AndroidContent.Context?, environment: JNIEnvironment? = nil) + + @JavaMethod + open func getStore() -> AndroidSwiftUIBridge.TreeStore? +} + +/// Launches a SwiftUI root view as the activity's content. +public enum AndroidSwiftUIApp { + + // retains the runtime for the activity's lifetime + private static var runtime: BridgeRuntime? + + public static func run(_ root: any AndroidSwiftUICore.View) { + guard let activity = MainActivity.shared else { + assertionFailure("MainActivity not created yet") + return + } + let host = SwiftUIHostView(activity as AndroidContent.Context) + guard let store = host.getStore() else { + assertionFailure("host view has no tree store") + return + } + let runtime = BridgeRuntime(root: root, store: store) + Self.runtime = runtime + runtime.start() + activity.setRootView(host) + } +} diff --git a/Sources/AndroidSwiftUIBridge/BridgeRuntime.swift b/Sources/AndroidSwiftUIBridge/BridgeRuntime.swift new file mode 100644 index 0000000..13c30c4 --- /dev/null +++ b/Sources/AndroidSwiftUIBridge/BridgeRuntime.swift @@ -0,0 +1,52 @@ +// +// BridgeRuntime.swift +// AndroidSwiftUIBridge +// +// Drives a ViewHost against a Kotlin TreeStore: evaluate → materialize → +// update, re-running on every state change. +// + +import AndroidSwiftUICore + +public final class BridgeRuntime { + + /// The active runtime; the callback sink dispatches into its registry. + /// One UI tree per process for now (matches one Activity / one window). + public private(set) static var current: BridgeRuntime? + + let host: ViewHost + let store: TreeStore + + public init(root: any View, store: TreeStore) { + self.host = ViewHost(root) + self.store = store + // UI events arrive on the platform main thread (Compose dispatch), and + // state writes inside them re-evaluate synchronously here. Event + // callbacks run outside composition, so assigning the store's Compose + // state from them is the standard, safe path. Cross-thread writes get + // a scheduler when async state arrives (R5). + self.host.onStateChange = { [weak self] in + self?.push() + } + } + + /// Installs this runtime as the process-wide dispatch target and renders + /// the first tree. + public func start() { + BridgeRuntime.current = self + push() + } + + func push() { + let tree = host.evaluate() + store.update(Materializer.materialize(tree)) + } + + // Dispatch entry points, called by the callback sink. + + public func invokeVoid(_ id: Int64) { host.callbacks.invokeVoid(id) } + public func invokeBool(_ id: Int64, _ value: Bool) { host.callbacks.invokeBool(id, value) } + public func invokeDouble(_ id: Int64, _ value: Double) { host.callbacks.invokeDouble(id, value) } + public func invokeInt(_ id: Int64, _ value: Int) { host.callbacks.invokeInt(id, value) } + public func invokeString(_ id: Int64, _ value: String) { host.callbacks.invokeString(id, value) } +} diff --git a/Sources/AndroidSwiftUIBridge/KotlinBindings.swift b/Sources/AndroidSwiftUIBridge/KotlinBindings.swift new file mode 100644 index 0000000..4028a34 --- /dev/null +++ b/Sources/AndroidSwiftUIBridge/KotlinBindings.swift @@ -0,0 +1,41 @@ +// +// KotlinBindings.swift +// AndroidSwiftUIBridge +// +// Swift→Kotlin bindings for the interpreter's node model. This is the safe +// bridge direction: methods resolve by name+signature lookup and fail loudly +// on mismatch. +// + +import SwiftJava + +/// Binding for `com.pureswift.swiftui.ViewNode`. +@JavaClass("com.pureswift.swiftui.ViewNode") +open class ViewNodeObject: JavaObject { + + /// The bridge constructor: one JNI call per node, arrays crossing as + /// single arguments. Prop/modifier-arg values are JSON literals; negative + /// count/provider mean "absent". + @JavaMethod + @_nonoverride public convenience init( + _ type: String, + _ id: String, + _ propKeys: [String], + _ propValues: [String], + _ modifierKinds: [String], + _ modifierArgs: [String], + _ children: [ViewNodeObject?], + _ count: Int32, + _ itemProviderId: Int64, + environment: JNIEnvironment? = nil + ) +} + +/// Binding for `com.pureswift.swiftui.TreeStore`. +@JavaClass("com.pureswift.swiftui.TreeStore") +open class TreeStore: JavaObject { + + /// Assigns a freshly materialized tree; Compose recomposes changed subtrees. + @JavaMethod + open func update(_ node: ViewNodeObject?) +} diff --git a/Sources/AndroidSwiftUIBridge/Materializer.swift b/Sources/AndroidSwiftUIBridge/Materializer.swift new file mode 100644 index 0000000..04d6912 --- /dev/null +++ b/Sources/AndroidSwiftUIBridge/Materializer.swift @@ -0,0 +1,84 @@ +// +// Materializer.swift +// AndroidSwiftUIBridge +// +// Walks the core's RenderNode IR and constructs the interpreter's Kotlin +// ViewNode objects — the typed transport that replaces string serialization. +// + +import AndroidSwiftUICore +import SwiftJava + +public enum Materializer { + + /// Builds the Kotlin mirror of an IR tree, depth-first. + public static func materialize(_ node: RenderNode) -> ViewNodeObject { + var propKeys: [String] = [] + var propValues: [String] = [] + for (key, value) in node.props { + propKeys.append(key) + propValues.append(jsonLiteral(value)) + } + var modifierKinds: [String] = [] + var modifierArgs: [String] = [] + for modifier in node.modifiers { + modifierKinds.append(modifier.kind) + modifierArgs.append(jsonObjectLiteral(modifier.args)) + } + let children = node.children.map { materialize($0) as ViewNodeObject? } + return ViewNodeObject( + node.type, + node.id, + propKeys, + propValues, + modifierKinds, + modifierArgs, + children, + Int32(node.count ?? -1), + Int64(-1) + ) + } + + /// Encodes a prop value as a JSON literal (`"text"`, `42`, `true`, `[…]`). + static func jsonLiteral(_ value: PropValue) -> String { + switch value { + case .string(let string): + return escapeJSON(string) + case .double(let double): + return "\(double)" + case .int(let int): + return "\(int)" + case .bool(let bool): + return bool ? "true" : "false" + case .array(let values): + return "[" + values.map(jsonLiteral).joined(separator: ",") + "]" + } + } + + static func jsonObjectLiteral(_ args: [String: PropValue]) -> String { + let members = args.map { "\(escapeJSON($0.key)):\(jsonLiteral($0.value))" } + return "{" + members.joined(separator: ",") + "}" + } + + /// Minimal JSON string escaping (quotes, backslashes, control characters). + static func escapeJSON(_ string: String) -> String { + var out = "\"" + for scalar in string.unicodeScalars { + switch scalar { + case "\"": out += "\\\"" + case "\\": out += "\\\\" + case "\n": out += "\\n" + case "\r": out += "\\r" + case "\t": out += "\\t" + default: + if scalar.value < 0x20 { + let hex = String(scalar.value, radix: 16) + out += "\\u" + String(repeating: "0", count: 4 - hex.count) + hex + } else { + out.unicodeScalars.append(scalar) + } + } + } + return out + "\"" + } +} diff --git a/Sources/AndroidSwiftUIBridge/SwiftCallbackSink.swift b/Sources/AndroidSwiftUIBridge/SwiftCallbackSink.swift new file mode 100644 index 0000000..682a55d --- /dev/null +++ b/Sources/AndroidSwiftUIBridge/SwiftCallbackSink.swift @@ -0,0 +1,44 @@ +// +// SwiftCallbackSink.swift +// AndroidSwiftUIBridge +// +// The entire Kotlin→Swift bridge surface. The JNI symbol for each method +// derives from THIS signature — the Kotlin `external` declarations in +// SwiftCallbackSink.kt must stay exactly in sync, and this class must never +// grow per-view methods. +// + +import SwiftJava + +@JavaClass("com.pureswift.swiftui.SwiftCallbackSink") +open class SwiftCallbackSink: JavaObject { +} + +@JavaImplementation("com.pureswift.swiftui.SwiftCallbackSink") +extension SwiftCallbackSink { + + @JavaMethod + func invokeVoid(_ id: Int64) { + BridgeRuntime.current?.invokeVoid(id) + } + + @JavaMethod + func invokeBool(_ id: Int64, _ value: Bool) { + BridgeRuntime.current?.invokeBool(id, value) + } + + @JavaMethod + func invokeDouble(_ id: Int64, _ value: Double) { + BridgeRuntime.current?.invokeDouble(id, value) + } + + @JavaMethod + func invokeInt(_ id: Int64, _ value: Int32) { + BridgeRuntime.current?.invokeInt(id, Int(value)) + } + + @JavaMethod + func invokeString(_ id: Int64, _ value: String) { + BridgeRuntime.current?.invokeString(id, value) + } +} diff --git a/Sources/SwiftUIDesktopDemo/DesktopDemo.swift b/Sources/SwiftUIDesktopDemo/DesktopDemo.swift new file mode 100644 index 0000000..e43df5f --- /dev/null +++ b/Sources/SwiftUIDesktopDemo/DesktopDemo.swift @@ -0,0 +1,39 @@ +// +// DesktopDemo.swift +// SwiftUIDesktopDemo +// +// The desktop rig's Swift entry: a counter view evaluated by the core and +// pushed through the bridge into the rig's Compose window. +// + +import AndroidSwiftUICore +import AndroidSwiftUIBridge +import SwiftJava + +struct CounterDemo: View { + + @State private var count = 0 + + var body: some View { + VStack(spacing: 12) { + Text("Count: \(count)") + Button("Increment") { count += 1 } + Toggle("Feature flag", isOn: .constant(true)) + } + } +} + +@JavaClass("com.pureswift.swiftui.desktop.SwiftRuntime") +open class SwiftRuntime: JavaObject { +} + +@JavaImplementation("com.pureswift.swiftui.desktop.SwiftRuntime") +extension SwiftRuntime { + + @JavaMethod + func start(_ store: TreeStore?) { + guard let store else { return } + let runtime = BridgeRuntime(root: CounterDemo(), store: store) + runtime.start() + } +}