The app previously used Skip to compile its SwiftUI codebase for Android. It now uses the system SwiftUI on Apple platforms and PureSwift/AndroidSwiftUI on Android.
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:
#if canImport(SwiftUI)
import SwiftUI
#else
import AndroidSwiftUI
#endifThis 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:
swift package resolve # exit 0, no mirrors, no SWIFTPM_ENABLE_MACROS=0Every blocker recorded in Documentation/DependencyState.md traced back to that stack:
- the
swift-android-nativetwo-URL conflict neededskip-android-bridgeas one of the two claimants — with Skip gone there is only one claimant left; - the
swift-java/swift-syntax603-vs-602 conflict came in through the same graph; skip-fuse-uiwas the package that could not build against currentskip-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'.
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 existingNavigationContext, soNavigationLinkpush and hardware-back pop are shared withNavigationViewrather than duplicatedLazyVStack/LazyHStack— eager, mapping onto the same LinearLayout path asVStack/HStackTabViewwith.tabItemand 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 AndroidDialog, because the fiber renderer mounts children into the parent'sViewGroupand has no path to aDialog's separate decor view.refreshable— stores aRefreshActionin the environment as SwiftUI does, but no gesture triggers it: bindingSwipeRefreshLayoutwould need an androidx dependency the package lacks- Swift Observation support —
.environment(object)and@Environment(Type.self), with invalidation driven bywithObservationTracking
.fileImporter is not needed on Android: the plugin import button is already gated behind
#if !os(Android).
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:
@Bindableis not implemented, and@Environment(Store.self) var store: Store?(the optional form) is unsupported — a missing injection traps rather than yieldingnil. - TabView: unselected tabs are unmounted, so their
@Stateresets; noTabViewStyle, 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-basednavigationDestination(for:)are not implemented;NavigationContext.pathholds type-erased views, not aHashabledata path.
- 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 forAndroidBluetooth, unrelated to Skip, and were only living in that directory because skipstone collected them.
CI (.github/workflows/ci.yml) archives the iOS app but does not produce an Android .aab.
Three things block it, each verified by trying:
- The app does not cross-compile for Android.
swift build --swift-sdk … --target BluetoothExplorerfails insideAndroidBluetooth: its@JavaImplementationmacro generates invalid Swift forLowEnergyScanCallback.swiftOnScanFailed(error:)— the parameter namederroris emitted into a context where it parses as a keyword, producingdeclaration name '…' is not covered by macro 'JavaImplementation'. This is an upstream swift-java/AndroidBluetooth bug. - A whole-package Android build additionally fails in WasmKit's
SystemExtras: it doesst_mode & S_IFMT, butst_modeisUInt32in the Android sysroot while swift-system'sCInterop.ModeisUInt16. This target is not reachable from the app, so building specific targets avoids it; a plainswift builddoes not. - The Android project is still Skip's.
Android/settings.gradle.ktsshells out toskip plugin --prebuild, andAndroid/app/build.gradle.ktsappliesskip-build-pluginwith askip { }block that reads the now-deletedSkip.env. With Skip removed none of that resolves.
Replacing (3) means writing a conventional Android app project, using
AndroidSwiftUI's 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/<abi>/. 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.