From 3d738e5afa56cbe799be15bb9db3a1c28c763d2d Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:18:40 -0400 Subject: [PATCH 01/33] Replace Skip dependencies with AndroidSwiftUI --- Package.swift | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/Package.swift b/Package.swift index 53f1e4b..7a00432 100644 --- a/Package.swift +++ b/Package.swift @@ -1,5 +1,4 @@ // swift-tools-version: 6.2 -// This is a Skip (https://skip.tools) package. import PackageDescription let package = Package( @@ -10,13 +9,12 @@ let package = Package( .library(name: "BluetoothExplorer", type: .dynamic, targets: ["BluetoothExplorer"]) ], dependencies: [ - .package(url: "https://source.skip.tools/skip.git", from: "1.7.1"), - .package(url: "https://source.skip.tools/skip-model.git", from: "1.0.0"), - .package(url: "https://github.com/MillerTechnologyPeru/skip-fuse-ui.git", branch: "feature/pureswift"), - .package(url: "https://github.com/MillerTechnologyPeru/skip-fuse.git", branch: "feature/pureswift"), .package(url: "https://github.com/PureSwift/GATT.git", branch: "master"), .package(url: "https://github.com/PureSwift/AndroidBluetooth.git", branch: "master"), .package(url: "https://github.com/PureSwift/Bluetooth.git", from: "7.2.0"), + // SwiftUI for Android. Apple platforms use the system SwiftUI instead, so this is only + // linked for .android — see the conditional target dependencies below. + .package(url: "https://github.com/PureSwift/AndroidSwiftUI.git", branch: "master"), .package(url: "https://github.com/swiftwasm/WasmKit.git", .upToNextMinor(from: "0.3.1")) ], targets: [ @@ -25,24 +23,25 @@ let package = Package( dependencies: [ "BluetoothExplorerUI", .product( - name: "SkipFuseUI", - package: "skip-fuse-ui" + name: "AndroidSwiftUI", + package: "AndroidSwiftUI", + condition: .when(platforms: [.android]) ) ], - resources: [.process("Resources")], - plugins: [.plugin(name: "skipstone", package: "skip")] + resources: [.process("Resources")] ), .target( name: "BluetoothExplorerUI", dependencies: [ "BluetoothExplorerModel", "BluetoothExplorerPluginEngine", - .product(name: "SkipModel", package: "skip-model"), - .product(name: "SkipFuse", package: "skip-fuse"), - .product(name: "SkipFuseUI", package: "skip-fuse-ui") + .product( + name: "AndroidSwiftUI", + package: "AndroidSwiftUI", + condition: .when(platforms: [.android]) + ) ], - resources: [.process("Resources")], - plugins: [.plugin(name: "skipstone", package: "skip")] + resources: [.process("Resources")] ), .target( name: "BluetoothExplorerPluginEngine", @@ -71,16 +70,12 @@ let package = Package( condition: .when(platforms: [.android]) ), .product( - name: "SkipFuse", - package: "skip-fuse" - ), - .product( - name: "SkipModel", - package: "skip-model" + name: "AndroidSwiftUI", + package: "AndroidSwiftUI", + condition: .when(platforms: [.android]) ) ], - resources: [.process("Resources")], - plugins: [.plugin(name: "skipstone", package: "skip")] + resources: [.process("Resources")] ), .testTarget( name: "BluetoothExplorerPluginEngineTests", From 30ef86c72a819d76a4acb55879e95abd54af8ba1 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:18:40 -0400 Subject: [PATCH 02/33] Remove Skip environment configuration --- Skip.env | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 Skip.env diff --git a/Skip.env b/Skip.env deleted file mode 100644 index 600340f..0000000 --- a/Skip.env +++ /dev/null @@ -1,23 +0,0 @@ -// The configuration file for your Skip App (https://skip.tools). -// Properties specified here are shared between -// Darwin/BluetoothExplorer.xcconfig and Android/settings.gradle.kts -// and will be included in the app's metadata files -// Info.plist and AndroidManifest.xml - -// PRODUCT_NAME is the default title of the app, which must match the app's Swift module name -PRODUCT_NAME = BluetoothExplorer - -// PRODUCT_BUNDLE_IDENTIFIER is the unique id for both the iOS and Android app -PRODUCT_BUNDLE_IDENTIFIER = org.pureswift.bluetoothexplorer - -// The semantic version of the app -MARKETING_VERSION = 1.0.0 - -// The build number specifying the internal app version -CURRENT_PROJECT_VERSION = 1 - -// The package name for the Android entry point, referenced by the AndroidManifest.xml -ANDROID_PACKAGE_NAME = bluetooth.explorer - -// If your Android appId is different from the iOS Bundle Identifer, specify it here -// ANDROID_APPLICATION_ID = org.pureswift.bluetoothexplorer From 015ebc697e47442ee852303b2c35a0628f219081 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:18:41 -0400 Subject: [PATCH 03/33] Remove Skip module configuration from the app target --- Sources/BluetoothExplorer/Skip/skip.yml | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 Sources/BluetoothExplorer/Skip/skip.yml diff --git a/Sources/BluetoothExplorer/Skip/skip.yml b/Sources/BluetoothExplorer/Skip/skip.yml deleted file mode 100644 index fdfcd15..0000000 --- a/Sources/BluetoothExplorer/Skip/skip.yml +++ /dev/null @@ -1,12 +0,0 @@ -# Configuration file for https://skip.tools project -# -# Kotlin dependencies and Gradle build options for this module can be configured here -#build: -# contents: -# - block: 'dependencies' -# contents: -# - 'implementation("androidx.compose.runtime:runtime")' - -# this is a natively-compiled Skip Fuse module -skip: - mode: 'native' From 17f673acf00972e6b2c7d6bf66400c5a3202c8a1 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:18:41 -0400 Subject: [PATCH 04/33] Remove Skip module configuration from the UI target --- Sources/BluetoothExplorerUI/Skip/skip.yml | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 Sources/BluetoothExplorerUI/Skip/skip.yml diff --git a/Sources/BluetoothExplorerUI/Skip/skip.yml b/Sources/BluetoothExplorerUI/Skip/skip.yml deleted file mode 100644 index fdfcd15..0000000 --- a/Sources/BluetoothExplorerUI/Skip/skip.yml +++ /dev/null @@ -1,12 +0,0 @@ -# Configuration file for https://skip.tools project -# -# Kotlin dependencies and Gradle build options for this module can be configured here -#build: -# contents: -# - block: 'dependencies' -# contents: -# - 'implementation("androidx.compose.runtime:runtime")' - -# this is a natively-compiled Skip Fuse module -skip: - mode: 'native' From 26b1bb3fe3357cef6542819cc362ecc12d7082c0 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:18:41 -0400 Subject: [PATCH 05/33] Remove Skip module configuration from the model target --- Sources/BluetoothExplorerModel/Skip/skip.yml | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 Sources/BluetoothExplorerModel/Skip/skip.yml diff --git a/Sources/BluetoothExplorerModel/Skip/skip.yml b/Sources/BluetoothExplorerModel/Skip/skip.yml deleted file mode 100644 index fdfcd15..0000000 --- a/Sources/BluetoothExplorerModel/Skip/skip.yml +++ /dev/null @@ -1,12 +0,0 @@ -# Configuration file for https://skip.tools project -# -# Kotlin dependencies and Gradle build options for this module can be configured here -#build: -# contents: -# - block: 'dependencies' -# contents: -# - 'implementation("androidx.compose.runtime:runtime")' - -# this is a natively-compiled Skip Fuse module -skip: - mode: 'native' From 8dc796f2e08134986efabc10671c45a61b296477 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:17:53 -0400 Subject: [PATCH 06/33] Add a logger shim to replace the Skip logging framework --- Sources/BluetoothExplorer/Logging.swift | 36 +++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 Sources/BluetoothExplorer/Logging.swift diff --git a/Sources/BluetoothExplorer/Logging.swift b/Sources/BluetoothExplorer/Logging.swift new file mode 100644 index 0000000..90ebc80 --- /dev/null +++ b/Sources/BluetoothExplorer/Logging.swift @@ -0,0 +1,36 @@ +// +// Logging.swift +// BluetoothExplorer +// +// A minimal logger so the app does not depend on a cross-platform logging framework. +// Apple platforms use `os.Logger`; Android falls back to printing, which AndroidSwiftUI's +// runtime surfaces through logcat. +// + +#if canImport(os) +import os + +typealias AppLogger = os.Logger +#else +/// Stand-in for `os.Logger` on platforms without the unified logging system. +struct AppLogger { + + let subsystem: String + let category: String + + init(subsystem: String, category: String) { + self.subsystem = subsystem + self.category = category + } + + func debug(_ message: String) { emit("DEBUG", message) } + func info(_ message: String) { emit("INFO", message) } + func notice(_ message: String) { emit("NOTICE", message) } + func warning(_ message: String) { emit("WARNING", message) } + func error(_ message: String) { emit("ERROR", message) } + + private func emit(_ level: String, _ message: String) { + print("[\(level)] \(subsystem)/\(category): \(message)") + } +} +#endif From 174cc0c096d8d81343b4af176cbe6651aabd2002 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:17:53 -0400 Subject: [PATCH 07/33] Drop Skip bridging from the app entry point --- .../BluetoothExplorerApp.swift | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/Sources/BluetoothExplorer/BluetoothExplorerApp.swift b/Sources/BluetoothExplorer/BluetoothExplorerApp.swift index 2d4a1b4..d0589bc 100644 --- a/Sources/BluetoothExplorer/BluetoothExplorerApp.swift +++ b/Sources/BluetoothExplorer/BluetoothExplorerApp.swift @@ -1,18 +1,21 @@ import Foundation -import SkipFuse +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import BluetoothExplorerModel import BluetoothExplorerUI /// A logger for the BluetoothExplorer module. -let logger: Logger = Logger(subsystem: "org.pureswift.bluetoothexplorer", category: "BluetoothExplorer") +let logger = AppLogger(subsystem: "org.pureswift.bluetoothexplorer", category: "BluetoothExplorer") /// The shared top-level view for the app, loaded from the platform-specific App delegates below. /// /// The default implementation merely loads the `ContentView` for the app and logs a message. -/* SKIP @bridge */public struct BluetoothExplorerRootView : View { - /* SKIP @bridge */public init() { +public struct BluetoothExplorerRootView : View { + public init() { } @State @@ -32,7 +35,7 @@ let logger: Logger = Logger(subsystem: "org.pureswift.bluetoothexplorer", catego } } .task { - logger.info("Skip app logs are viewable in the Xcode console for iOS; Android logs can be viewed in Studio or using adb logcat") + logger.info("BluetoothExplorer started") } } } @@ -40,37 +43,37 @@ let logger: Logger = Logger(subsystem: "org.pureswift.bluetoothexplorer", catego /// Global application delegate functions. /// /// These functions can update a shared observable object to communicate app state changes to interested views. -/* SKIP @bridge */public final class BluetoothExplorerAppDelegate : Sendable { - /* SKIP @bridge */public static let shared = BluetoothExplorerAppDelegate() +public final class BluetoothExplorerAppDelegate : Sendable { + public static let shared = BluetoothExplorerAppDelegate() private init() { } - /* SKIP @bridge */public func onInit() { + public func onInit() { logger.debug("onInit") } - /* SKIP @bridge */public func onLaunch() { + public func onLaunch() { logger.debug("onLaunch") } - /* SKIP @bridge */public func onResume() { + public func onResume() { logger.debug("onResume") } - /* SKIP @bridge */public func onPause() { + public func onPause() { logger.debug("onPause") } - /* SKIP @bridge */public func onStop() { + public func onStop() { logger.debug("onStop") } - /* SKIP @bridge */public func onDestroy() { + public func onDestroy() { logger.debug("onDestroy") } - /* SKIP @bridge */public func onLowMemory() { + public func onLowMemory() { logger.debug("onLowMemory") } } From 087804846d6090de0f4d00016ef70af70e869ef9 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:17:53 -0400 Subject: [PATCH 08/33] Remove Skip template scaffolding and Compose interop --- Sources/BluetoothExplorer/ContentView.swift | 63 ++++----------------- 1 file changed, 10 insertions(+), 53 deletions(-) diff --git a/Sources/BluetoothExplorer/ContentView.swift b/Sources/BluetoothExplorer/ContentView.swift index 63bdec1..0291b2d 100644 --- a/Sources/BluetoothExplorer/ContentView.swift +++ b/Sources/BluetoothExplorer/ContentView.swift @@ -1,13 +1,16 @@ +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import BluetoothExplorerUI enum ContentTab: String, Hashable { - case welcome, home, plugins, settings + case devices, plugins, settings } struct ContentView: View { - @AppStorage("tab") var tab = ContentTab.welcome - @AppStorage("name") var welcomeName = "Skipper" + @AppStorage("tab") var tab = ContentTab.devices @AppStorage("appearance") var appearance = "" var body: some View { @@ -15,8 +18,8 @@ struct ContentView: View { NavigationStack { CentralList() } - .tabItem { Label("Welcome", systemImage: "heart.fill") } - .tag(ContentTab.welcome) + .tabItem { Label("Devices", systemImage: "dot.radiowaves.left.and.right") } + .tag(ContentTab.devices) NavigationStack { PluginsView() @@ -25,7 +28,7 @@ struct ContentView: View { .tag(ContentTab.plugins) NavigationStack { - SettingsView(appearance: $appearance, welcomeName: $welcomeName) + SettingsView(appearance: $appearance) .navigationTitle("Settings") } .tabItem { Label("Settings", systemImage: "gearshape.fill") } @@ -35,31 +38,11 @@ struct ContentView: View { } } -struct WelcomeView : View { - @State var heartBeating = false - @Binding var welcomeName: String - - var body: some View { - VStack(spacing: 0) { - Text("Hello [\(welcomeName)](https://skip.dev)!") - .padding() - Image(systemName: "heart.fill") - .foregroundStyle(.red) - .scaleEffect(heartBeating ? 1.5 : 1.0) - .animation(.easeInOut(duration: 1).repeatForever(), value: heartBeating) - .task { heartBeating = true } - } - .font(.largeTitle) - } -} - -struct SettingsView : View { +struct SettingsView: View { @Binding var appearance: String - @Binding var welcomeName: String var body: some View { Form { - TextField("Name", text: $welcomeName) Picker("Appearance", selection: $appearance) { Text("System").tag("") Text("Light").tag("light") @@ -69,32 +52,6 @@ struct SettingsView : View { let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { Text("Version \(version) (\(buildNumber))") } - HStack { - PlatformHeartView() - Text("Powered by [Skip](https://skip.tools)") - } } } } - -/// A view that shows a blue heart on iOS and a green heart on Android. -struct PlatformHeartView : View { - var body: some View { - #if os(Android) - ComposeView { - HeartComposer() - } - #else - Text(verbatim: "💙") - #endif - } -} - -#if SKIP -/// Use a ContentComposer to integrate Compose content. This code will be transpiled to Kotlin. -struct HeartComposer : ContentComposer { - @Composable func Compose(context: ComposeContext) { - androidx.compose.material3.Text("💚", modifier: context.modifier) - } -} -#endif From 4314bba066183459640b34e0c369a2fcfce55de2 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:17:53 -0400 Subject: [PATCH 09/33] Import Observation directly in Store --- Sources/BluetoothExplorerModel/Model/Store.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/BluetoothExplorerModel/Model/Store.swift b/Sources/BluetoothExplorerModel/Model/Store.swift index 0adeb13..5bb3fe7 100644 --- a/Sources/BluetoothExplorerModel/Model/Store.swift +++ b/Sources/BluetoothExplorerModel/Model/Store.swift @@ -8,8 +8,6 @@ import Foundation import Observation -import SkipFuse -import SkipModel import Bluetooth import GATT import BluetoothExplorerPluginEngine From edc963345f6a98e7a0e9f655b82eee984bd5b125 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:17:54 -0400 Subject: [PATCH 10/33] Import Observation directly in PluginManager --- Sources/BluetoothExplorerModel/Model/PluginManager.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/BluetoothExplorerModel/Model/PluginManager.swift b/Sources/BluetoothExplorerModel/Model/PluginManager.swift index 59230dc..8aab2c2 100644 --- a/Sources/BluetoothExplorerModel/Model/PluginManager.swift +++ b/Sources/BluetoothExplorerModel/Model/PluginManager.swift @@ -8,7 +8,6 @@ import Foundation import Observation -import SkipFuse import BluetoothExplorerPluginEngine @MainActor From b9da0915ebf78998c6b26bbc9a47e84697bdac73 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:17:54 -0400 Subject: [PATCH 11/33] Import Observation directly in CentralListViewModel --- .../ViewModel/CentralListViewModel.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/BluetoothExplorerModel/ViewModel/CentralListViewModel.swift b/Sources/BluetoothExplorerModel/ViewModel/CentralListViewModel.swift index 312b802..ec6e7e2 100644 --- a/Sources/BluetoothExplorerModel/ViewModel/CentralListViewModel.swift +++ b/Sources/BluetoothExplorerModel/ViewModel/CentralListViewModel.swift @@ -9,9 +9,6 @@ import Foundation import Observation import Bluetooth import GATT -import SkipFuse -import SkipUI -import SkipModel @MainActor @Observable From 47329b48c20738df42695fe53650a3a42cd7ccbe Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:17:54 -0400 Subject: [PATCH 12/33] Import Observation directly in PeripheralViewModel --- .../BluetoothExplorerModel/ViewModel/PeripheralViewModel.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/BluetoothExplorerModel/ViewModel/PeripheralViewModel.swift b/Sources/BluetoothExplorerModel/ViewModel/PeripheralViewModel.swift index 55aab43..8a2bd36 100644 --- a/Sources/BluetoothExplorerModel/ViewModel/PeripheralViewModel.swift +++ b/Sources/BluetoothExplorerModel/ViewModel/PeripheralViewModel.swift @@ -9,8 +9,6 @@ import Foundation import Observation import Bluetooth import GATT -import SkipFuse -import SkipModel @MainActor @Observable From 81692b8b8aeba9e087080539fd048b1803edad9c Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:17:54 -0400 Subject: [PATCH 13/33] Import AndroidSwiftUI on Android in AsyncButton --- Sources/BluetoothExplorerUI/AsyncButton.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/BluetoothExplorerUI/AsyncButton.swift b/Sources/BluetoothExplorerUI/AsyncButton.swift index d8d2943..62f5f2f 100644 --- a/Sources/BluetoothExplorerUI/AsyncButton.swift +++ b/Sources/BluetoothExplorerUI/AsyncButton.swift @@ -5,7 +5,11 @@ // Created by Alsey Coleman Miller on 23/12/21. // +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif enum ActionOption: CaseIterable { case disableButton From 784bbeecd76342839ffb045d8899a9edf15a1df1 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:17:54 -0400 Subject: [PATCH 14/33] Import AndroidSwiftUI on Android in AttributeCell --- Sources/BluetoothExplorerUI/AttributeCell.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/BluetoothExplorerUI/AttributeCell.swift b/Sources/BluetoothExplorerUI/AttributeCell.swift index efbb3fb..ae20305 100644 --- a/Sources/BluetoothExplorerUI/AttributeCell.swift +++ b/Sources/BluetoothExplorerUI/AttributeCell.swift @@ -5,7 +5,11 @@ // Created by Alsey Coleman Miller on 19/12/21. // +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import Bluetooth import GATT From a731e0a6a0a3d88a0fe077ebb4a0e46d1a1a7de9 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:17:54 -0400 Subject: [PATCH 15/33] Import AndroidSwiftUI on Android in AttributeValueCell --- Sources/BluetoothExplorerUI/AttributeValueCell.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/BluetoothExplorerUI/AttributeValueCell.swift b/Sources/BluetoothExplorerUI/AttributeValueCell.swift index ccae711..ce3f299 100644 --- a/Sources/BluetoothExplorerUI/AttributeValueCell.swift +++ b/Sources/BluetoothExplorerUI/AttributeValueCell.swift @@ -7,7 +7,11 @@ import Foundation import Bluetooth +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import BluetoothExplorerModel import BluetoothExplorerPluginEngine From db8205cf0e6b538f1303a76bf9128471f094dfbe Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:17:54 -0400 Subject: [PATCH 16/33] Import AndroidSwiftUI on Android in AttributeValuesSection --- Sources/BluetoothExplorerUI/AttributeValuesSection.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/BluetoothExplorerUI/AttributeValuesSection.swift b/Sources/BluetoothExplorerUI/AttributeValuesSection.swift index f57ab90..14bd7b3 100644 --- a/Sources/BluetoothExplorerUI/AttributeValuesSection.swift +++ b/Sources/BluetoothExplorerUI/AttributeValuesSection.swift @@ -7,7 +7,11 @@ import Foundation import Bluetooth +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import BluetoothExplorerModel import BluetoothExplorerPluginEngine From bd30a839e5bcd427adeb40865ed44a5b97a16178 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:17:54 -0400 Subject: [PATCH 17/33] Import AndroidSwiftUI on Android in CentralCell --- Sources/BluetoothExplorerUI/CentralCell.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/BluetoothExplorerUI/CentralCell.swift b/Sources/BluetoothExplorerUI/CentralCell.swift index 74a5120..7351bdc 100644 --- a/Sources/BluetoothExplorerUI/CentralCell.swift +++ b/Sources/BluetoothExplorerUI/CentralCell.swift @@ -5,7 +5,11 @@ // Created by Alsey Coleman Miller on 18/12/21. // +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import BluetoothExplorerModel public struct CentralCell: View { From 3f1e66fadae78b7793703f91b869b7fa55d88ad1 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:17:54 -0400 Subject: [PATCH 18/33] Import AndroidSwiftUI on Android in CentralList --- Sources/BluetoothExplorerUI/CentralList.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/BluetoothExplorerUI/CentralList.swift b/Sources/BluetoothExplorerUI/CentralList.swift index 4dd520b..dad5a21 100644 --- a/Sources/BluetoothExplorerUI/CentralList.swift +++ b/Sources/BluetoothExplorerUI/CentralList.swift @@ -6,7 +6,11 @@ // Copyright © 2019 Alsey Coleman Miller. All rights reserved. // +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import BluetoothExplorerModel public struct CentralList: View { From 7a4d5db4dd5f305d91cdbc2904bd395f70c93211 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:17:54 -0400 Subject: [PATCH 19/33] Import AndroidSwiftUI on Android in CharacteristicView --- Sources/BluetoothExplorerUI/CharacteristicView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/BluetoothExplorerUI/CharacteristicView.swift b/Sources/BluetoothExplorerUI/CharacteristicView.swift index 7b9fd86..1d61515 100644 --- a/Sources/BluetoothExplorerUI/CharacteristicView.swift +++ b/Sources/BluetoothExplorerUI/CharacteristicView.swift @@ -5,7 +5,11 @@ // Created by Alsey Coleman Miller on 22/12/21. // +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import Bluetooth import GATT import BluetoothExplorerModel From bb128181ca8aede737c3aec2f6a7abbe2c3f5a46 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:17:54 -0400 Subject: [PATCH 20/33] Import AndroidSwiftUI on Android in DecodedFieldsView --- Sources/BluetoothExplorerUI/DecodedFieldsView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/BluetoothExplorerUI/DecodedFieldsView.swift b/Sources/BluetoothExplorerUI/DecodedFieldsView.swift index 8e36877..35c32bd 100644 --- a/Sources/BluetoothExplorerUI/DecodedFieldsView.swift +++ b/Sources/BluetoothExplorerUI/DecodedFieldsView.swift @@ -6,7 +6,11 @@ // subset (no ByteCountFormatter; Text(verbatim:) throughout). // +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import BluetoothExplorerModel import BluetoothExplorerPluginEngine From 1ed3f3aa076f58e94578ffbf41a0d88be326c776 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:17:54 -0400 Subject: [PATCH 21/33] Import AndroidSwiftUI on Android in DescriptorView --- Sources/BluetoothExplorerUI/DescriptorView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/BluetoothExplorerUI/DescriptorView.swift b/Sources/BluetoothExplorerUI/DescriptorView.swift index 617335e..cd715d5 100644 --- a/Sources/BluetoothExplorerUI/DescriptorView.swift +++ b/Sources/BluetoothExplorerUI/DescriptorView.swift @@ -5,7 +5,11 @@ // Created by Alsey Coleman Miller on 23/12/21. // +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import Bluetooth import GATT import BluetoothExplorerModel From 115432f9b5ef668e7080e5247aa23745dc36df72 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:17:54 -0400 Subject: [PATCH 22/33] Import AndroidSwiftUI on Android in PeripheralView --- Sources/BluetoothExplorerUI/PeripheralView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/BluetoothExplorerUI/PeripheralView.swift b/Sources/BluetoothExplorerUI/PeripheralView.swift index da55c86..b96abde 100644 --- a/Sources/BluetoothExplorerUI/PeripheralView.swift +++ b/Sources/BluetoothExplorerUI/PeripheralView.swift @@ -6,7 +6,11 @@ // Copyright © 2021 Alsey Coleman Miller. All rights reserved. // +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import BluetoothExplorerModel import BluetoothExplorerPluginEngine From 0754f23ac4a09824a644403cea54b1243d6fa25b Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:17:54 -0400 Subject: [PATCH 23/33] Import AndroidSwiftUI on Android in PluginsView --- Sources/BluetoothExplorerUI/PluginsView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/BluetoothExplorerUI/PluginsView.swift b/Sources/BluetoothExplorerUI/PluginsView.swift index 4fd80a6..3aca61e 100644 --- a/Sources/BluetoothExplorerUI/PluginsView.swift +++ b/Sources/BluetoothExplorerUI/PluginsView.swift @@ -7,7 +7,11 @@ // listed here lives in one place the user can inspect. // +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif #if canImport(UniformTypeIdentifiers) import UniformTypeIdentifiers #endif From b996037fadd2958972424158aec1a51a4abbe5cc Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:17:55 -0400 Subject: [PATCH 24/33] Import AndroidSwiftUI on Android in ServiceView --- Sources/BluetoothExplorerUI/ServiceView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/BluetoothExplorerUI/ServiceView.swift b/Sources/BluetoothExplorerUI/ServiceView.swift index 03c7719..49e87aa 100644 --- a/Sources/BluetoothExplorerUI/ServiceView.swift +++ b/Sources/BluetoothExplorerUI/ServiceView.swift @@ -5,7 +5,11 @@ // Created by Alsey Coleman Miller on 22/12/21. // +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import Bluetooth import GATT import BluetoothExplorerModel From 8df5434f1dd2acea484b37b23168c1663485be11 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:17:55 -0400 Subject: [PATCH 25/33] Import AndroidSwiftUI on Android in WriteAttributeView --- Sources/BluetoothExplorerUI/WriteAttributeView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/BluetoothExplorerUI/WriteAttributeView.swift b/Sources/BluetoothExplorerUI/WriteAttributeView.swift index 2a4cb7c..8ee313d 100644 --- a/Sources/BluetoothExplorerUI/WriteAttributeView.swift +++ b/Sources/BluetoothExplorerUI/WriteAttributeView.swift @@ -6,7 +6,11 @@ // import Foundation +#if canImport(SwiftUI) import SwiftUI +#else +import AndroidSwiftUI +#endif import Bluetooth import BluetoothExplorerModel From debbef7b56800bd0c8d620bd9b217e13c456abb8 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:22:07 -0400 Subject: [PATCH 26/33] Document the Skip to AndroidSwiftUI migration --- Documentation/AndroidSwiftUIMigration.md | 78 ++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 Documentation/AndroidSwiftUIMigration.md diff --git a/Documentation/AndroidSwiftUIMigration.md b/Documentation/AndroidSwiftUIMigration.md new file mode 100644 index 0000000..2a41b52 --- /dev/null +++ b/Documentation/AndroidSwiftUIMigration.md @@ -0,0 +1,78 @@ +# Migrating off Skip to AndroidSwiftUI + +The app previously used [Skip](https://skip.tools) to compile its SwiftUI codebase for Android. It +now uses the system SwiftUI on Apple platforms and [PureSwift/AndroidSwiftUI](https://github.com/PureSwift/AndroidSwiftUI) +on Android. + +## What Skip was doing + +Skip's footprint was smaller than it appeared — nine `import` lines and one `Logger` — but it also +owned the build: + +| Skip piece | Replacement | +|---|---| +| `SkipFuseUI` — SwiftUI for Android | `AndroidSwiftUI`, linked only `.when(platforms: [.android])` | +| `SkipModel` — Observation for Android | `import Observation` directly; the Swift Android SDK ships `Observation.swiftmodule` | +| `SkipFuse` — Foundation shims, `Logger` | `AppLogger` in `Sources/BluetoothExplorer/Logging.swift` (`os.Logger` on Apple, print-based elsewhere) | +| `skipstone` build plugin + three `skip.yml` | nothing — plain SwiftPM targets | +| `/* SKIP @bridge */` on the root view and app delegate | nothing — `Darwin/Sources/Main.swift` was already a real `@main` App | +| `ComposeView` / `#if SKIP` Compose interop in `ContentView` | removed with the rest of the Skip template scaffolding | +| `Skip.env` | removed | + +Views now import conditionally: + +```swift +#if canImport(SwiftUI) +import SwiftUI +#else +import AndroidSwiftUI +#endif +``` + +## The dependency-graph side effect + +This removed the whole Skip fork stack — `skip-fuse`, `skip-fuse-ui`, `skip-android-bridge`, +`skip-ui`, `skip-foundation`, `skip-model`, and their transitive Java/JNI packages. The package +dropped from ~30 resolved dependencies to 20, and **the graph now resolves from a clean checkout**: + +```sh +swift package resolve # exit 0, no mirrors, no SWIFTPM_ENABLE_MACROS=0 +``` + +Every blocker recorded in `Documentation/DependencyState.md` traced back to that stack: + +- the `swift-android-native` two-URL conflict needed `skip-android-bridge` as one of the two + claimants — with Skip gone there is only one claimant left; +- the `swift-java` / `swift-syntax` 603-vs-602 conflict came in through the same graph; +- `skip-fuse-ui` was the package that could not build against current `skip-ui`. + +Two `PureSwift` fixes are still required and are still open as PRs — `PureSwift/Android#43` and +`PureSwift/AndroidBluetooth#4`. Until they merge, local SwiftPM mirrors pointing at fixed clones are +needed; `AndroidBluetooth` otherwise fails with `product 'AndroidManifest' ... not found in package +'Android'`. + +## What AndroidSwiftUI needed + +The app's UI relies on SwiftUI that AndroidSwiftUI did not implement. These were added to +AndroidSwiftUI first (branch `feature/swiftui-containers`): + +- `NavigationStack` — reusing the existing `NavigationContext`, so `NavigationLink` push and + hardware-back pop are shared with `NavigationView` rather than duplicated +- `LazyVStack` / `LazyHStack` — eager, mapping onto the same LinearLayout path as `VStack`/`HStack` +- `TabView` with `.tabItem` and selection +- `.sheet(isPresented:content:)` +- `.refreshable` +- Swift **Observation** support — `.environment(object)` and `@Environment(Type.self)`, with + invalidation driven by `withObservationTracking` + +`.fileImporter` is not needed on Android: the plugin import button is already gated behind +`#if !os(Android)`. + +## Platform status + +- **Apple platforms** — the package resolves and every target builds with no Skip involvement. +- **Android** — the dependency wiring is in place, but the Android app build has not been verified + end to end. It additionally needs the two PureSwift PRs above, and the Kotlin JNI peers under + `Sources/BluetoothExplorer/Skip/` (`ScanCallback.kt`, `BluetoothGattCallback.kt`) rehomed into the + Android app project — they are peers for `AndroidBluetooth`, unrelated to Skip, and were only + living in that directory because skipstone collected them. From 764d0846c1a14e3a152fd91097820a68537fe16b Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:38:01 -0400 Subject: [PATCH 27/33] Raise the per-call plugin deadline to tolerate contended hosts --- .../BluetoothExplorerPluginEngine/Wasm/WasmPluginRunner.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/BluetoothExplorerPluginEngine/Wasm/WasmPluginRunner.swift b/Sources/BluetoothExplorerPluginEngine/Wasm/WasmPluginRunner.swift index a3ad6fa..139536f 100644 --- a/Sources/BluetoothExplorerPluginEngine/Wasm/WasmPluginRunner.swift +++ b/Sources/BluetoothExplorerPluginEngine/Wasm/WasmPluginRunner.swift @@ -66,7 +66,7 @@ final class WasmPluginRunner: @unchecked Sendable { init( manifest: PluginManifest, moduleBytes: [UInt8], - deadline: Duration = .milliseconds(50), + deadline: Duration = .milliseconds(250), warmupDeadline: Duration = .seconds(5) ) { self.manifest = manifest From c784e5e374d4c1c6d6978106cde83f1c38ca5841 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:38:01 -0400 Subject: [PATCH 28/33] Match the raised plugin deadline default --- .../BluetoothExplorerPluginEngine/Wasm/WasmParserPlugin.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/BluetoothExplorerPluginEngine/Wasm/WasmParserPlugin.swift b/Sources/BluetoothExplorerPluginEngine/Wasm/WasmParserPlugin.swift index 7b0ccf3..ff6723f 100644 --- a/Sources/BluetoothExplorerPluginEngine/Wasm/WasmParserPlugin.swift +++ b/Sources/BluetoothExplorerPluginEngine/Wasm/WasmParserPlugin.swift @@ -23,7 +23,7 @@ public struct WasmParserPlugin: ParserPlugin { public init( manifest: PluginManifest, moduleBytes: [UInt8], - deadline: Duration = .milliseconds(50), + deadline: Duration = .milliseconds(250), warmupDeadline: Duration = .seconds(5) ) throws { try manifest.validate() From d6cc255d05ec0f4ac1a9e8f8d5aea21b20a11e75 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:38:01 -0400 Subject: [PATCH 29/33] Serialize the GATT plugin stress suite --- .../GATTPluginTests.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Tests/BluetoothExplorerPluginEngineTests/GATTPluginTests.swift b/Tests/BluetoothExplorerPluginEngineTests/GATTPluginTests.swift index 2b25bcc..2c1debb 100644 --- a/Tests/BluetoothExplorerPluginEngineTests/GATTPluginTests.swift +++ b/Tests/BluetoothExplorerPluginEngineTests/GATTPluginTests.swift @@ -96,7 +96,11 @@ private let knownDivergences: [UInt16: String] = [:] // MARK: - Tests -@Suite("GATT characteristic plugins") +// Serialized: each test loads all 13 bundled plugins, and every plugin holds its own warm +// interpreter instance on its own thread. Running these tests concurrently multiplies that into +// dozens of contending interpreters, which is a property of the test harness rather than of the +// engine, and makes per-call latency wildly variable. +@Suite("GATT characteristic plugins", .serialized) struct GATTPluginTests { private func loadAll() throws -> [LoadedPlugin] { From 105b243f29e46e769855a995ebdc2d08bf7efd95 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 09:46:03 -0400 Subject: [PATCH 30/33] Record the AndroidSwiftUI additions and their known gaps --- Documentation/AndroidSwiftUIMigration.md | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/Documentation/AndroidSwiftUIMigration.md b/Documentation/AndroidSwiftUIMigration.md index 2a41b52..0433e10 100644 --- a/Documentation/AndroidSwiftUIMigration.md +++ b/Documentation/AndroidSwiftUIMigration.md @@ -59,15 +59,33 @@ AndroidSwiftUI first (branch `feature/swiftui-containers`): - `NavigationStack` — reusing the existing `NavigationContext`, so `NavigationLink` push and hardware-back pop are shared with `NavigationView` rather than duplicated - `LazyVStack` / `LazyHStack` — eager, mapping onto the same LinearLayout path as `VStack`/`HStack` -- `TabView` with `.tabItem` and selection -- `.sheet(isPresented:content:)` -- `.refreshable` +- `TabView` with `.tabItem` and selection — tab items are read back by walking the modifier chain + for trait-writing modifiers; only the selected tab is mounted +- `.sheet(isPresented:content:)` — a full-screen overlay in the same view hierarchy rather than an + Android `Dialog`, because the fiber renderer mounts children into the parent's `ViewGroup` and has + no path to a `Dialog`'s separate decor view +- `.refreshable` — stores a `RefreshAction` in the environment as SwiftUI does, but no gesture + triggers it: binding `SwipeRefreshLayout` would need an androidx dependency the package lacks - Swift **Observation** support — `.environment(object)` and `@Environment(Type.self)`, with invalidation driven by `withObservationTracking` `.fileImporter` is not needed on Android: the plugin import button is already gated behind `#if !os(Android)`. +### Known gaps in those additions + +All four compile and were verified to build together for Android, but none has been exercised on a +device or emulator. Notable limitations, each documented in the source: + +- **Observation**: `@Bindable` is not implemented, and `@Environment(Store.self) var store: Store?` + (the optional form) is unsupported — a missing injection traps rather than yielding `nil`. +- **TabView**: unselected tabs are unmounted, so their `@State` resets; no `TabViewStyle`, paging or + badges. +- **Sheet**: no animation, detents or drag-to-dismiss, and no `@Environment(\.dismiss)` — content + must dismiss itself through the binding. +- **NavigationStack**: `NavigationStack(path:)` and value-based `navigationDestination(for:)` are + not implemented; `NavigationContext.path` holds type-erased views, not a `Hashable` data path. + ## Platform status - **Apple platforms** — the package resolves and every target builds with no Skip involvement. From 9c05a94f3df538f1208b8e49325ea494cc4022f7 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 19:09:15 -0400 Subject: [PATCH 31/33] Inline app identity into the xcconfig after removing Skip.env --- Darwin/BluetoothExplorer.xcconfig | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/Darwin/BluetoothExplorer.xcconfig b/Darwin/BluetoothExplorer.xcconfig index cac0327..fea7034 100644 --- a/Darwin/BluetoothExplorer.xcconfig +++ b/Darwin/BluetoothExplorer.xcconfig @@ -1,12 +1,17 @@ -#include "../Skip.env" - -// Set the action that will be executed as part of the Xcode Run Script phase -// Setting to "launch" will build and run the app in the first open Android emulator or device -// Setting to "build" will just run gradle build, but will not launch the app -// Setting to "none" will completely disable the build and launch of the Android app -//SKIP_ACTION = launch -//SKIP_ACTION = build -SKIP_ACTION = none +// App identity. These previously lived in Skip.env, which was shared with the Android build; with +// Skip removed they are declared here and the Android project carries its own copy. + +// PRODUCT_NAME must match the app's Swift module name +PRODUCT_NAME = BluetoothExplorer + +// The unique id for the app +PRODUCT_BUNDLE_IDENTIFIER = org.pureswift.bluetoothexplorer + +// The semantic version of the app +MARKETING_VERSION = 1.0.0 + +// The build number specifying the internal app version +CURRENT_PROJECT_VERSION = 1 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor From 86df59d0432a9898d02c7392702f8b477bfed7f0 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 19:11:09 -0400 Subject: [PATCH 32/33] Add CI for tests, iOS archive and the Android plugin engine build --- .github/workflows/ci.yml | 97 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..35b122c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,97 @@ +# NOTE: every job here fails on a clean checkout until PureSwift/AndroidBluetooth#4 is merged. +# AndroidBluetooth requests the `AndroidManifest` product from `Android`, but it moved to +# `swift-android-native` (PureSwift/Android#40), and SwiftPM validates the whole package graph even +# for dependencies that are conditional on `.android`: +# error: product 'AndroidManifest' required by package 'androidbluetooth' ... not found +# That is a one-line manifest fix upstream, not a problem with this repo. +name: CI + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Test + runs-on: macos-15 + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + - name: Build + run: swift build --build-tests + - name: Test + run: swift test + + ios-archive: + name: iOS Archive + runs-on: macos-15 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + # WasmKit declares `.treatAllWarnings(as: .error)` for Apple platforms, which conflicts with + # the `-suppress-warnings` Xcode passes to package dependencies: + # error: Conflicting options '-warnings-as-errors' and '-suppress-warnings' + # The setting only reaches package targets from the command line, so it cannot live in the + # xcconfig or the project. + - name: Archive + working-directory: Darwin + run: | + xcodebuild archive \ + -project BluetoothExplorer.xcodeproj \ + -scheme "BluetoothExplorer App" \ + -destination 'generic/platform=iOS' \ + -archivePath "$RUNNER_TEMP/BluetoothExplorer.xcarchive" \ + -skipPackagePluginValidation \ + -skipMacroValidation \ + SWIFT_SUPPRESS_WARNINGS=NO \ + CODE_SIGNING_ALLOWED=NO + + - name: Verify archive contents + run: | + APP="$RUNNER_TEMP/BluetoothExplorer.xcarchive/Products/Applications/BluetoothExplorer.app" + test -d "$APP" || { echo "::error::archive did not produce BluetoothExplorer.app"; exit 1; } + # The bundled WASM parser plugins must ship inside the app. + find "$APP" -name '*.wasm' -print | tee /tmp/wasm-list + test -s /tmp/wasm-list || { echo "::error::no bundled .wasm plugins in the archive"; exit 1; } + + - uses: actions/upload-artifact@v4 + with: + name: ios-archive + path: ${{ runner.temp }}/BluetoothExplorer.xcarchive + retention-days: 14 + + android-package: + name: Android Swift Package + runs-on: macos-15 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Install Swift Android SDK + run: | + brew install skiptools/skip/skip || (brew update && brew install skiptools/skip/skip) + skip android sdk install --version 6.3.3 + + # Only the plugin engine is built here. The full app does not yet cross-compile for Android: + # AndroidBluetooth's `@JavaImplementation` macro generates invalid code for its scan callback, + # and WasmKit's SystemExtras target does not compile against the Android sysroot + # (`st_mode` is UInt32 there while swift-system's CInterop.Mode is UInt16). Neither is + # reachable from this target. See Documentation/AndroidSwiftUIMigration.md. + - name: Build plugin engine for Android + run: swift build --swift-sdk swift-6.3.3-RELEASE_android --target BluetoothExplorerPluginEngine From 13b2ed44a62dab5e4df7e2265660cfd4e6fade55 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Tue, 21 Jul 2026 19:11:34 -0400 Subject: [PATCH 33/33] Document what blocks an Android app archive --- Documentation/AndroidSwiftUIMigration.md | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Documentation/AndroidSwiftUIMigration.md b/Documentation/AndroidSwiftUIMigration.md index 0433e10..dc0a87c 100644 --- a/Documentation/AndroidSwiftUIMigration.md +++ b/Documentation/AndroidSwiftUIMigration.md @@ -94,3 +94,33 @@ device or emulator. Notable limitations, each documented in the source: `Sources/BluetoothExplorer/Skip/` (`ScanCallback.kt`, `BluetoothGattCallback.kt`) rehomed into the Android app project — they are peers for `AndroidBluetooth`, unrelated to Skip, and were only living in that directory because skipstone collected them. + +## Building an Android app archive + +CI (`.github/workflows/ci.yml`) archives the iOS app but does **not** produce an Android `.aab`. +Three things block it, each verified by trying: + +1. **The app does not cross-compile for Android.** `swift build --swift-sdk … --target BluetoothExplorer` + fails inside `AndroidBluetooth`: its `@JavaImplementation` macro generates invalid Swift for + `LowEnergyScanCallback.swiftOnScanFailed(error:)` — the parameter named `error` is emitted into a + context where it parses as a keyword, producing + `declaration name '…' is not covered by macro 'JavaImplementation'`. This is an upstream + swift-java/AndroidBluetooth bug. +2. **A whole-package Android build additionally fails in WasmKit's `SystemExtras`**: it does + `st_mode & S_IFMT`, but `st_mode` is `UInt32` in the Android sysroot while swift-system's + `CInterop.Mode` is `UInt16`. This target is not reachable from the app, so building specific + targets avoids it; a plain `swift build` does not. +3. **The Android project is still Skip's.** `Android/settings.gradle.kts` shells out to + `skip plugin --prebuild`, and `Android/app/build.gradle.kts` applies `skip-build-plugin` with a + `skip { }` block that reads the now-deleted `Skip.env`. With Skip removed none of that resolves. + +Replacing (3) means writing a conventional Android app project, using +[AndroidSwiftUI's `Demo/`](https://github.com/PureSwift/AndroidSwiftUI/tree/master/Demo) as the +template: its `app/src/main/java/com/pureswift/swiftandroid/` Kotlin classes (`MainActivity`, +`Application`, `NativeLibrary`, `ListViewAdapter`, `Fragment`, …) are the peers AndroidSwiftUI's +`@JavaClass` bindings bind to, and `build-swift.sh` shows the packaging step — build the Swift +package for the target architecture, then copy the resulting `.so` into +`app/src/main/jniLibs//`. The Kotlin JNI peers currently under `Sources/BluetoothExplorer/Skip/` +(`ScanCallback.kt`, `BluetoothGattCallback.kt`) belong in that project too. + +None of this is worth doing until (1) is fixed upstream, since the `.so` cannot be produced.