🔗 Framework 集成方式对比
KMP 将 shared 模块编译为一个原生 iOS Framework(shared.framework),Xcode 项目通过以下方式引用:
| 方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| SPM 直接集成 | Gradle 任务生成 Framework,Xcode Build Phase 自动调用 | 无需 CocoaPods;增量编译;官方推荐 | 配置稍复杂 |
| CocoaPods | KMP Gradle 插件生成 Podspec,pod install 引入 | 与现有 CocoaPods 工程无缝集成 | 需要安装 CocoaPods;每次修改需 pod install |
| XCFramework 手动 | 手动构建 XCFramework,拖入 Xcode | 最简单,适合演示 | 不适合开发迭代 |
📦 SPM 直接集成(推荐)
8.1 配置 shared 模块输出 Framework
shared/build.gradle.kts
kotlin {
val iosTargets = listOf(iosX64(), iosArm64(), iosSimulatorArm64())
iosTargets.forEach { target ->
target.binaries.framework {
baseName = "shared"
isStatic = true // 推荐静态 Framework,减少启动时间
}
}
}
8.2 在 Xcode 中配置 Build Phase
在 Xcode 项目中,选中 iosApp Target → Build Phases → 点击 + → New Run Script Phase,添加如下脚本:
Xcode Run Script Phase
cd "$SRCROOT/.."
./gradlew :shared:embedAndSignAppleFrameworkForXcode
将此 Phase 拖动到 "Compile Sources" 之前,确保 Framework 在编译前生成。
8.3 配置 Framework Search Paths
在 Xcode Build Settings 中,添加 Framework Search Paths:
Build Settings → Framework Search Paths
$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)
embedAndSignAppleFrameworkForXcode 任务 是 KMP Gradle 插件提供的,它会自动根据当前构建配置(Debug/Release)和目标 SDK(iphoneos/iphonesimulator)选择正确的 Framework 输出路径。
🥥 CocoaPods 集成
8.4 配置 KMP CocoaPods 插件
shared/build.gradle.kts — CocoaPods 方式
plugins {
id("org.jetbrains.kotlin.native.cocoapods")
}
kotlin {
cocoapods {
summary = "KMP Shared Module"
homepage = "https://github.com/example/newsapp"
version = "1.0"
ios.deploymentTarget = "16.0"
podfile = project.file("../iosApp/Podfile")
framework {
baseName = "shared"
isStatic = true
}
}
}
iosApp/Podfile
platform :ios, '16.0'
use_frameworks!
target 'iosApp' do
pod 'shared', :path => '../shared'
end
Terminal — 安装 Pods
cd iosApp
pod install
# 之后打开 iosApp.xcworkspace,而不是 .xcodeproj
open iosApp.xcworkspace
🔀 Swift 调用 Kotlin
8.5 Kotlin → ObjC/Swift 的类型映射
Kotlin/Native 会自动生成 Objective-C 兼容的头文件(shared.h),Swift 通过这些头文件调用 Kotlin 代码。
| Kotlin 类型 | ObjC/Swift 映射 | 注意事项 |
|---|---|---|
String | NSString / String | 自动桥接 |
Int / Long / Float | Int32 / Int64 / Float | 数值类型一一对应 |
List<T> | NSArray<T> | 不可变;需转为 Swift Array |
Map<K, V> | NSDictionary<K, V> | 自动桥接 |
data class | Swift class(不是 struct) | 无 Equatable/Hashable |
sealed class | 多个 ObjC class(非枚举) | SKIE 可改善为 Swift enum |
Flow<T> | 无直接映射 | 需 SKIE 或手动包装 |
suspend fun | 接受回调的函数 | SKIE 可改善为 async/await |
8.6 调用 Kotlin 类的基本示例
Swift
import shared // 导入 KMP Framework
// 调用 Kotlin 对象
let platform = Platform()
print(platform.name) // 输出: iOS
// 使用 Kotlin data class(注意:是引用类型)
let article = Article(
id: "1",
title: "Hello KMP",
summary: "test",
imageUrl: nil,
publishedAt: Clock.System.now(),
isBookmarked: false
)
// 调用 Kotlin 单例 object
Logger.shared.d(tag: "Swift", message: "Hello from iOS")
// sealed class 在没有 SKIE 时很麻烦
let state = viewModel.uiState.value
if let success = state as? ArticleListStateSuccess {
let articles = success.articles
}
✨ SKIE 库详解
8.7 SKIE 改善了什么
SKIE(Swift Kotlin Interface Enhancer)在编译时对 KMP Framework 进行增强,让 Swift 调用 Kotlin 代码更自然:
Flow → AsyncSequence
Kotlin Flow/StateFlow 自动变成 Swift AsyncSequence,可以用 for await 循环消费。
sealed class → Swift enum
Kotlin sealed class 变成 Swift enum,可以用 switch exhaustive 匹配,编译时确保完整性。
suspend fun → async/await
Kotlin suspend 函数变成 Swift async 函数,可以直接在 Swift 并发中使用,无需回调。
默认参数
Kotlin 函数的默认参数在 Swift 端也能正常使用,无需每次传入所有参数。
8.8 SKIE 安装
shared/build.gradle.kts
plugins {
id("co.touchlab.skie") version "0.9.0"
}
// 可选:SKIE 全局配置
skie {
features {
// 启用所有 SKIE 特性(默认全部开启)
coroutinesInterop.set(true)
sealedInterop.set(true)
defaultArgumentsInExternalModules.set(true)
}
}
8.9 SKIE 效果对比
无 SKIE — Swift 消费 sealed class
// 没有 SKIE:繁琐的类型转换
let state = viewModel.uiState.value
if let loading = state as? ArticleListStateLoading {
// 处理加载
} else if let success = state as? ArticleListStateSuccess {
updateUI(success.articles)
} else if let error = state as? ArticleListStateError {
showError(error.message)
}
有 SKIE — Swift 消费 sealed class
// SKIE 后:类型安全的 switch
switch viewModel.uiState.value {
case .loading:
showLoadingIndicator()
case .success(let success):
updateUI(success.articles)
case .error(let error):
showError(error.message)
}
📱 SwiftUI 消费共享 ViewModel
8.10 在 SwiftUI 中使用 ViewModel
iosApp/ContentView.swift
import SwiftUI
import shared // KMP Framework
@MainActor
class HomeViewModelWrapper: ObservableObject {
let viewModel: HomeViewModel
@Published var uiState: HomeUiState = HomeUiState(
articles: [],
isLoading: true,
isRefreshing: false,
error: nil,
searchQuery: ""
)
private var observeTask: Task<Void, Never>?
init() {
// 通过 Koin 获取 ViewModel
viewModel = KoinHelper.shared.getHomeViewModel()
}
func startObserving() {
observeTask = Task {
// SKIE 将 StateFlow 变为 AsyncSequence
for await state in viewModel.uiState {
self.uiState = state
}
}
}
func stopObserving() {
observeTask?.cancel()
}
func refresh() {
viewModel.refresh()
}
func toggleBookmark(_ id: String) {
viewModel.toggleBookmark(articleId: id)
}
}
HomeView.swift
struct HomeView: View {
@StateObject private var wrapper = HomeViewModelWrapper()
var body: some View {
NavigationStack {
Group {
if wrapper.uiState.isLoading {
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if let error = wrapper.uiState.error {
ErrorView(message: error) {
wrapper.refresh()
}
} else {
ArticleListView(
articles: wrapper.uiState.articles,
isRefreshing: wrapper.uiState.isRefreshing,
onRefresh: wrapper.refresh,
onBookmarkToggle: wrapper.toggleBookmark
)
}
}
.navigationTitle("每日资讯")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("刷新") { wrapper.refresh() }
}
}
}
.task {
wrapper.startObserving()
}
.onDisappear {
wrapper.stopObserving()
}
}
}
📱 完整 iOS 实战示例
8.11 文章列表 View
ArticleListView.swift
struct ArticleListView: View {
let articles: [Article]
let isRefreshing: Bool
let onRefresh: () -> Void
let onBookmarkToggle: (String) -> Void
var body: some View {
List {
ForEach(articles, id: \.id) { article in
NavigationLink(destination: ArticleDetailView(articleId: article.id)) {
ArticleRowView(
article: article,
onBookmarkToggle: { onBookmarkToggle(article.id) }
)
}
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
}
}
.listStyle(.plain)
.refreshable { onRefresh() }
.overlay {
if articles.isEmpty {
ContentUnavailableView(
"暂无内容",
systemImage: "newspaper",
description: Text("下拉刷新获取最新资讯")
)
}
}
}
}
struct ArticleRowView: View {
let article: Article
let onBookmarkToggle: () -> Void
var body: some View {
HStack(spacing: 12) {
// 封面图(AsyncImage 原生加载)
if let urlStr = article.imageUrl, let url = URL(string: urlStr) {
AsyncImage(url: url) { phase in
switch phase {
case .success(let image):
image.resizable()
.scaledToFill()
.frame(width: 80, height: 80)
.clipShape(RoundedRectangle(cornerRadius: 8))
default:
RoundedRectangle(cornerRadius: 8)
.fill(Color.secondary.opacity(0.2))
.frame(width: 80, height: 80)
}
}
}
VStack(alignment: .leading, spacing: 4) {
Text(article.title)
.font(.subheadline.weight(.semibold))
.lineLimit(2)
Text(article.summary)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
}
Spacer()
Button(action: onBookmarkToggle) {
Image(systemName: article.isBookmarked ? "bookmark.fill" : "bookmark")
.foregroundStyle(article.isBookmarked ? .purple : .secondary)
}
.buttonStyle(.borderless)
}
}
}
8.12 在 iOS 端初始化 Koin
iOSApp.swift
import SwiftUI
import shared
@main
struct iOSApp: App {
init() {
// 初始化 Koin
KoinHelperKt.doInitKoin()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
commonMain — KoinHelper(iOS 初始化辅助)
fun initKoin() {
startKoin {
modules(sharedModule, databaseModule)
}
}
线程注意: Kotlin/Native 在 iOS 上的 Coroutines 调度器默认运行在主线程(
Dispatchers.Main 映射到 iOS 主线程)。不要在 Kotlin suspend 函数中直接做 UI 操作,应在 Swift 端的 @MainActor 上下文中更新 UI 状态。
🎯 实践任务
- 配置 Xcode Run Script Phase,让 Gradle 构建 Framework
- 安装 SKIE 插件,验证 sealed class 变成 Swift switch-friendly enum
- 创建
HomeViewModelWrapper,用for await消费 StateFlow - 实现
ArticleListView和ArticleRowView,展示文章列表