Skip to content
Merged
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
17 changes: 17 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -54,6 +66,11 @@ let package = Package(
.product(
name: "AndroidKit",
package: "Android"
),
.product(
name: "AndroidLooper",
package: "swift-android-native",
condition: .when(platforms: [.android])
)
],
swiftSettings: [
Expand Down
12 changes: 12 additions & 0 deletions Sources/AndroidSwiftUI/AndroidApplication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
//

import AndroidKit
#if canImport(AndroidLooper)
import AndroidLooper
#endif

@JavaClass("com.pureswift.swiftandroid.Application")
open class Application: AndroidApp.Application {
Expand All @@ -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
Expand Down
21 changes: 0 additions & 21 deletions Sources/AndroidSwiftUI/MainActivity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ extension MainActivity {

// start app
AndroidSwiftUIMain()

runAsync()
}

@JavaMethod
Expand All @@ -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" }
Expand Down
10 changes: 8 additions & 2 deletions Sources/AndroidSwiftUI/SwiftUIHostView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<AndroidOS.Looper>().getMainLooper())
let runtime = BridgeRuntime(root: root, store: store) { block in
let runnable = Runnable { block() }
Expand Down
Loading