如何将Swift Data与SwiftUI视图解耦并封装为Swift Package
问题
我在iOS平台上使用Swift Data(仅支持iOS 17及以上版本)和SwiftUI开发,现有一个RecordingEditorWidget视图,目前通过@Bindable宏与Swift Data的VoiceMemo、Recording模型高度耦合。现在希望将该视图封装为独立的Swift Package,使其不依赖或强制使用特定的Memo或Recording类型(在Package中包含顶层数据模型并不合理)。
以下是关键关联的简化代码示例:
App代码
import SwiftUI import SwiftData @main struct RecPackage_Stack_QApp: App { var body: some Scene { WindowGroup { ContentView() }.modelContainer(for: VoiceMemo.self) } }
数据模型代码
@Model final class VoiceMemo { var name: String @Relationship(deleteRule: .cascade) var recording: Recording? func replaceRecording(newRecording: Recording) { recording = newRecording } func deleteRecording() { recording = nil } init(name: String, recording: Recording? = nil) { self.name = name self.recording = recording } } @Model final class Recording { var attribute01:Double var attribute02:Double init(attribute01: Double = 0, attribute02: Double = 1) { self.attribute01 = attribute01 self.attribute02 = attribute02 } }
ContentView代码
import SwiftUI import SwiftData enum Navigation { case main, edit } struct ContentView: View { @State var showEditScreen: Bool = false @Query private var memos: [VoiceMemo] @Environment(\.modelContext) var modelContext @State private var memoToEdit: VoiceMemo? var body: some View { if !showEditScreen { VStack { Spacer() Button("ADD MEMO") { let newMemo = VoiceMemo(name: "new Memo...") modelContext.insert(newMemo) memoToEdit = newMemo showEditScreen = true } Spacer() ForEach(memos, id: \.self) { memo in HStack{ Button(memo.name) { memoToEdit = memo showEditScreen = true } if let rec = memo.recording { Text(rec.attribute01.description) Text(rec.attribute02.description) } } } Spacer() } .padding() } else { AddEditScreen(memo: memoToEdit!, showThisScreen: $showEditScreen) } } }
AddEditScreen代码
struct AddEditScreen: View { @Bindable var memo:VoiceMemo @Binding var showThisScreen:Bool var body: some View { VStack(alignment: .leading) { Button("<< BACK to main") { showThisScreen = false } Spacer() TextField("hello", text: $memo.name) Spacer() RecordingEditorWidget(memo: memo) }.padding(20) } }
待封装的RecordingEditorWidget代码
// 希望提取到独立Swift Package中的视图 struct RecordingEditorWidget: View { // 应移除对`VoiceMemo`和`Recording`类型的依赖 @Bindable var memo:VoiceMemo @State var isRecording: Bool = false enum RecordingState { case blank, recording, recorded } var getState: RecordingState { if isRecording { return .recording } else if memo.recording == nil { return .blank } else { return .recorded } } var body: some View { ZStack{ switch getState { case .blank: Button("Record") { isRecording = true // 模拟录制过程 DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { memo.recording = Recording() isRecording = false } } case .recording: Text("Recording in progress...") case .recorded: // 模拟音频编辑器 @Bindable var recording = memo.recording! VStack { Spacer() Slider(value: $recording.attribute01) Spacer() Slider(value: $recording.attribute02) Spacer() Button("Trash Recording") { memo.deleteRecording() } } } }.background(.green.opacity(0.2)) } }
解决方案
核心思路是通过抽象依赖,让RecordingEditorWidget只关注自身UI逻辑和必要的交互回调,完全脱离对外部数据模型的直接依赖。具体步骤如下:
1. 创建Swift Package并实现通用视图
在Package中编写解耦后的RecordingEditorWidget,通过绑定和闭包回调实现与外部的交互:
// Swift Package中的RecordingEditorWidget import SwiftUI struct RecordingEditorWidget: View { // 外部传入:是否已有录制内容 let hasRecording: Bool // 外部传入:两个可编辑属性的双向绑定 @Binding var attribute01: Double @Binding var attribute02: Double // 交互回调:触发录制开始逻辑 let onStartRecording: () -> Void // 交互回调:录制完成后通知外部处理数据 let onFinishRecording: () -> Void // 交互回调:触发删除录制内容逻辑 let onDeleteRecording: () -> Void @State private var isRecording: Bool = false private enum RecordingState { case blank, recording, recorded } private var currentState: RecordingState { if isRecording { return .recording } else if !hasRecording { return .blank } else { return .recorded } } var body: some View { ZStack { switch currentState { case .blank: Button("Record") { isRecording = true onStartRecording() // 模拟录制时长 DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { isRecording = false onFinishRecording() } } case .recording: Text("Recording in progress...") case .recorded: VStack { Spacer() Slider(value: $attribute01) Spacer() Slider(value: $attribute02) Spacer() Button("Trash Recording") { onDeleteRecording() } } } } .background(.green.opacity(0.2)) } } // Package内预览(可选) struct RecordingEditorWidget_Previews: PreviewProvider { static var previews: some View { RecordingEditorWidget( hasRecording: false, attribute01: .constant(0), attribute02: .constant(1), onStartRecording: {}, onFinishRecording: {}, onDeleteRecording: {} ) } }
2. 主项目中适配使用
修改AddEditScreen,将VoiceMemo的属性和操作映射到Package视图的参数上:
struct AddEditScreen: View { @Bindable var memo: VoiceMemo @Binding var showThisScreen: Bool // 临时存储录制前的属性值,避免未创建Recording时的绑定空值问题 @State private var tempAttribute01: Double = 0 @State private var tempAttribute02: Double = 1 var body: some View { VStack(alignment: .leading) { Button("<< BACK to main") { showThisScreen = false } Spacer() TextField("hello", text: $memo.name) Spacer() RecordingEditorWidget( hasRecording: memo.recording != nil, attribute01: bindingForAttribute01, attribute02: bindingForAttribute02, onStartRecording: { // 录制开始前的自定义逻辑(可选) }, onFinishRecording: { // 录制完成后创建Recording实例并关联到memo let newRecording = Recording( attribute01: tempAttribute01, attribute02: tempAttribute02 ) memo.replaceRecording(newRecording: newRecording) }, onDeleteRecording: { memo.deleteRecording() // 重置临时属性值 tempAttribute01 = 0 tempAttribute02 = 1 } ) } .padding(20) .onAppear { // 页面加载时同步已有Recording的属性到临时变量 if let recording = memo.recording { tempAttribute01 = recording.attribute01 tempAttribute02 = recording.attribute02 } } } // 自定义绑定:根据是否存在Recording切换绑定目标 private var bindingForAttribute01: Binding<Double> { memo.recording != nil ? $memo.recording!.attribute01 : $tempAttribute01 } private var bindingForAttribute02: Binding<Double> { memo.recording != nil ? $memo.recording!.attribute02 : $tempAttribute02 } }
3. 功能验证
修改完成后,RecordingEditorWidget完全独立于主项目的Swift Data模型:
- 不依赖任何外部自定义数据类型
- 所有数据交互通过绑定和闭包完成
- 完整保留原有的录制、编辑、删除功能
- 主项目只需负责将自身数据模型的逻辑映射到Package视图的参数中
内容的提问来源于stack exchange,提问作者Nerdy Bunz
相关产品推荐
相关产品推荐

