diff --git a/Package.swift b/Package.swift index 638fe32..8d694fe 100644 --- a/Package.swift +++ b/Package.swift @@ -38,6 +38,18 @@ let package = Package( url: "https://github.com/swiftlang/swift-java.git", branch: "main" ), + // A Swift global actor backed by the Android main looper, so `@MainActor` + // / `DispatchQueue.main` work on Android without hand-draining a RunLoop. + // Same identity (`swift-android-native`) as the PureSwift/Android + // dependency; pinned to the fork/branch that package uses so the Android + // build's single-revision requirement is satisfied. The `CoreFoundation` + // trait drains the dispatch main queue through `CFRunLoopRunInMode`, which + // is what makes `DispatchQueue.main` reliable as the render scheduler. + .package( + url: "https://github.com/MillerTechnologyPeru/swift-android-native.git", + branch: "feature/pureswift", + traits: ["CoreFoundation"] + ), .package(path: "SwiftUICore") ], targets: [ @@ -54,6 +66,11 @@ let package = Package( .product( name: "AndroidKit", package: "Android" + ), + .product( + name: "AndroidLooper", + package: "swift-android-native", + condition: .when(platforms: [.android]) ) ], swiftSettings: [ diff --git a/Sources/AndroidSwiftUI/AndroidApplication.swift b/Sources/AndroidSwiftUI/AndroidApplication.swift index 028b2e8..21fd8ce 100644 --- a/Sources/AndroidSwiftUI/AndroidApplication.swift +++ b/Sources/AndroidSwiftUI/AndroidApplication.swift @@ -6,6 +6,9 @@ // import AndroidKit +#if canImport(AndroidLooper) +import AndroidLooper +#endif @JavaClass("com.pureswift.swiftandroid.Application") open class Application: AndroidApp.Application { @@ -20,6 +23,15 @@ extension Application { func onCreateSwift() { log("\(self).\(#function)") Application.shared = self + + // Bind the Android main looper to `AndroidMainActor` at process launch. + // `Application.onCreate` runs on the main thread — the required call site + // — so `@MainActor` and `DispatchQueue.main` dispatch correctly from here + // on, without hand-draining `RunLoop.main`. + #if canImport(AndroidLooper) + let boundMainLooper = AndroidMainActor.setupMainLooper() + log("AndroidMainActor.setupMainLooper() -> \(boundMainLooper)") + #endif } @JavaMethod diff --git a/Sources/AndroidSwiftUI/MainActivity.swift b/Sources/AndroidSwiftUI/MainActivity.swift index 7c8e27b..f8613dd 100644 --- a/Sources/AndroidSwiftUI/MainActivity.swift +++ b/Sources/AndroidSwiftUI/MainActivity.swift @@ -37,8 +37,6 @@ extension MainActivity { // start app AndroidSwiftUIMain() - - runAsync() } @JavaMethod @@ -47,25 +45,6 @@ extension MainActivity { } } -private extension MainActivity { - - func runAsync() { - RunLoop.main.run(until: Date() + 0.1) - DispatchQueue.main.async { - Self.log("\(self).\(#function) Main Thread Async") - } - DispatchQueue.global(qos: .default).async { - Self.log("\(self).\(#function) Default Dispatch Queue Async") - } - Task { - Self.log("\(self).\(#function) Task Started") - await MainActor.run { - RunLoop.main.run(until: Date() + 0.1) - } - } - } -} - extension MainActivity { static var logTag: String { "MainActivity" } diff --git a/Sources/AndroidSwiftUI/SwiftUIHostView.swift b/Sources/AndroidSwiftUI/SwiftUIHostView.swift index 48d5b7b..b123ecf 100644 --- a/Sources/AndroidSwiftUI/SwiftUIHostView.swift +++ b/Sources/AndroidSwiftUI/SwiftUIHostView.swift @@ -37,8 +37,14 @@ public enum AndroidSwiftUIApp { assertionFailure("host view has no tree store") return } - // marshal re-renders onto the main looper: JNI object creation and - // Compose state writes must run there, not on a Swift Task's thread + // Re-renders post to the main looper through a JVM `Runnable`, NOT + // `DispatchQueue.main`. Rendering makes JNI calls (materializing the + // `ViewNode` tree), and JNI `FindClass` resolves against the class loader + // of the Java frame on the stack. A `Runnable.run()` invocation carries + // the app's class loader; the dispatch main-queue drain runs in a native + // context whose fallback boot class loader can't see the app's classes, + // so JNI aborts with `NoClassDefFoundError`. `AndroidMainActor`/ + // `DispatchQueue.main` (bound at launch) remain correct for non-JNI work. let handler = AndroidOS.Handler(try! JavaClass().getMainLooper()) let runtime = BridgeRuntime(root: root, store: store) { block in let runnable = Runnable { block() }