应用开发
Skip 允许你在应用的 iOS 与 Android 版本之间自由决定共享多少代码。跨平台主题一章详细介绍如何集成 Android 或 iOS 特有代码;本章则聚焦于两个平台共享的开发模式。
下文假设你已经熟悉 iOS 开发。我们将重点说明 Skip 双平台开发与标准 iOS 开发的不同,包括如何使用 Skip 工具,以及出现问题时如何处理。
所有跨平台工具都有局限,Skip 也不例外。因此,Skip 让你可以直接从 Android 构建中排除不受支持的 iOS 代码。你可以在应用的 iOS 端使用任意 iOS 特性,而不必破坏项目结构。当某项特性在 Android 上不可用时,只需提供回退方案或替代实现。你可以从 Skip 已支持的数千个跨平台模块 ↗中寻找解决方案,也可以移植尚不能为 Android 编译的 Swift 包。
无论是为了绕过限制,还是打造有差异化的 Android 体验,Skip 都能让你轻松集成 Android 特有方案。
请记住:构建错误只是在告诉你,哪些内容可能尚未在 Android 上开箱即用。解决它们也许需要额外工作,但它们并不是不可逾越的障碍。
要在 Android 上运行和测试应用,你需要一个 Android 模拟器,或者一台已配对且启用开发者模式的 Android 设备。按照命令行参考中的说明,先运行 skip android emulator create,再运行 skip android emulator launch,即可创建并启动模拟器。
你也可以安装并启动 Android Studio.app,然后从欢迎界面的省略号菜单中打开 Device Manager,创建所需的模拟器。之后既可以通过 Device Manager 启动模拟器,也可以在终端中运行类似 ~/Library/Android/sdk/emulator/emulator @Pixel_6_API_33 的命令。
在 Android 真机上运行
Section titled “在 Android 真机上运行”要在已连接的 Android 设备上安装并运行应用,必须按照 ADB 文档 ↗在设备上启用 USB 调试,然后将 Android 设备与开发电脑配对。
请确保同一时间只有一台真机或一个模拟器处于运行状态,否则 Skip 无法判断应该在哪里启动应用。也可以在项目的 .xcconfig 文件中,将 ANDROID_SERIAL 变量设置为目标真机或模拟器的标识符。运行 /opt/homebrew/bin/adb devices 可查看所有已配对的可用标识符。
如果你按照创建应用中的说明使用 skip create 创建了项目,那么 Skip 应用每次成功构建后,都会自动尝试在正在运行的 Android 模拟器或设备上启动。必须恰好有一个模拟器或设备处于运行状态,Skip 项目的 Launch APK 脚本阶段才能成功安装并运行应用。
如果 Skip 的 Xcode 插件出现问题,请查看常见问题排查。
仅构建和运行 iOS
Section titled “仅构建和运行 iOS”默认情况下,每当你从 Xcode 运行 iOS 应用时,Skip 也会创建并运行 Android 应用。在迭代开发过程中并排构建和运行两端应用,有助于确保它们的外观和行为一致。
调试 iOS 特有问题时,可以编辑项目根目录中与 Package.swift 同级的 AppName.xcconfig 文件,将 SKIP_ACTION = launch 改为 SKIP_ACTION = build,从而禁止启动 Android 应用。这样仍会构建 Android 端,但不会在模拟器上启动。
如果要完全跳过 Android 构建,请在同一文件中设置 SKIP_ACTION = none。当你只专注于 iOS 时,这可以加快构建速度。
相互独立的 iOS 与 Android 应用
Section titled “相互独立的 iOS 与 Android 应用”如果选择创建彼此独立、但共享双平台 Swift 框架的 iOS 与 Android 应用,那么你需要在各自对应的 IDE 中构建和运行它们。项目类型指南提供了将双平台框架整合到开发流程中的建议。
在 Xcode 中构建双平台框架时,会构建 iOS 代码并运行 SkipStone 构建插件,但不会执行 Android 构建。受 Xcode 插件限制,调用 Android 编译器的方式只有两种:以 macOS 为目标运行模块的单元测试套件,或者导出框架的构建产物。详情请参阅测试与部署文档。
使用 Skip 编写双平台代码与开发标准 iOS 应用很相似,而看到自己的 Swift 与 SwiftUI 在 Android 上运行会是非常棒的体验。不过,同时面向两个平台也会带来纯 iOS 开发中没有的复杂性:
- 开发过程中,你很可能会希望使用某个尚未在 Android 上获得支持的 iOS API、框架或特性。本节会介绍遇到双平台覆盖限制时有哪些选择。
- 移植指南总结了编译跨平台 Swift 时常见的问题。此外,编译后的 Swift 必须通过桥接与 Android 的 Kotlin 和 Java API 交互。
- 编写双平台应用也意味着要使用双平台库。依赖管理文档介绍了如何使用其他双平台库,以及 iOS 或 Android 特有库。
当开发方向出现问题时,Skip 会尽可能早地发出警告。例如,Skip 构建插件可能在真正尝试为 Android 编译项目之前,就报告警告和错误。无论错误来自桥接、转译、Kotlin 编译还是原生编译,Skip 都会尽量将其映射回出错的 Swift 源代码,并在 Xcode 中显示。因此,每条错误消息通常会出现两次:一次直接显示在 Swift 源码中,另一次显示在 Xcode 侧边栏的问题导航器里。点击条目即可跳转到出错代码。
最常见的构建错误包括:
运行时错误与调试
Section titled “运行时错误与调试”处理错误是开发工作不可分割的一部分。请务必阅读调试一章,了解如何查看生成代码、读取日志,以及调试 Skip 框架或应用的 Android 端。
UI 与视图模型编码
Section titled “UI 与视图模型编码”Google 推荐使用 Jetpack Compose ↗ 开发 Android 用户界面。Skip 可以将 SwiftUI 的大部分内容转译为 Compose,让你使用 SwiftUI 构建跨 iOS 与 Android 的界面。你也可以在自己选择的 Android IDE 中,用纯 Compose 单独编写 Android UI。正如跨平台主题所述,Skip 甚至允许你在 SwiftUI 与 Compose 之间灵活切换。最终,是使用 SwiftUI、Jetpack Compose,还是将二者结合,由你决定。
@Observable 模型
Section titled “@Observable 模型”无论使用 Skip 转译的 SwiftUI,还是直接用 Kotlin 编写 Compose API,Skip 都会确保 @Observable 模型类型参与 Compose 状态跟踪。这样,它们就能像驱动 iOS 界面一样,无缝驱动 Android 用户界面。
@Observable 的集成过程是透明的,但用 Swift 模型驱动自定义 Compose UI 时需要注意:
- 任何定义
@Observable类型的 Swift 文件都必须import SkipFuse。该导入会启用状态跟踪,使 Android UI 与 Swift 模型保持同步。如果缺少此导入,SkipStone 构建插件会发出警告。 - 要从 Kotlin UI 使用
@Observable,请确保该@Observable已被桥接。 - 如果正在编写自定义 Compose UI,还必须添加对
SkipModel的 SwiftPM 依赖,@Observable才能在 Android 上正常工作,如下例所示。使用 SwiftUI 界面时不需要额外添加,因为对SkipFuseUI的依赖已经包含SkipModel。
...let package = Package( name: "travel-posters-model", ... dependencies: [ .package(url: "https://github.com/skiptools/skip.git", from: "1.2.0"), .package(url: "https://github.com/skiptools/skip-model.git", from: "1.0.0"), // <-- 在此添加 .package(url: "https://github.com/skiptools/skip-fuse.git", from: "1.0.0") ], targets: [ .target(name: "TravelPostersModel", dependencies: [ .product(name: "SkipFuse", package: "skip-fuse"), .product(name: "SkipModel", package: "skip-model") // <-- 在此添加 ], plugins: [.plugin(name: "skipstone", package: "skip")]), ... ])TravelPosters ↗ 示例中的 CityManager ↗ 类型,展示了如何在相互独立的 iOS 与 Android 应用之间共享 @Observable。
SwiftUI
Section titled “SwiftUI”Skip 允许你在两个平台之间共享全部或部分用户界面。由于 Skip 会在 Android 上把 SwiftUI 视图转换为 Jetpack Compose,最终两个平台得到的都是真正的原生界面,而不是对原生效果的跨平台模拟。
在 Android 上,import SwiftUI 会提供 SkipFuseUI,由它把 SwiftUI 视图桥接到 Jetpack Compose。View 及其 SwiftUI 属性必须使用默认(internal)或 public 可见性。私有视图和属性对 Skip 桥接层不可见,因此不会出现在 Android 上。下面是一个有效的跨平台 SwiftUI 视图:
import SwiftUI
struct MyView : View { @State var counter = 1 // 所有 SwiftUI 类型和成员都应使用 internal 或 public 访问级别 private let title = "..." // 非 SwiftUI 成员可以使用 private ...
var body: some View { ... }}SkipFuseUI 底层使用 SkipUI 用户界面库。SkipUI 文档列出了受支持的 SwiftUI 组件,并介绍了各种 SkipUI 主题。阅读这些内容可以帮助你在编写跨平台 SwiftUI 时避开常见问题。
Android 尚不支持的 iOS 特性
Section titled “Android 尚不支持的 iOS 特性”Skip 对 iOS API 的覆盖范围会随每个版本持续扩大,但仍有部分 iOS 框架和特性尚不能在 Android 上使用。当前覆盖情况请查看 SkipUI 支持的组件。
使用 Android 尚不支持的 iOS API,会触发“API 不可用”错误,或者 Android Swift 编译器的构建错误。遇到错误时,请先查看移植指南,确认该 API 是否其实可用,只是 Android 端需要使用不同的导入方式。如果正在编写 SwiftUI 代码,请查阅 SkipUI 模块了解支持范围。
当 Android 缺少所需 API 时,你仍有多种选择。可以尝试使用受支持的替代 API 完成任务。Swift Package Index ↗ 列出了许多已知能够为 Android 构建的跨平台 Swift 包。如果找不到现成方案,可以使用 Skip 的 iOS 与 Android 集成技术,分别实现 iOS 和 Android 代码路径,充分利用各平台的原生方案。如果所需 API 所属框架已经有 Android 镜像——无论是 Skip 开源库 ↗还是社区库——也许只需在现有库中补充缺失 API 即可。如果扩展了现有库,请考虑将改进贡献回 Skip 社区。按照这里的说明可以配置 Xcode,用于本地 Skip 库开发。
Skip Fuse 支持数千个第三方模块 ↗。如果这些模块无法满足需求,并且 Skip 库或社区库中也没有所需功能,可以考虑自行创建双平台库或共享 API。也欢迎将成果作为社区库贡献出来。
某些 iOS 应用扩展和功能尚未在 Android 上实现,或者在 Android 上没有直接对应物。请使用跨平台主题中的技术实现仅 iOS 或仅 Android 的方案。例如,可以通过编译器指令将 iOS 小组件排除在 Android 构建之外,再加入一个 Kotlin 文件,为 Android 实现原生小组件。
嵌入自定义 SwiftUI 与 Compose 视图
Section titled “嵌入自定义 SwiftUI 与 Compose 视图”Skip 让在共享 SwiftUI 代码旁结合平台特有 UI 框架变得非常容易。在 iOS 上可以使用任意 SwiftUI 视图,包括由 MapKit、Charts 或其他 Apple 框架支持的视图;在 Android 上,则可以在 #if SKIP 代码块内通过 ComposeView 直接进入 Jetpack Compose。
核心模式如下:
- 使用
#if !SKIP包裹 iOS 特有的导入和视图代码。 - 使用
#if SKIP(或#else分支)包裹 Android 特有的 Compose 导入和视图代码。 - 在 Android 上使用
ComposeView,从 SkipUI 视图层级桥接到原始 Jetpack Compose 可组合项。
下面来自 Skip Showcase 应用 ↗的示例展示了这种模式:在 iOS 上嵌入 Apple MapKit 的 Map,在 Android 上嵌入 Google Maps 的 GoogleMap 可组合项。
// Copyright 2023–2026 Skipimport SwiftUI#if !SKIPimport MapKit#else// 在 skip.yml 中添加依赖:implementation("com.google.maps.android:maps-compose:6.4.1")import com.google.maps.android.compose.__import com.google.android.gms.maps.model.CameraPositionimport com.google.android.gms.maps.model.LatLng#endif
struct MapPlayground: View { var body: some View { MapView(latitude: 48.8566, longitude: 2.3522) }}
struct MapView : View { let latitude: Double let longitude: Double
var body: some View { #if !SKIP // 在 Darwin 平台上使用新的 SwiftUI Map 类型 if #available(iOS 17.0, macOS 14.0, *) { Map(initialPosition: .region(MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: latitude, longitude: longitude), span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)))) } else { Text("Map requires iOS 17") .font(.title) } #else // 在 Android 平台上,通过 ComposeView 使用 com.google.maps.android.compose.GoogleMap ComposeView { ctx in GoogleMap(cameraPositionState: rememberCameraPositionState { position = CameraPosition.fromLatLngZoom(LatLng(latitude, longitude), Float(12.0)) }) } #endif }}嵌入传统 UIKit 与 Android XML 视图
Section titled “嵌入传统 UIKit 与 Android XML 视图”还可以在 Skip 应用中嵌入传统平台视图:iOS 上的 UIKit UIView 子类,以及 Android 上基于传统 XML 的 View 子类。当你需要集成没有 SwiftUI 或 Compose 对应实现的现有平台组件时,这种方式很有用。
在 iOS 上,使用 UIViewRepresentable(macOS 上使用 NSViewRepresentable)包装 UIKit 视图,以便在 SwiftUI 中使用。在 Android 上,则在 ComposeView 内使用 Compose 的 AndroidView 工厂包装传统 Android 视图,以便加入 SkipUI 视图层级。
下面来自 Skip Bookings 应用 ↗的示例展示了如何在 iOS 上嵌入 WKWebView,并在 Android 上嵌入 android.webkit.WebView:
import SwiftUI#if !SKIPimport WebKit#elseimport android.webkit.WebViewimport android.webkit.WebViewClientimport androidx.compose.ui.viewinterop.AndroidView#endif
// 将传统 UIKit.UIView/AndroidView 适配到 SwiftUI/Compose 所需的平台特有 View 父类型#if canImport(UIKit)typealias ViewAdapter = UIViewRepresentable#elseif canImport(AppKit)typealias ViewAdapter = NSViewRepresentable#elsetypealias ViewAdapter = View#endif
/// 这是一个可作为嵌入式浏览器视图使用的最简 WebView。/// 它不包含地址栏或导航按钮。/// 如需更高级的网页组件,请使用 https://github.com/skiptools/skip-webstruct WebView: ViewAdapter { let url: URL var enableJavaScript: Bool = true
#if SKIP // 在 Android 平台上,先用 AndroidView 包装 WebView, // 将传统视图适配到 Compose 上下文,再用 ComposeView 包装, // 使其集成到 SwiftUI 视图层级中 var body: some View { ComposeView { context in AndroidView(factory: { ctx in let webView = WebView(ctx) let client = WebViewClient() webView.webViewClient = client webView.settings.javaScriptEnabled = enableJavaScript webView.setBackgroundColor(0x000000) webView.loadUrl(url.absoluteString) return webView }, modifier: context.modifier, update: { webView in }) } } #else // 在 Darwin 平台上,使用 UIViewRepresentable/NSViewRepresentable // 适配系统加载 WKWebView,将传统 UIKit 视图接入 // SwiftUI 视图层级 func makeCoordinator() -> WKWebView { let webView = WKWebView(frame: .zero) webView.configuration.defaultWebpagePreferences.allowsContentJavaScript = enableJavaScript webView.load(URLRequest(url: url)) return webView }
func update(webView: WKWebView) { }
#if canImport(UIKit) func makeUIView(context: Context) -> WKWebView { context.coordinator } func updateUIView(_ uiView: WKWebView, context: Context) { update(webView: uiView) } #elseif canImport(AppKit) func makeNSView(context: Context) -> WKWebView { context.coordinator } func updateNSView(_ nsView: WKWebView, context: Context) { update(webView: nsView) } #endif #endif}要了解如何使用 Skip 处理本地化、资源与图片加载、JSON 编解码等常见跨平台开发任务,请参阅常见主题。