跳转到内容
Skip 中文站
3.2k

应用开发

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 Studio 设备管理器截图

要在已连接的 Android 设备上安装并运行应用,必须按照 ADB 文档 ↗在设备上启用 USB 调试,然后将 Android 设备与开发电脑配对。

如果你正准备搭建真机调试环境,可以从支持持续系统更新的 Android 设备和明确支持数据传输的 USB-C 配件开始。

推广链接说明:以下链接为 Amazon Associates 联盟链接。通过链接完成符合条件的购买后,本站可能获得佣金,你的购买价格不会因此增加。

Android 测试设备

用于真机检查不同屏幕尺寸、系统版本、深色模式、无障碍和平台交互。优先选择可解锁、仍能获得系统更新的机型。

查看 Android 测试设备推广链接 · Amazon.com 美国站

USB-C 扩展与连接

连接 Android 真机、外接显示器与调试配件。选购时注意数据传输规格、供电能力和端口组合。

查看 USB-C 扩展设备推广链接 · Amazon.com 美国站

As an Amazon Associate I earn from qualifying purchases.

请确保同一时间只有一台真机或一个模拟器处于运行状态,否则 Skip 无法判断应该在哪里启动应用。也可以在项目的 .xcconfig 文件中,将 ANDROID_SERIAL 变量设置为目标真机或模拟器的标识符。运行 /opt/homebrew/bin/adb devices 可查看所有已配对的可用标识符。

如果你按照创建应用中的说明使用 skip create 创建了项目,那么 Skip 应用每次成功构建后,都会自动尝试在正在运行的 Android 模拟器或设备上启动。必须恰好有一个模拟器或设备处于运行状态,Skip 项目的 Launch APK 脚本阶段才能成功安装并运行应用。

如果 Skip 的 Xcode 插件出现问题,请查看常见问题排查。

默认情况下,每当你从 Xcode 运行 iOS 应用时,Skip 也会创建并运行 Android 应用。在迭代开发过程中并排构建和运行两端应用,有助于确保它们的外观和行为一致。

调试 iOS 特有问题时,可以编辑项目根目录中与 Package.swift 同级的 AppName.xcconfig 文件,将 SKIP_ACTION = launch 改为 SKIP_ACTION = build,从而禁止启动 Android 应用。这样仍会构建 Android 端,但不会在模拟器上启动。

如果要完全跳过 Android 构建,请在同一文件中设置 SKIP_ACTION = none。当你只专注于 iOS 时,这可以加快构建速度。

如果选择创建彼此独立、但共享双平台 Swift 框架的 iOS 与 Android 应用,那么你需要在各自对应的 IDE 中构建和运行它们。项目类型指南提供了将双平台框架整合到开发流程中的建议。

在 Xcode 中构建双平台框架时,会构建 iOS 代码并运行 SkipStone 构建插件,但不会执行 Android 构建。受 Xcode 插件限制,调用 Android 编译器的方式只有两种:以 macOS 为目标运行模块的单元测试套件,或者导出框架的构建产物。详情请参阅测试与部署文档。

框架测试开发截图

使用 Skip 编写双平台代码与开发标准 iOS 应用很相似,而看到自己的 Swift 与 SwiftUI 在 Android 上运行会是非常棒的体验。不过,同时面向两个平台也会带来纯 iOS 开发中没有的复杂性:

  1. 开发过程中,你很可能会希望使用某个尚未在 Android 上获得支持的 iOS API、框架或特性。本节会介绍遇到双平台覆盖限制时有哪些选择。
  2. 移植指南总结了编译跨平台 Swift 时常见的问题。此外,编译后的 Swift 必须通过桥接与 Android 的 Kotlin 和 Java API 交互。
  3. 编写双平台应用也意味着要使用双平台库。依赖管理文档介绍了如何使用其他双平台库,以及 iOS 或 Android 特有库。

当开发方向出现问题时,Skip 会尽可能早地发出警告。例如,Skip 构建插件可能在真正尝试为 Android 编译项目之前,就报告警告和错误。无论错误来自桥接、转译、Kotlin 编译还是原生编译,Skip 都会尽量将其映射回出错的 Swift 源代码,并在 Xcode 中显示。因此,每条错误消息通常会出现两次:一次直接显示在 Swift 源码中,另一次显示在 Xcode 侧边栏的问题导航器里。点击条目即可跳转到出错代码。

Kotlin 编译错误截图

最常见的构建错误包括:

  • 使用了 Android 尚不支持的 API。对于尚未移植到 Android 的 API,下文会介绍可选方案。
  • 跨平台 Swift 需要调整 import。详情请查阅移植指南。

处理错误是开发工作不可分割的一部分。请务必阅读调试一章,了解如何查看生成代码、读取日志,以及调试 Skip 框架或应用的 Android 端。


Google 推荐使用 Jetpack Compose ↗ 开发 Android 用户界面。Skip 可以将 SwiftUI 的大部分内容转译为 Compose,让你使用 SwiftUI 构建跨 iOS 与 Android 的界面。你也可以在自己选择的 Android IDE 中,用纯 Compose 单独编写 Android UI。正如跨平台主题所述,Skip 甚至允许你在 SwiftUI 与 Compose 之间灵活切换。最终,是使用 SwiftUI、Jetpack Compose,还是将二者结合,由你决定。

无论使用 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。

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 时避开常见问题。


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。

核心模式如下:

  1. 使用 #if !SKIP 包裹 iOS 特有的导入和视图代码。
  2. 使用 #if SKIP(或 #else 分支)包裹 Android 特有的 Compose 导入和视图代码。
  3. 在 Android 上使用 ComposeView,从 SkipUI 视图层级桥接到原始 Jetpack Compose 可组合项。

下面来自 Skip Showcase 应用 ↗的示例展示了这种模式:在 iOS 上嵌入 Apple MapKit 的 Map,在 Android 上嵌入 Google Maps 的 GoogleMap 可组合项。

// Copyright 2023–2026 Skip
import SwiftUI
#if !SKIP
import 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.CameraPosition
import 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
}
}

还可以在 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 !SKIP
import WebKit
#else
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.ui.viewinterop.AndroidView
#endif
// 将传统 UIKit.UIView/AndroidView 适配到 SwiftUI/Compose 所需的平台特有 View 父类型
#if canImport(UIKit)
typealias ViewAdapter = UIViewRepresentable
#elseif canImport(AppKit)
typealias ViewAdapter = NSViewRepresentable
#else
typealias ViewAdapter = View
#endif
/// 这是一个可作为嵌入式浏览器视图使用的最简 WebView。
/// 它不包含地址栏或导航按钮。
/// 如需更高级的网页组件,请使用 https://github.com/skiptools/skip-web
struct 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 编解码等常见跨平台开发任务,请参阅常见主题。